Updating to Java 25-isms. Also accepting multiple IDE refactorings.

This commit is contained in:
a2geek committed 2026-09-19 16:28:57 -05:00
1 parent a7f72b2278
commit 88c47e63fc
29 files changed
+248 -304

No files matched your search

+1 -1
View File
@@ -65,7 +65,7 @@ jobs:
strategy:
matrix:
# See: https://docs.github.com/en/actions/concepts/runners/about-github-hosted-runners#overview-of-github-hosted-runners
# ... and follow the links to the related Github repositories
# ... and follow the links to the related GitHub repositories
include:
- label: ubuntu-latest
os: linux
+2 -2
View File
@@ -1,8 +1,8 @@
# ShrinkIt Archive Library for Java
This project is a direct off-shoot of the AppleCommander project and implements a pure Java API to manipulate ShrinkIt (NuFX)
archives ([Apple File Type Note $E0/8002](http://www.apple2online.com/web_documents/ft_e0.8002_shrinkit.pdf)).
archives ([Apple File Type Note $E0/8002](https://web.archive.org/web/20200218012014/https://apple2online.com/web_documents/ft_e0.8002_shrinkit.pdf)).
*Developers*
Being new to Gradle, all of the Maven release components are in the `build.gradle` file. More a warning than anything, just in case it causes issues. Please make more portable and submit a PR.
Being new to Gradle, all the Maven release components are in the `build.gradle` file. More a warning than anything, just in case it causes issues. Please make more portable and submit a PR.
+1 -1
View File
@@ -5,5 +5,5 @@ platform=$(uname | tr '[:upper:]' '[:lower:]' | sed 's/darwin/macosx/')
machine=$(uname -m | tr '[:upper:]' '[:lower:]' | sed 's/arm64/aarch64/')
# These are useful aliases while developing
alias nufxscan="java -jar ${PWD}/build/libs/ShrinkItArchive-*.jar"
alias nufxscan="java -jar ${PWD}/$(find build/libs/ -iname '*.jar' -not -iname '*sources*' -not -iname '*javadoc*')"
alias nufxscann="${PWD}/build/native/nativeCompile/NufxScan-*"
@@ -56,7 +56,7 @@ public class HeaderBlock {
private String filename;
private String rawFilename;
private long headerSize = 0;
private List<ThreadRecord> threads = new ArrayList<ThreadRecord>();
private List<ThreadRecord> threads = new ArrayList<>();
/**
* Create the Header Block. This is done dynamically since
@@ -65,7 +65,7 @@ public class HeaderBlock {
public HeaderBlock(LittleEndianByteInputStream bs) throws IOException {
int type = bs.seekFileType(4);
if (type == 0) {
throw new IOException("Unable to decode this archive."); // FIXME - NLS
throw new IOException("Unable to decode this archive.");
}
headerCrc = bs.readWord();
attribCount = bs.readWord();
@@ -170,7 +170,7 @@ public class HeaderBlock {
}
/**
* Locate a ThreadRecord by it's ThreadKind.
* Locate a ThreadRecord by its ThreadKind.
*/
protected ThreadRecord findThreadRecord(ThreadKind tk) {
for (ThreadRecord r : threads) {
@@ -186,20 +186,13 @@ public class HeaderBlock {
* Due to some oddities, breaking apart by byte value...
*/
public String getFileSystemSeparator() {
switch (getFileSysInfo() & 0xff) {
case 0xaf:
case 0x2f:
return "/";
case 0x3a:
case 0xba:
case 0x3f: // Note that $3F is per the documentation(!)
return ":";
case 0x5c:
case 0xdc:
return "\\";
default:
return "";
}
return switch (getFileSysInfo() & 0xff) {
case 0xaf, 0x2f -> "/";
// Note that $3F is per the documentation(!)
case 0x3a, 0xba, 0x3f -> ":";
case 0x5c, 0xdc -> "\\";
default -> "";
};
}
public long getUncompressedSize() {
@@ -221,6 +214,24 @@ public class HeaderBlock {
return size;
}
public String getFileSysIdText() {
return switch (getFileSysInfo()) {
case 1 -> "ProDOS/SOS";
case 2 -> "DOS 3.3";
case 3 -> "DOS 3.2";
case 4 -> "Apple II Pascal";
case 5 -> "Macintosh HFS";
case 6 -> "Macintosh MFS";
case 7 -> "Lisa File System";
case 8 -> "Apple CP/M";
case 10 -> "MS-DOS";
case 11 -> "High Sierra";
case 12 -> "ISO 9660";
case 13 -> "AppleShare";
default -> "Reserved";
};
}
// GENERATED CODE
public int getHeaderCrc() {
@@ -1,32 +0,0 @@
/*
* ShrinkItArchive
* Copyright (C) 2026 Rob Greene
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, see
* <https://www.gnu.org/licenses/>.
*/
package org.applecommander.shrinkit;
/**
* Handy reference for constant values based on header blocks.
* These are likely user interface oriented.
*
* @author rob
*/
public interface HeaderConstants {
public static final String[] FILE_SYS = {
"Reserved", "ProDOS/SOS", "DOS 3.3", "DOS 3.2", "Apple II Pascal", "Macintosh HFS", "Macintosh MFS",
"Lisa File System", "Apple CP/M", "Reserved", "MS-DOS", "High Sierra", "ISO 9660", "AppleShare"
};
}
@@ -38,7 +38,7 @@ import org.applecommander.shrinkit.io.LittleEndianByteInputStream;
public class MasterHeaderBlock {
private static final int MASTER_HEADER_LENGTH = 48;
private int masterCrc;
private boolean validCrc;
private final boolean validCrc;
private long totalRecords;
private Date archiveCreateWhen;
private Date archiveModWhen;
@@ -57,12 +57,12 @@ public class MasterHeaderBlock {
headerOffset = 128;
int count = bs.read();
if (count != 0) {
throw new IOException("This is actually a Binary II archive with multiple files in it."); // FIXME - NLS
throw new IOException("This is actually a Binary II archive with multiple files in it.");
}
fileType = bs.seekFileType();
}
if (!(fileType == NuFileArchive.NUFILE_ARCHIVE)) {
throw new IOException("Unable to decode this archive."); // FIXME - NLS
throw new IOException("Unable to decode this archive.");
}
masterCrc = bs.readWord();
bs.resetCrc(); // CRC is computed from this point to the end of the header
@@ -37,12 +37,12 @@ public class NuFileArchive {
VERSION = NuFileArchive.class.getPackage().getImplementationVersion();
}
private MasterHeaderBlock master;
private List<HeaderBlock> headers;
private final MasterHeaderBlock master;
private final List<HeaderBlock> headers;
private long totalSize = 0;
/**
* Need to enumerate some basic sub-types of archives.
* Need to enumerate some basic subtypes of archives.
*/
public static final int NUFILE_ARCHIVE = 1;
public static final int NUFX_ARCHIVE = 2;
@@ -54,7 +54,7 @@ public class NuFileArchive {
public NuFileArchive(InputStream inputStream) throws IOException {
LittleEndianByteInputStream bs = new LittleEndianByteInputStream(inputStream);
master = new MasterHeaderBlock(bs);
headers = new ArrayList<HeaderBlock>();
headers = new ArrayList<>();
for (int i=0; i<master.getTotalRecords(); i++) {
HeaderBlock header = new HeaderBlock(bs);
header.readThreads(bs);
@@ -19,7 +19,6 @@
package org.applecommander.shrinkit;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
@@ -40,16 +39,18 @@ public class NufxScan {
private static boolean verify = false;
public static void main(String[] args) throws IOException {
static void main(String[] args) throws IOException {
if (args.length == 0) {
System.out.println("Usage: nufxscan [ -v | --verify ] <path> ...");
System.out.println();
System.out.println("Scan NuFX/Shrinkit archives. Please include at least one path name.");
System.out.println();
System.out.println("Options:");
System.out.println(" -v Show version and exit.");
System.out.println(" --verify Enable verify flag. Scan becomes quiet and only reports files that");
System.out.println(" the library cannot process. Helpful in identifying bugs.");
System.out.println("""
Usage: nufxscan [ -v | --verify ] <path> ...
Scan NuFX/Shrinkit archives. Please include at least one path name.
Options:
-v Show version and exit.
--verify Enable verify flag. Scan becomes quiet and only reports files that
the library cannot process. Helpful in identifying bugs.
""");
} else if (args.length == 1 && "-v".equals(args[0])) {
System.out.printf("ShrinkIt Library version %s\n", NuFileArchive.VERSION);
} else {
@@ -77,25 +78,27 @@ public class NufxScan {
private static void scanDirectory(File directory) throws IOException {
if (!directory.isDirectory()) {
throw new IllegalArgumentException("'" + directory.toString() + "' is not a directory");
throw new IllegalArgumentException("'" + directory + "' is not a directory");
}
File[] files = directory.listFiles(new FileFilter() {
public boolean accept(File file) {
boolean isSHK = file.getName().toLowerCase().endsWith(".shk");
boolean isSDK = file.getName().toLowerCase().endsWith(".sdk");
boolean isDirectory = file.isDirectory();
return isSHK || isSDK || isDirectory;
}
});
for (File file : files) {
if (file.isDirectory()) {
scanDirectory(file);
} else {
if (verify) {
verifyArchive(file);
}
else {
displayArchive(file);
File[] files = directory.listFiles(file -> {
boolean isSHK = file.getName().toLowerCase().endsWith(".shk");
boolean isSDK = file.getName().toLowerCase().endsWith(".sdk");
boolean isDirectory = file.isDirectory();
return isSHK || isSDK || isDirectory;
});
if (files == null) {
System.out.println("No ShrinkIt archives found.");
}
else {
for (File file : files) {
if (file.isDirectory()) {
scanDirectory(file);
} else {
if (verify) {
verifyArchive(file);
} else {
displayArchive(file);
}
}
}
}
@@ -104,7 +107,7 @@ public class NufxScan {
private static void displayArchive(File archive) throws IOException {
System.out.printf("Details for %s\n\n", archive.getAbsoluteFile());
try (InputStream is = new FileInputStream(archive)) {
NuFileArchive a = null;
NuFileArchive a;
try {
a = new NuFileArchive(is);
} catch (Throwable t) {
@@ -139,7 +142,7 @@ public class NufxScan {
threadsPrinted++;
}
System.out.printf("%08x %08x ", origSize, compSize);
if (filename == null || filename.length() == 0) {
if (filename == null || filename.isEmpty()) {
filename = "<Unknown>";
}
System.out.println(filename);
@@ -155,7 +158,7 @@ public class NufxScan {
private static void verifyArchive(File archive) throws IOException {
try (InputStream is = new FileInputStream(archive)) {
NuFileArchive a = null;
NuFileArchive a;
try {
a = new NuFileArchive(is);
} catch (Throwable t) {
@@ -30,13 +30,12 @@ public enum ThreadClass {
* @throws IllegalArgumentException if the thread_class is unknown
*/
public static ThreadClass find(int threadClass) {
switch (threadClass) {
case 0x0000: return MESSAGE;
case 0x0001: return CONTROL;
case 0x0002: return DATA;
case 0x0003: return FILENAME;
default:
throw new IllegalArgumentException("Unknown thread_class of " + threadClass);
}
return switch (threadClass) {
case 0x0000 -> MESSAGE;
case 0x0001 -> CONTROL;
case 0x0002 -> DATA;
case 0x0003 -> FILENAME;
default -> throw new IllegalArgumentException("Unknown thread_class of " + threadClass);
};
}
}
@@ -34,7 +34,7 @@ public enum ThreadFormat {
private final int threadFormat;
private final String name;
private ThreadFormat(int threadFormat, String name) {
ThreadFormat(int threadFormat, String name) {
this.threadFormat = threadFormat;
this.name = name;
}
@@ -31,29 +31,31 @@ public enum ThreadKind {
* @throws IllegalArgumentException when the thread_kind cannot be determined
*/
public static ThreadKind find(int threadKind, ThreadClass threadClass) {
switch (threadClass) {
case MESSAGE:
switch (threadKind) {
case 0x0000: return ASCII_TEXT;
case 0x0001: return ALLOCATED_SPACE;
case 0x0002: return APPLE_IIGS_ICON;
}
throw new IllegalArgumentException("Unknown thread_kind "+threadKind+" for message thread_class of " + threadClass);
case CONTROL:
if (threadKind == 0x0000) return CREATE_DIRECTORY;
throw new IllegalArgumentException("Unknown thread_kind "+threadKind+" for control thread_class of " + threadClass);
case DATA:
switch (threadKind) {
case 0x0000: return DATA_FORK;
case 0x0001: return DISK_IMAGE;
case 0x0002: return RESOURCE_FORK;
}
throw new IllegalArgumentException("Unknown thread_kind "+threadKind+" for data thread_class of " + threadClass);
case FILENAME:
if (threadKind == 0x0000) return FILENAME;
throw new IllegalArgumentException("Unknown thread_kind "+threadKind+" for filename thread_class of " + threadClass);
default:
throw new IllegalArgumentException("Unknown thread_class of " + threadClass);
}
return switch (threadClass) {
case MESSAGE -> switch (threadKind) {
case 0x0000 -> ASCII_TEXT;
case 0x0001 -> ALLOCATED_SPACE;
case 0x0002 -> APPLE_IIGS_ICON;
default -> throw new IllegalArgumentException("Unknown thread_kind " + threadKind
+ " for message thread_class of " + threadClass);
};
case CONTROL -> {
if (threadKind == 0x0000) yield CREATE_DIRECTORY;
throw new IllegalArgumentException("Unknown thread_kind " + threadKind
+ " for control thread_class of " + threadClass);
}
case DATA -> switch (threadKind) {
case 0x0000 -> DATA_FORK;
case 0x0001 -> DISK_IMAGE;
case 0x0002 -> RESOURCE_FORK;
default -> throw new IllegalArgumentException("Unknown thread_kind " + threadKind
+ " for data thread_class of " + threadClass);
};
case FILENAME -> {
if (threadKind == 0x0000) yield FILENAME;
throw new IllegalArgumentException("Unknown thread_kind " + threadKind
+ " for filename thread_class of " + threadClass);
}
};
}
}
@@ -34,7 +34,7 @@ import org.applecommander.shrinkit.io.NufxLzw2InputStream;
* <p>
* Depending on the type of thread, the data may be text. If so,
* <code>isText</code> will return true and <code>getText</code>
* will return the string. Otherwise the data should be read through
* will return the string. Otherwise, the data should be read through
* one of the <code>InputStream</code> options.
*
* @author robgreene@users.sourceforge.net
@@ -72,8 +72,7 @@ public class ThreadRecord {
/* supposed to be block size, but SHK v3.0.1 stored it wrong */
threadEof = hb.getExtraType() * 512;
// System.out.println("Found erroneous storage type... fixing.");
} else if (hb.getStorageType() == 256 &&
hb.getExtraType() == 280 &&
} else if (hb.getStorageType() == 256 && hb.getExtraType() == 280 &&
hb.getFileSysId() == 2 ) { // FileSysDOS33
/*
* Fix for less-common ShrinkIt problem: looks like an old
@@ -122,16 +121,13 @@ public class ThreadRecord {
* Get the appropriate input data stream for this thread to decompress the contents.
*/
public InputStream getInputStream() throws IOException {
switch (threadFormat) {
case UNCOMPRESSED:
return getRawInputStream();
case DYNAMIC_LZW1:
return new NufxLzw1InputStream(new LittleEndianByteInputStream(getRawInputStream()));
case DYNAMIC_LZW2:
return new NufxLzw2InputStream(new LittleEndianByteInputStream(getRawInputStream()));
default:
throw new IOException("The thread format " + threadFormat + " does not have an InputStream associated with it!");
}
return switch (threadFormat) {
case UNCOMPRESSED -> getRawInputStream();
case DYNAMIC_LZW1 -> new NufxLzw1InputStream(new LittleEndianByteInputStream(getRawInputStream()));
case DYNAMIC_LZW2 -> new NufxLzw2InputStream(new LittleEndianByteInputStream(getRawInputStream()));
default -> throw new IOException("The thread format " + threadFormat
+ " does not have an InputStream associated with it!");
};
}
// GENERATED CODE
@@ -28,7 +28,7 @@ public interface BitConstants {
/**
* The low-tech way to compute a bit mask. Allowing up to 16 bits at this time.
*/
public static final int[] BIT_MASKS = new int[] {
int[] BIT_MASKS = new int[] {
0x0000, 0x0001, 0x0003, 0x0007, 0x000f,
0x001f, 0x003f, 0x007f, 0x00ff, 0x01ff,
0x03ff, 0x07ff, 0x0fff, 0x1fff, 0x3fff,
@@ -21,20 +21,18 @@ package org.applecommander.shrinkit.io;
import java.io.IOException;
import java.io.InputStream;
/**
* The BitInputStream allows varying bit sizes to be pulled out of the
* wrapped InputStream. This is useful for LZW type compression algorithms
* where 9-12 bit codes are used instead of the 8-bit byte.
* <p>
* Warning: The <code>read(byte[])</code> and <code>read(byte[], int, int)</code>
* methods of <code>InputStream</code> will not work appropriately with any
* bit size &gt; 8 bits.
*
* @author robgreene@users.sourceforge.net
*/
/// The BitInputStream allows varying bit sizes to be pulled out of the
/// wrapped InputStream. This is useful for LZW type compression algorithms
/// where 9-12 bit codes are used instead of the 8-bit byte.
///
/// Warning: The `read(byte[])` and `read(byte[], int, int)`
/// methods of `InputStream` will not work appropriately with any
/// bit size > 8 bits.
///
/// @author robgreene@users.sourceforge.net
public class BitInputStream extends InputStream implements BitConstants {
/** Our source of data. */
private InputStream is;
private final InputStream is;
/** The number of bits to read for a request. This can be adjusted dynamically. */
private int requestedNumberOfBits;
/** The current bit mask to use when returning a <code>read()</code> request. */
@@ -21,20 +21,18 @@ package org.applecommander.shrinkit.io;
import java.io.IOException;
import java.io.OutputStream;
/**
* The BitOutputStream allows varying bit sizes to be written to the wrapped
* OutputStream. This is useful for LZW type compression algorithms
* where 9-12 bit codes are used instead of the 8-bit byte.
* <p>
* Warning: The <code>write(byte[])</code> and <code>write(byte[], int, int)</code>
* methods of <code>OutputStream</code> will not work appropriately with any
* bit size &gt; 8 bits.
*
* @author robgreene@users.sourceforge.net
*/
/// The BitOutputStream allows varying bit sizes to be written to the wrapped
/// OutputStream. This is useful for LZW type compression algorithms
/// where 9-12 bit codes are used instead of the 8-bit byte.
///
/// Warning: The `write(byte[])` and `write(byte[], int, int)`
/// methods of `OutputStream` will not work appropriately with any
/// bit size > 8 bits.
///
/// @author robgreene@users.sourceforge.net
public class BitOutputStream extends OutputStream implements BitConstants {
/** Our data target. */
private OutputStream os;
private final OutputStream os;
/** The number of bits to write for a request. This can be adjusted dynamically. */
private int requestedNumberOfBits;
/** The current bit mask to use for a <code>write(int)</code> request. */
@@ -45,7 +43,7 @@ public class BitOutputStream extends OutputStream implements BitConstants {
private int bitsOfData = 0;
/**
* Create a BitOutpuStream wrapping the given <code>OutputStream</code>
* Create a BitOutputStream wrapping the given <code>OutputStream</code>
* and writing the number of bits specified.
*/
public BitOutputStream(OutputStream os, int startingNumberOfBits) {
@@ -54,7 +52,7 @@ public class BitOutputStream extends OutputStream implements BitConstants {
}
/**
* Set the number of bits to be write with each call to <code>write(int)</code>.
* Set the number of bits to be written with each call to <code>write(int)</code>.
*/
public void setRequestedNumberOfBits(int numberOfBits) {
this.requestedNumberOfBits = numberOfBits;
@@ -28,27 +28,27 @@ package org.applecommander.shrinkit.io;
*/
public interface ByteConstants {
/** Master Header Block identifier "magic" bytes. */
public static final byte[] NUFILE_ID = { 0x4e, (byte)0xf5, 0x46, (byte)0xe9, 0x6c, (byte)0xe5 };
byte[] NUFILE_ID = { 0x4e, (byte)0xf5, 0x46, (byte)0xe9, 0x6c, (byte)0xe5 };
/** Header Block identifier "magic" bytes. */
public static final byte[] NUFX_ID = { 0x4e, (byte)0xf5, 0x46, (byte)0xd8 };
byte[] NUFX_ID = { 0x4e, (byte)0xf5, 0x46, (byte)0xd8 };
/** Binary II identifier "magic" bytes. */
public static final byte[] BXY_ID = { 0x0a, 0x47, 0x4c };
byte[] BXY_ID = { 0x0a, 0x47, 0x4c };
/** Apple IIgs Toolbox TimeRec seconds byte position. */
public static final int TIMEREC_SECOND = 0;
int TIMEREC_SECOND = 0;
/** Apple IIgs Toolbox TimeRec seconds byte position. */
public static final int TIMEREC_MINUTE = 1;
int TIMEREC_MINUTE = 1;
/** Apple IIgs Toolbox TimeRec minutes byte position. */
public static final int TIMEREC_HOUR = 2;
int TIMEREC_HOUR = 2;
/** Apple IIgs Toolbox TimeRec hours byte position. */
public static final int TIMEREC_YEAR = 3;
int TIMEREC_YEAR = 3;
/** Apple IIgs Toolbox TimeRec year byte position. */
public static final int TIMEREC_DAY = 4;
int TIMEREC_DAY = 4;
/** Apple IIgs Toolbox TimeRec day byte position. */
public static final int TIMEREC_MONTH = 5;
int TIMEREC_MONTH = 5;
/** Apple IIgs Toolbox TimeRec weekday (Mon, Tue, etc) byte position. */
public static final int TIMEREC_WEEKDAY = 7;
int TIMEREC_WEEKDAY = 7;
/** Apple IIgs Toolbox TimeRec length. */
public static final int TIMEREC_LENGTH = 8;
int TIMEREC_LENGTH = 8;
/** A null TimeRec */
public static final byte[] TIMEREC_NULL = new byte[TIMEREC_LENGTH];
byte[] TIMEREC_NULL = new byte[TIMEREC_LENGTH];
}
@@ -33,9 +33,9 @@ import org.applecommander.shrinkit.NuFileArchive;
* @author robgreene@users.sourceforge.net
*/
public class LittleEndianByteInputStream extends InputStream implements ByteConstants {
private InputStream inputStream;
private final InputStream inputStream;
private long bytesRead = 0;
private CRC16 crc = new CRC16();
private final CRC16 crc = new CRC16();
/**
* Construct a LittleEndianByteInputStream from an InputStream.
@@ -150,7 +150,7 @@ public class LittleEndianByteInputStream extends InputStream implements ByteCons
long b = readByte();
long c = readByte();
long d = readByte();
return (long)(a | b<<8 | c<<16 | d<<24);
return a | b<<8 | c<<16 | d<<24;
}
/**
* Read the TimeRec into a Java Date object.
@@ -33,9 +33,9 @@ import org.applecommander.shrinkit.CRC16;
* @author robgreene@users.sourceforge.net
*/
public class LittleEndianByteOutputStream extends OutputStream implements ByteConstants {
private OutputStream outputStream;
private final OutputStream outputStream;
private long bytesWritten = 0;
private CRC16 crc = new CRC16();
private final CRC16 crc = new CRC16();
/**
* Construct a LittleEndianByteOutputStream from an OutputStream.
@@ -50,6 +50,7 @@ public class LittleEndianByteOutputStream extends OutputStream implements ByteCo
public void write(int b) throws IOException {
outputStream.write(b);
crc.update(b);
bytesWritten++;
}
/**
@@ -86,7 +87,7 @@ public class LittleEndianByteOutputStream extends OutputStream implements ByteCo
* @see <a href="http://www.nulib.com/library/nufx-addendum.htm">NuFX addendum</a>
*/
public void writeDate(Date date) throws IOException {
byte[] data = null;
byte[] data;
if (date == null) {
data = TIMEREC_NULL;
} else {
@@ -25,29 +25,24 @@ import java.util.List;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
/**
* This is the generic Shrinkit LZW decompression algorithm.
* It does not deal with the vagaries of the LZW/1 and LZW/2 data streams.
* It does, however, deal with dictionary clears (0x100) and the
* <code>BitInputStream</code> bit sizes.
*
* @author robgreene@users.sourceforge.net
*/
/// This is the generic Shrinkit LZW decompression algorithm.
/// It does not deal with the vagaries of the LZW/1 and LZW/2 data streams.
/// It does, however, deal with dictionary clears (`0x100`) and the
/// `BitInputStream` bit sizes.
///
/// @author robgreene@users.sourceforge.net
public class LzwInputStream extends InputStream {
private BitInputStream is;
private final BitInputStream is;
private List<int[]> dictionary;
private Queue<Integer> outputBuffer = new ConcurrentLinkedQueue<Integer>();
private final Queue<Integer> outputBuffer = new ConcurrentLinkedQueue<>();
private boolean newBuffer = true;
// See Wikipedia entry on LZW for variable naming
private int k;
private int[] w;
private int[] entry;
/**
* Create the <code>LzwInputStream</code> based on the given
* <code>BitInputStream</code>.
* @see BitInputStream
*/
/// Create the `LzwInputStream` based on the given `BitInputStream`.
/// @see BitInputStream
public LzwInputStream(BitInputStream is) {
this.is = is;
}
@@ -72,7 +67,7 @@ public class LzwInputStream extends InputStream {
if (dictionary == null) {
is.setRequestedNumberOfBits(9);
// Setup default dictionary for all bytes
dictionary = new ArrayList<int[]>();
dictionary = new ArrayList<>();
for (short i=0; i<256; i++) dictionary.add(new int[] { i });
dictionary.add(new int[] { 0x100 }); // 0x100 not used by NuFX
}
@@ -111,7 +106,6 @@ public class LzwInputStream extends InputStream {
throw new IOException("Invalid code of <" + k + "> encountered");
}
for (int i : entry) outputBuffer.add(i);
//int[] newEntry = Arrays.copyOf(w, w.length+1);
int[] newEntry = new int[w.length+1];
System.arraycopy(w, 0, newEntry, 0, w.length);
newEntry[w.length] = entry[0];
@@ -32,8 +32,8 @@ import org.applecommander.shrinkit.CRC16;
* @author robgreene@users.sourceforge.net
*/
public class LzwOutputStream extends OutputStream {
private BitOutputStream os;
private Map<ByteArray,Integer> dictionary = new HashMap<ByteArray,Integer>();
private final BitOutputStream os;
private final Map<ByteArray,Integer> dictionary = new HashMap<>();
private int[] w = new int[0];
private int nextCode = 0x101;
@@ -42,11 +42,11 @@ public class LzwOutputStream extends OutputStream {
*
* @author robgreene@users.sourceforge.net
*/
private class ByteArray {
private static class ByteArray {
/** Data being managed. */
private int[] data;
private final int[] data;
/** The computed hash code -- CRC-16 for lack of imagination. */
private int hashCode;
private final int hashCode;
public ByteArray(int d) {
this(new int[] { d });
@@ -58,8 +58,10 @@ public class LzwOutputStream extends OutputStream {
hashCode = (int)crc.getValue();
}
public boolean equals(Object obj) {
ByteArray ba = (ByteArray)obj;
if (data.length != ba.data.length) return false;
if (!(obj instanceof ByteArray ba)) {
return false;
}
if (data.length != ba.data.length) return false;
for (int i=0; i<data.length; i++) {
if (data[i] != ba.data[i]) return false;
}
@@ -23,49 +23,33 @@ import java.io.InputStream;
import org.applecommander.shrinkit.CRC16;
/**
* The <code>NufxLzw1InputStream</code> reads a data fork or
* resource fork written in the NuFX LZW/1 format.
* <p>
* The layout of the LZW/1 data is as follows:
* <table border="0">
* <tr>
* <th colspan="3">"Fork" Header</th>
* </tr><tr>
* <td>+0</td>
* <td>Word</td>
* <td>CRC-16 of the uncompressed data within the thread</td>
* </tr><tr>
* <td>+2</td>
* <td>Byte</td>
* <td>Low-level volume number use to format 5.25" disks</td>
* </tr><tr>
* <td>+3</td>
* <td>Byte</td>
* <td>RLE character used to decode this thread</td>
* </tr><tr>
* <th colspan="3">Each subsequent 4K chunk of data</th>
* </tr><tr>
* <td>+0</td>
* <td>Word</td>
* <td>Length after RLE compression (if RLE is not used, length
* will be 4096</td>
* </tr><tr>
* <td>+2</td>
* <td>Byte</td>
* <td>A $01 indicates LZW applied to this chunk; $00 that LZW
* <b>was not</b> applied to this chunk</td>
* </tr>
* <table>
* <p>
* Note that the LZW string table is <em>cleared</em> after
* every chunk.
*
* @author robgreene@users.sourceforge.net
*/
/// The `NufxLzw1InputStream` reads a data fork or
/// resource fork written in the NuFX LZW/1 format.
///
/// The layout of the LZW/1 data is as follows:
///
/// * "Fork" Header
///
/// | Offset | Size | Description |
/// | :--- | :--- | :--- |
/// | +0 | Word | CRC-16 of the uncompressed data within the thread. |
/// | +2 | Byte | Low-level volume number use to format 5.25" disks. |
/// | +3 | Byte | RLE character used to decode this thread. |
///
/// * Each subsequent 4K chunk of data:
///
/// | Offset | Size | Description |
/// | :--- | :--- | :--- |
/// | +0 | Word | Length after RLE compression (if RLE is not used, length will be 4096). |
/// | +2 | Byte | A `$01` indicates LZW applied to this chunk; `$00` that LZW *was not* applied to this chunk. |
///
/// Note that the LZW string table is _cleared_ after
/// every chunk.
///
/// @author robgreene@users.sourceforge.net
public class NufxLzw1InputStream extends InputStream {
/** This is the raw data stream with all markers and compressed data. */
private LittleEndianByteInputStream dataStream;
private final LittleEndianByteInputStream dataStream;
/** Used for an LZW-only <code>InputStream</code>. */
private LzwInputStream lzwStream;
/** Used for an RLE-only <code>InputStream</code>. */
@@ -83,7 +67,7 @@ public class NufxLzw1InputStream extends InputStream {
/** This is the RLE character to use. */
private int rleCharacter;
/** Used to track the CRC of data we've extracted */
private CRC16 dataCrc = new CRC16();
private final CRC16 dataCrc = new CRC16();
/**
* Create the LZW/1 input stream.
@@ -23,46 +23,33 @@ import java.io.InputStream;
import org.applecommander.shrinkit.CRC16;
/**
* The <code>NufxLzw2InputStream</code> reads a data fork or
* resource fork written in the NuFX LZW/2 format.
* <p>
* The layout of the LZW/2 data is as follows:
* <table border="0">
* <tr>
* <th colspan="3">"Fork" Header</th>
* </tr><tr>
* <td>+0</td>
* <td>Byte</td>
* <td>Low-level volume number used to format 5.25" disks</td>
* </tr><tr>
* <td>+1</td>
* <td>Byte</td>
* <td>RLE character used to decode this thread</td>
* </tr><tr>
* <th colspan="3">Each subsequent 4K chunk of data</th>
* </tr><tr>
* <td>+0</td>
* <td>Word</td>
* <td>Bits 0-12: Length after RLE compression<br/>
* Bit 15: LZW flag (set to 1 if LZW used)</td>
* </tr><tr>
* <td>+2</td>
* <td>Word</td>
* <td>If LZW flag = 1, total bytes in chunk<br/>
* Else (flag = 0) start of data</td>
* </tr>
* <table>
* <p>
* The LZW/2 dictionary is only cleared when the table becomes full and is indicated
* in the input stream by 0x100. It is also cleared whenever a chunk that is not
* LZW encoded is encountered.
*
* @author robgreene@users.sourceforge.net
*/
/// The `NufxLzw2InputStream` reads a data fork or
/// resource fork written in the NuFX LZW/2 format.
///
/// The layout of the LZW/2 data is as follows:
///
/// * "Fork" Header:
///
/// | Offset | Size | Description |
/// | :--- | :--- | :--- |
/// | +0 | Byte | Low-level volume number used to format 5.25" disks. |
/// | +1 | Byte | RLE character used to decode this thread. |
///
/// * Each subsequent 4K chunk of data:
///
/// | Offset | Size | Description |
/// | :--- | :--- | :--- |
/// | +0 | Word | Bits 0-12: Length after RLE compression.<br/>Bit 15: LZW flag (set to 1 if LZW used). |
/// | +2 | Word | If LZW flag = 1, total bytes in chunk.<br/>Else (flag = 0) start of data. |
///
/// The LZW/2 dictionary is only cleared when the table becomes full and is indicated
/// in the input stream by `0x100`. It is also cleared whenever a chunk that is not
/// LZW encoded is encountered.
///
/// @author robgreene@users.sourceforge.net
public class NufxLzw2InputStream extends InputStream {
/** This is the raw data stream with all markers and compressed data. */
private LittleEndianByteInputStream dataStream;
private final LittleEndianByteInputStream dataStream;
/** Used for an LZW-only <code>InputStream</code>. */
private LzwInputStream lzwStream;
/** Used for an RLE-only <code>InputStream</code>. */
@@ -78,7 +65,7 @@ public class NufxLzw2InputStream extends InputStream {
/** This is the RLE character to use. */
private int rleCharacter;
/** Used to track the CRC of data we've extracted */
private CRC16 dataCrc = new CRC16();
private final CRC16 dataCrc = new CRC16();
/**
* Create the LZW/2 input stream.
@@ -31,8 +31,8 @@ import java.io.InputStream;
* @author robgreene@users.sourceforge.net
*/
public class RleInputStream extends InputStream {
private InputStream bs;
private int escapeChar;
private final InputStream bs;
private final int escapeChar;
private int repeatedByte;
private int numBytes = -1;
@@ -66,5 +66,4 @@ public class RleInputStream extends InputStream {
numBytes--;
return repeatedByte;
}
}
@@ -31,8 +31,8 @@ import java.io.OutputStream;
* @author robgreene@users.sourceforge.net
*/
public class RleOutputStream extends OutputStream {
private OutputStream os;
private int escapeChar;
private final OutputStream os;
private final int escapeChar;
private int repeatedByte;
private int numBytes = -1;
@@ -71,7 +71,7 @@ public class RleOutputStream extends OutputStream {
/**
* Flush out any remaining data.
* If we only have 1 byte and it is <em>not</em> the repeated
* If we only have 1 byte, and it is <em>not</em> the repeated
* byte, we can just dump that byte. Otherwise, we need to
* write out the escape character, the repeated byte, and
* the number of bytes.
@@ -19,6 +19,7 @@
package org.applecommander.shrinkit;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import org.junit.Assert;
import org.junit.Test;
@@ -39,14 +40,14 @@ public class CRC16Test {
@Test
public void testUpdate() throws UnsupportedEncodingException {
CRC16 crc16 = new CRC16();
crc16.update("123456789".getBytes("UTF-8"));
crc16.update("123456789".getBytes(StandardCharsets.UTF_8));
Assert.assertEquals(0x31c3, crc16.getValue());
crc16.update("ABCDEFGHIJKLMNOPQRSTUVWXYZ".getBytes("UTF-8"));
crc16.update("ABCDEFGHIJKLMNOPQRSTUVWXYZ".getBytes(StandardCharsets.UTF_8));
Assert.assertEquals(0x92cc, crc16.getValue());
crc16.update("abcdefghijklmnopqrstuvwxyz".getBytes("UTF-8"));
crc16.update("abcdefghijklmnopqrstuvwxyz".getBytes(StandardCharsets.UTF_8));
Assert.assertEquals(0xfc85, crc16.getValue());
crc16.reset();
crc16.update("xxx123456789xxx".getBytes("UTF-8"), 3, 9);
crc16.update("xxx123456789xxx".getBytes(StandardCharsets.UTF_8), 3, 9);
Assert.assertEquals(0x31c3, crc16.getValue());
}
@@ -29,8 +29,7 @@ public class TestHelper {
private TestHelper() {
// Prevent construction
}
public static void checkDate(byte[] streamData, Date actual) throws IOException {
try (LittleEndianByteInputStream is = new LittleEndianByteInputStream(streamData)) {
Assert.assertEquals(is.readDate(), actual);
@@ -106,9 +106,9 @@ public class LittleEndianByteOutputStreamTest extends TestBase {
bs.close();
byte[] expected = new byte[] {
// From NuFX documentation, final revision 3
0x00, 0x0a, 0x01, 0x58, 0x16, 0x0a, 0x00, 0x07, // 01:10:00am 10/22/1988 saturday
0x00, 0x10, 0x0b, 0x58, 0x11, 0x0b, 0x00, 0x05, // 11:16:00am 11/17/1988 thursday
0x00, 0x0c, 0x0d, 0x58, 0x16, 0x0a, 0x00, 0x07, // 01:12:00pm 10/22/1988 saturday
0x00, 0x0a, 0x01, 0x58, 0x16, 0x0a, 0x00, 0x07, // 01:10:00am 10/22/1988 Saturday
0x00, 0x10, 0x0b, 0x58, 0x11, 0x0b, 0x00, 0x05, // 11:16:00am 11/17/1988 Thursday
0x00, 0x0c, 0x0d, 0x58, 0x16, 0x0a, 0x00, 0x07, // 01:12:00pm 10/22/1988 Saturday
};
assertEquals(expected, os.toByteArray());
}
@@ -35,7 +35,7 @@ public class NufxLzw1Test extends TestBase {
try (NufxLzw1InputStream is = new NufxLzw1InputStream(new LittleEndianByteInputStream(getTextFileLzw1StreamData()))) {
byte[] expected = getTextFileData();
byte[] actual = new byte[expected.length];
is.read(actual);
int _ = is.read(actual);
assertEquals(expected, actual);
assertTrue(is.isCrcValid());
}
@@ -69,7 +69,7 @@ public class NufxLzwTest extends TestBase {
}
/**
* Given details about an archive file and it's expected contents, locate that
* Given details about an archive file, and its expected contents, locate that
* file and then check it against the contents.
*/
protected void check(String archiveName, String archiveFile, String expectedContentFile) throws IOException {
@@ -91,12 +91,14 @@ public class NufxLzwTest extends TestBase {
}
}
InputStream is = getClass().getResourceAsStream(expectedContentFile);
assert is != null;
ByteArrayOutputStream buf = new ByteArrayOutputStream();
int b;
while ( (b = is.read()) != -1 ) {
buf.write(b);
}
byte[] expected = buf.toByteArray();
assert actual != null;
assertEquals(expected, actual);
}
}