UnixCompressFilter.java
- /* Copyright 2002-2025 CS GROUP
- * Licensed to CS GROUP (CS) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * CS licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- package org.orekit.data;
- import java.io.IOException;
- import java.io.InputStream;
- import java.util.Arrays;
- import org.hipparchus.util.FastMath;
- import org.orekit.errors.OrekitException;
- import org.orekit.errors.OrekitIOException;
- import org.orekit.errors.OrekitMessages;
- /** Filter for Unix compressed data.
- * @author Luc Maisonobe
- * @since 9.2
- */
- public class UnixCompressFilter implements DataFilter {
- /** Suffix for Unix compressed files. */
- private static final String SUFFIX = ".Z";
- /** Empty constructor.
- * <p>
- * This constructor is not strictly necessary, but it prevents spurious
- * javadoc warnings with JDK 18 and later.
- * </p>
- * @since 12.0
- */
- public UnixCompressFilter() {
- // nothing to do
- }
- /** {@inheritDoc} */
- @Override
- public DataSource filter(final DataSource original) {
- final String oName = original.getName();
- final DataSource.Opener oOpener = original.getOpener();
- if (oName.endsWith(SUFFIX)) {
- final String fName = oName.substring(0, oName.length() - SUFFIX.length());
- final DataSource.StreamOpener fOpener = () -> new ZInputStream(oName, new Buffer(oOpener.openStreamOnce()));
- return new DataSource(fName, fOpener);
- } else {
- return original;
- }
- }
- /** Filtering of Unix compressed stream. */
- private static class ZInputStream extends InputStream {
- /** First magic header byte. */
- private static final int MAGIC_HEADER_1 = 0x1f;
- /** Second magic header byte. */
- private static final int MAGIC_HEADER_2 = 0x9d;
- /** Byte bits width. */
- private static final int BYTE_WIDTH = 8;
- /** Initial bits width. */
- private static final int INIT_WIDTH = 9;
- /** Reset table code. */
- private static final int RESET_TABLE = 256;
- /** First non-predefined entry. */
- private static final int FIRST = 257;
- /** File name. */
- private final String name;
- /** Indicator for end of input. */
- private boolean endOfInput;
- /** Common sequences table. */
- private final UncompressedSequence[] table;
- /** Next available entry in the table. */
- private int available;
- /** Flag for block mode when table is full. */
- private final boolean blockMode;
- /** Maximum width allowed. */
- private final int maxWidth;
- /** Current input width in bits. */
- private int currentWidth;
- /** Maximum key that can be encoded with current width. */
- private int currentMaxKey;
- /** Number of bits read since last reset. */
- private int bitsRead;
- /** Lookahead byte, already read but not yet used. */
- private int lookAhead;
- /** Number of bits in the lookahead byte. */
- private int lookAheadWidth;
- /** Input buffer. */
- private Buffer input;
- /** Previous uncompressed sequence output. */
- private UncompressedSequence previousSequence;
- /** Uncompressed sequence being output. */
- private UncompressedSequence currentSequence;
- /** Number of bytes of the current sequence already output. */
- private int alreadyOutput;
- /** Simple constructor.
- * @param name file name
- * @param input underlying compressed stream
- * @exception IOException if first bytes cannot be read
- */
- ZInputStream(final String name, final Buffer input)
- throws IOException {
- this.name = name;
- this.input = input;
- this.endOfInput = false;
- // check header
- if (input.getByte() != MAGIC_HEADER_1 || input.getByte() != MAGIC_HEADER_2) {
- throw new OrekitException(OrekitMessages.NOT_A_SUPPORTED_UNIX_COMPRESSED_FILE, name);
- }
- final int header3 = input.getByte();
- this.blockMode = (header3 & 0x80) != 0;
- this.maxWidth = header3 & 0x1f;
- // set up table, with at least all entries for one byte
- this.table = new UncompressedSequence[1 << FastMath.max(INIT_WIDTH, maxWidth)];
- for (int i = 0; i < FIRST; ++i) {
- table[i] = new UncompressedSequence(null, (byte) i);
- }
- // initialize decompression state
- initialize();
- }
- /** Initialize compression state.
- */
- private void initialize() {
- this.available = FIRST;
- this.bitsRead = 0;
- this.lookAhead = 0;
- this.lookAheadWidth = 0;
- this.currentWidth = INIT_WIDTH;
- this.currentMaxKey = (1 << currentWidth) - 1;
- this.previousSequence = null;
- this.currentSequence = null;
- this.alreadyOutput = 0;
- }
- /** Read next input key.
- * @return next input key or -1 if end of stream is reached
- * @exception IOException if a read error occurs
- */
- private int nextKey() throws IOException {
- int keyMask = (1 << currentWidth) - 1;
- while (true) {
- // initialize key with the last bits remaining from previous read
- int key = lookAhead & keyMask;
- // read more bits until key is complete
- for (int remaining = currentWidth - lookAheadWidth; remaining > 0; remaining -= BYTE_WIDTH) {
- lookAhead = input.getByte();
- lookAheadWidth += BYTE_WIDTH;
- if (lookAhead < 0) {
- if (key == 0 || key == keyMask) {
- // the key is either a set of padding 0 bits
- // or a full key containing -1 if read() is called several times after EOF
- return -1;
- } else {
- // end of stream encountered in the middle of a read
- throw new OrekitIOException(OrekitMessages.UNEXPECTED_END_OF_FILE, name);
- }
- }
- key = (key | lookAhead << (currentWidth - remaining)) & keyMask;
- }
- // store the extra bits already read in the lookahead byte for next call
- lookAheadWidth -= currentWidth;
- lookAhead = lookAhead >>> (BYTE_WIDTH - lookAheadWidth);
- bitsRead += currentWidth;
- if (blockMode && key == RESET_TABLE) {
- // skip the padding bits inserted when compressor flushed its buffer
- final int superSize = currentWidth * 8;
- int padding = (superSize - 1 - (bitsRead + superSize - 1) % superSize) / 8;
- while (padding-- > 0) {
- input.getByte();
- }
- // reset the table to handle a new block and read again next key
- Arrays.fill(table, FIRST, table.length, null);
- initialize();
- // reset the lookahead mask as the current width has changed
- keyMask = (1 << currentWidth) - 1;
- } else {
- // return key at current width
- return key;
- }
- }
- }
- /** Select next uncompressed sequence to output.
- * @return true if there is a next sequence
- * @exception IOException if a read error occurs
- */
- private boolean selectNext() throws IOException {
- // read next input key
- final int key = nextKey();
- if (key < 0) {
- // end of stream reached
- return false;
- }
- if (previousSequence != null && available < table.length) {
- // update the table with the next uncompressed byte appended to previous sequence
- final byte nextByte;
- if (key == available) {
- nextByte = previousSequence.getByte(0);
- } else if (table[key] != null) {
- nextByte = table[key].getByte(0);
- } else {
- throw new OrekitIOException(OrekitMessages.CORRUPTED_FILE, name);
- }
- table[available++] = new UncompressedSequence(previousSequence, nextByte);
- if (available > currentMaxKey && currentWidth < maxWidth) {
- // we need to increase the key size
- currentMaxKey = (1 << ++currentWidth) - 1;
- }
- }
- currentSequence = table[key];
- if (currentSequence == null) {
- // the compressed file references a non-existent table entry
- // (this is not the well-known case of entry being used just before
- // being defined, which is already handled above), the file is corrupted
- throw new OrekitIOException(OrekitMessages.CORRUPTED_FILE, name);
- }
- alreadyOutput = 0;
- return true;
- }
- /** {@inheritDoc} */
- @Override
- public int read() throws IOException {
- final byte[] b = new byte[1];
- return read(b, 0, 1) < 0 ? -1 : b[0];
- }
- /** {@inheritDoc} */
- @Override
- public int read(final byte[] b, final int offset, final int len) throws IOException {
- if (currentSequence == null) {
- if (endOfInput || !selectNext()) {
- // we have reached end of data
- endOfInput = true;
- return -1;
- }
- }
- // copy as many bytes as possible from current sequence
- final int n = FastMath.min(len, currentSequence.length() - alreadyOutput);
- for (int i = 0; i < n; ++i) {
- b[offset + i] = currentSequence.getByte(alreadyOutput++);
- }
- if (alreadyOutput >= currentSequence.length()) {
- // we have just exhausted the current sequence
- previousSequence = currentSequence;
- currentSequence = null;
- alreadyOutput = 0;
- }
- return n;
- }
- /** {@inheritDoc} */
- @Override
- public int available() {
- return currentSequence == null ? 0 : currentSequence.length() - alreadyOutput;
- }
- }
- /** Uncompressed bits sequence. */
- private static class UncompressedSequence {
- /** Prefix sequence (null if this is a start sequence). */
- private final UncompressedSequence prefix;
- /** Last byte in the sequence. */
- private final byte last;
- /** Index of the last byte in the sequence (i.e. length - 1). */
- private final int index;
- /** Simple constructor.
- * @param prefix prefix of the sequence (null if this is a start sequence)
- * @param last last byte of the sequence
- */
- UncompressedSequence(final UncompressedSequence prefix, final byte last) {
- this.prefix = prefix;
- this.last = last;
- this.index = prefix == null ? 0 : prefix.index + 1;
- }
- /** Get the length of the sequence.
- * @return length of the sequence
- */
- public int length() {
- return index + 1;
- }
- /** Get a byte from the sequence.
- * @param outputIndex index of the byte in the sequence, counting from 0
- * @return byte at {@code outputIndex}
- */
- public byte getByte(final int outputIndex) {
- return index == outputIndex ? last : prefix.getByte(outputIndex);
- }
- }
- /** Buffer for reading input data. */
- private static class Buffer {
- /** Size of input/output buffers. */
- private static final int BUFFER_SIZE = 4096;
- /** Underlying compressed stream. */
- private final InputStream input;
- /** Buffer data. */
- private final byte[] data;
- /** Start of pending data. */
- private int start;
- /** End of pending data. */
- private int end;
- /** Simple constructor.
- * @param input input stream
- */
- Buffer(final InputStream input) {
- this.input = input;
- this.data = new byte[BUFFER_SIZE];
- this.start = 0;
- this.end = start;
- }
- /** Get one input byte.
- * @return input byte, or -1 if end of input has been reached
- * @throws IOException if input data cannot be read
- */
- private int getByte() throws IOException {
- if (start == end) {
- // the buffer is empty
- start = 0;
- end = input.read(data);
- if (end == -1) {
- return -1;
- }
- }
- return data[start++] & 0xFF;
- }
- }
- }