commit 68a5b3a5f5b5e0a33e49eca3dbd52079dbdfe418 Author: Robert Greene Date: Mon Jun 16 03:37:52 2008 +0000 Initial commit. Reads basic NuFile/NuFX archive format. Does not handle CRC-16, does not write, does not handle compressed threads. diff --git a/.classpath b/.classpath new file mode 100644 index 0000000..d6419eb --- /dev/null +++ b/.classpath @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/.project b/.project new file mode 100644 index 0000000..b66a2aa --- /dev/null +++ b/.project @@ -0,0 +1,17 @@ + + + ShrinkItArchive + + + + + + org.eclipse.jdt.core.javabuilder + + + + + + org.eclipse.jdt.core.javanature + + diff --git a/samples/Joystick.SHK b/samples/Joystick.SHK new file mode 100644 index 0000000..56d4748 Binary files /dev/null and b/samples/Joystick.SHK differ diff --git a/samples/Scc.shk b/samples/Scc.shk new file mode 100644 index 0000000..4947c3f Binary files /dev/null and b/samples/Scc.shk differ diff --git a/src/com/webcodepro/shrinkit/ByteConstants.java b/src/com/webcodepro/shrinkit/ByteConstants.java new file mode 100644 index 0000000..a9d11cd --- /dev/null +++ b/src/com/webcodepro/shrinkit/ByteConstants.java @@ -0,0 +1,33 @@ +package com.webcodepro.shrinkit; + +/** + * Provides constants for the ByteSource and ByteTarget classes. + * + * @author robgreene@users.sourceforge.net + * @see ByteSource + * @see ByteTarget + */ +public interface ByteConstants { + /** Master Header Block identifier "magic" bytes. */ + public static final 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 }; + /** Apple IIgs Toolbox TimeRec seconds byte position. */ + public static final int TIMEREC_SECOND = 0; + /** Apple IIgs Toolbox TimeRec seconds byte position. */ + public static final int TIMEREC_MINUTE = 1; + /** Apple IIgs Toolbox TimeRec minutes byte position. */ + public static final int TIMEREC_HOUR = 2; + /** Apple IIgs Toolbox TimeRec hours byte position. */ + public static final int TIMEREC_YEAR = 3; + /** Apple IIgs Toolbox TimeRec year byte position. */ + public static final int TIMEREC_DAY = 4; + /** Apple IIgs Toolbox TimeRec day byte position. */ + public static final int TIMEREC_MONTH = 5; + /** Apple IIgs Toolbox TimeRec weekday (Mon, Tue, etc) byte position. */ + public static final int TIMEREC_WEEKDAY = 7; + /** Apple IIgs Toolbox TimeRec length. */ + public static final int TIMEREC_LENGTH = 8; + /** A null TimeRec */ + public static final byte[] TIMEREC_NULL = new byte[TIMEREC_LENGTH]; +} diff --git a/src/com/webcodepro/shrinkit/ByteSource.java b/src/com/webcodepro/shrinkit/ByteSource.java new file mode 100644 index 0000000..a68016a --- /dev/null +++ b/src/com/webcodepro/shrinkit/ByteSource.java @@ -0,0 +1,105 @@ +package com.webcodepro.shrinkit; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.Date; +import java.util.GregorianCalendar; + +/** + * A simple class to hide the source of byte data. + * @author robgreene@users.sourceforge.net + */ +public class ByteSource implements ByteConstants { + private InputStream inputStream; + + /** + * Construct a ByteSource from an InputStream. + */ + public ByteSource(InputStream inputStream) { + this.inputStream = inputStream; + } + /** + * Construct a ByteSource from a byte array. + */ + public ByteSource(byte[] data) { + this.inputStream = new ByteArrayInputStream(data); + } + + /** + * Get the next byte. + * Returns -1 if at end of input. + * Note that an unsigned byte needs to be returned in a larger container (ie, a short or int or long). + */ + public int read() throws IOException { + return inputStream.read(); + } + /** + * Get the next byte and fail if we are at EOF. + * Note that an unsigned byte needs to be returned in a larger container (ie, a short or int or long). + */ + public int readByte() throws IOException { + int i = read(); + if (i == -1) throw new IOException("Expecting a byte but at EOF"); + return i; + } + /** + * Get the next set of bytes as an array. + * If EOF encountered, an IOException is thrown. + */ + public byte[] readBytes(int bytes) throws IOException { + byte[] data = new byte[bytes]; + int read = inputStream.read(data); + if (read < bytes) { + throw new IOException("Requested " + bytes + " bytes, but " + read + " read"); + } + return data; + } + + /** + * Test that the NuFile id is embedded in the ByteSource. + */ + public boolean checkNuFileId() throws IOException { + byte[] data = readBytes(6); + return Arrays.equals(data, NUFILE_ID); + } + /** + * Test that the NuFx id is embedded in the ByteSource. + */ + public boolean checkNuFxId() throws IOException { + byte[] data = readBytes(4); + return Arrays.equals(data, NUFX_ID); + } + /** + * Read the two bytes in as a "Word" which needs to be stored as a Java int. + */ + public int readWord() throws IOException { + return (readByte() | readByte() << 8) & 0xffff; + } + /** + * Read the two bytes in as a "Long" which needs to be stored as a Java long. + */ + public long readLong() throws IOException { + long a = readByte(); + long b = readByte(); + long c = readByte(); + long d = readByte(); + return (long)(a | b<<8 | c<<16 | d<<24); + } + /** + * Read the TimeRec into a Java Date object. + * Note that years 1900-1939 are assumed to be 2000-2039 per the NuFX addendum + * at http://www.nulib.com/library/nufx-addendum.htm. + * @see http://www.nulib.com/library/nufx-addendum.htm + */ + public Date readDate() throws IOException { + byte[] data = readBytes(TIMEREC_LENGTH); + if (Arrays.equals(TIMEREC_NULL, data)) return null; + int year = data[TIMEREC_YEAR]+1900; + if (year < 1940) year+= 100; + GregorianCalendar gc = new GregorianCalendar(year, data[TIMEREC_MONTH]-1, data[TIMEREC_DAY], + data[TIMEREC_HOUR], data[TIMEREC_MINUTE], data[TIMEREC_SECOND]); + return gc.getTime(); + } +} diff --git a/src/com/webcodepro/shrinkit/HeaderBlock.java b/src/com/webcodepro/shrinkit/HeaderBlock.java new file mode 100644 index 0000000..940c136 --- /dev/null +++ b/src/com/webcodepro/shrinkit/HeaderBlock.java @@ -0,0 +1,200 @@ +package com.webcodepro.shrinkit; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * The Header Block contains information and content + * about a single entry (be it a file or disk image). + *

+ * Note that we need to support multiple versions of the NuFX + * archive format. Some details may be invalid, depending on + * version, and those are documented in the getter methods. + * + * @author robgreene@users.sourceforge.net + * @see http://www.nulib.com/library/FTN.e08002.htm + */ +public class HeaderBlock { + private int headerCrc; + private int attribCount; + private int versionNumber; + private long totalThreads; + private int fileSysId; + private int fileSysInfo; + private long access; + private long fileType; + private long extraType; + private int storageType; + private Date createWhen; + private Date modWhen; + private Date archiveWhen; + private int optionSize; + private byte[] optionListBytes; + private byte[] attribBytes; + private String filename; + private List threads; + + /** + * Create the Header Block. This is done dynamically since + * the Header Block size varies significantly. + */ + public HeaderBlock(ByteSource bs) throws IOException { + bs.checkNuFxId(); + headerCrc = bs.readWord(); + attribCount = bs.readWord(); + versionNumber = bs.readWord(); + totalThreads = bs.readLong(); + fileSysId = bs.readWord(); + fileSysInfo = bs.readWord(); + access = bs.readLong(); + fileType = bs.readLong(); + extraType = bs.readLong(); + storageType = bs.readWord(); + createWhen = bs.readDate(); + modWhen = bs.readDate(); + archiveWhen = bs.readDate(); + // Read the mysterious option_list + if (versionNumber >= 1) { + optionSize = bs.readWord(); + if (optionSize > 0) { + optionListBytes = bs.readBytes(optionSize-2); + } + } + // Compute attribute bytes that exist and read (if needed) + int sizeofAttrib = attribCount - 58; + if (versionNumber >= 1) { + if (optionSize == 0) sizeofAttrib -= 2; + else sizeofAttrib -= optionSize; + } + if (sizeofAttrib > 0) { + attribBytes = bs.readBytes(sizeofAttrib); + } + // Read the (defunct) filename + int length = bs.readWord(); + if (length > 0) { + filename = new String(bs.readBytes(length)); + } + } + /** + * Read in all data threads. All ThreadRecords are read and then + * each thread's data is read (per NuFX spec). + */ + public void readThreads(ByteSource bs) throws IOException { + threads = new ArrayList(); + for (long l=0; l getThreadRecords() { + return threads; + } + public void setThreadRecords(List threads) { + this.threads = threads; + } +} diff --git a/src/com/webcodepro/shrinkit/MasterHeaderBlock.java b/src/com/webcodepro/shrinkit/MasterHeaderBlock.java new file mode 100644 index 0000000..dca381f --- /dev/null +++ b/src/com/webcodepro/shrinkit/MasterHeaderBlock.java @@ -0,0 +1,87 @@ +package com.webcodepro.shrinkit; + +import java.io.IOException; +import java.util.Date; + +/** + * The Master Header Block contains information about the entire + * ShrinkIt archive. + *

+ * Note that we need to support multiple versions of the NuFX + * archive format. Some details may be invalid, depending on + * version, and those are documented in the getter methods. + * + * @author robgreene@users.sourceforge.net + * @see http://www.nulib.com/library/FTN.e08002.htm + */ +public class MasterHeaderBlock { + private static final int MASTER_HEADER_LENGTH = 48; + private int masterCrc; + private long totalRecords; + private Date archiveCreateWhen; + private Date archiveModWhen; + private int masterVersion; + private long masterEof; + + /** + * Create the Master Header Block, based on the ByteSource. + * To avoid byte counting, we read in the fixed size header + * and then work our way through the data. When we are done, + * that data is thrown away, and we don't need to ensure + * that we've read a consistent number of bytes. + */ + public MasterHeaderBlock(ByteSource bs) throws IOException { + bs = new ByteSource(bs.readBytes(MASTER_HEADER_LENGTH)); + bs.checkNuFileId(); + masterCrc = bs.readWord(); + totalRecords = bs.readLong(); + archiveCreateWhen = bs.readDate(); + archiveModWhen = bs.readDate(); + masterVersion = bs.readWord(); + if (masterVersion > 0) { + bs.readBytes(8); // documented to be null, but we don't care + masterEof = bs.readLong(); + } else { + masterEof = -1; + } + } + + // GENERATED CODE + + public int getMasterCrc() { + return masterCrc; + } + public void setMasterCrc(int masterCrc) { + this.masterCrc = masterCrc; + } + public long getTotalRecords() { + return totalRecords; + } + public void setTotalRecords(long totalRecords) { + this.totalRecords = totalRecords; + } + public Date getArchiveCreateWhen() { + return archiveCreateWhen; + } + public void setArchiveCreateWhen(Date archiveCreateWhen) { + this.archiveCreateWhen = archiveCreateWhen; + } + public Date getArchiveModWhen() { + return archiveModWhen; + } + public void setArchiveModWhen(Date archiveModWhen) { + this.archiveModWhen = archiveModWhen; + } + public int getMasterVersion() { + return masterVersion; + } + public void setMasterVersion(int masterVersion) { + this.masterVersion = masterVersion; + } + public long getMasterEof() { + return masterEof; + } + public void setMasterEof(long masterEof) { + this.masterEof = masterEof; + } +} diff --git a/src/com/webcodepro/shrinkit/NuFileArchive.java b/src/com/webcodepro/shrinkit/NuFileArchive.java new file mode 100644 index 0000000..e034173 --- /dev/null +++ b/src/com/webcodepro/shrinkit/NuFileArchive.java @@ -0,0 +1,37 @@ +package com.webcodepro.shrinkit; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +/** + * Basic reading of a NuFX archive. + * + * @author robgreene@users.sourceforge.net + */ +public class NuFileArchive { + private MasterHeaderBlock master; + private List headers; + + /** + * Read in the NuFile/NuFX/Shrinkit archive. + */ + public NuFileArchive(InputStream inputStream) throws IOException { + ByteSource bs = new ByteSource(inputStream); + master = new MasterHeaderBlock(bs); + headers = new ArrayList(); + for (int i=0; i getHeaderBlocks() { + return headers; + } +} diff --git a/src/com/webcodepro/shrinkit/ThreadClass.java b/src/com/webcodepro/shrinkit/ThreadClass.java new file mode 100644 index 0000000..ba8da6f --- /dev/null +++ b/src/com/webcodepro/shrinkit/ThreadClass.java @@ -0,0 +1,24 @@ +package com.webcodepro.shrinkit; + +/** + * Define and decode the thread_class field. + * @author robgreene@users.sourceforge.net + */ +public enum ThreadClass { + MESSAGE, CONTROL, DATA, FILENAME; + + /** + * Find the given 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); + } + } +} diff --git a/src/com/webcodepro/shrinkit/ThreadFormat.java b/src/com/webcodepro/shrinkit/ThreadFormat.java new file mode 100644 index 0000000..f9cfe1b --- /dev/null +++ b/src/com/webcodepro/shrinkit/ThreadFormat.java @@ -0,0 +1,27 @@ +package com.webcodepro.shrinkit; + +/** + * Define and decode the thread_format field. + * @author robgreene@users.sourceforge.net + */ +public enum ThreadFormat { + UNCOMPRESSED, HUFFMAN_SQUEEZE, DYNAMIC_LZW1, DYNAMIC_LZW2, + UNIX_12BIT_COMPRESS, UNIX_16BIT_COMPRESS; + + /** + * Find the ThreadFormat. + * @throws IllegalArgumentException if the thread_format is unknown + */ + public static ThreadFormat find(int threadFormat) { + switch (threadFormat) { + case 0x0000: return UNCOMPRESSED; + case 0x0001: return HUFFMAN_SQUEEZE; + case 0x0002: return DYNAMIC_LZW1; + case 0x0003: return DYNAMIC_LZW2; + case 0x0004: return UNIX_12BIT_COMPRESS; + case 0x0005: return UNIX_16BIT_COMPRESS; + default: + throw new IllegalArgumentException("Unknown thread_format of " + threadFormat); + } + } +} diff --git a/src/com/webcodepro/shrinkit/ThreadKind.java b/src/com/webcodepro/shrinkit/ThreadKind.java new file mode 100644 index 0000000..af8b383 --- /dev/null +++ b/src/com/webcodepro/shrinkit/ThreadKind.java @@ -0,0 +1,41 @@ +package com.webcodepro.shrinkit; + +/** + * Define and decode the thread_kind field. + * @author robgreene@users.sourceforge.net + */ +public enum ThreadKind { + ASCII_TEXT, ALLOCATED_SPACE, APPLE_IIGS_ICON, CREATE_DIRECTORY, DATA_FORK, DISK_IMAGE, RESOURCE_FORK, + FILENAME; + + /** + * Find the specific 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 for message thread_class of " + threadKind); + case CONTROL: + if (threadKind == 0x0000) return CREATE_DIRECTORY; + throw new IllegalArgumentException("Unknown thread_kind for control thread_class of " + threadKind); + 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 for data thread_class of " + threadKind); + case FILENAME: + if (threadKind == 0x0000) return FILENAME; + throw new IllegalArgumentException("Unknown thread_kind for filename thread_class of " + threadKind); + default: + throw new IllegalArgumentException("Unknown thread_class of " + threadClass); + } + } +} diff --git a/src/com/webcodepro/shrinkit/ThreadRecord.java b/src/com/webcodepro/shrinkit/ThreadRecord.java new file mode 100644 index 0000000..c49f051 --- /dev/null +++ b/src/com/webcodepro/shrinkit/ThreadRecord.java @@ -0,0 +1,116 @@ +package com.webcodepro.shrinkit; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * This represents a single thread from the Shrinkit archive. + * As it is constructed, the thread "header" is read. Once all + * threads have been constructed, use readThreadData + * to load up the data. + *

+ * Depending on the type of thread, the data may be text. If so, + * isText will return true and getText + * will return the string. Otherwise the data should be read through + * one of the InputStream options. + * + * @author robgreene@users.sourceforge.net + */ +public class ThreadRecord { + private ThreadClass threadClass; + private ThreadFormat threadFormat; + private ThreadKind threadKind; + private int threadCrc; + private long threadEof; + private long compThreadEof; + private byte[] threadData; + + /** + * Construct the ThreadRecord and read the header details. + */ + public ThreadRecord(ByteSource bs) throws IOException { + threadClass = ThreadClass.find(bs.readWord()); + threadFormat = ThreadFormat.find(bs.readWord()); + threadKind = ThreadKind.find(bs.readWord(), threadClass); + threadCrc = bs.readWord(); + threadEof = bs.readLong(); + compThreadEof = bs.readLong(); + } + + /** + * Read the raw thread data. This must be called. + */ + public void readThreadData(ByteSource bs) throws IOException { + threadData = bs.readBytes((int)compThreadEof); + } + /** + * Determine if this is a text-type field. + */ + public boolean isText() { + return threadKind == ThreadKind.ASCII_TEXT || threadKind == ThreadKind.FILENAME; + } + /** + * Return the text data. + */ + public String getText() { + return isText() ? new String(threadData, 0, (int)threadEof) : null; + } + /** + * Get raw data bytes (compressed). + */ + public byte[] getBytes() { + return threadData; + } + /** + * Get the raw data input stream. + */ + public InputStream getRawInputStream() { + return new ByteArrayInputStream(threadData); + } + + // GENERATED CODE + + public ThreadClass getThreadClass() { + return threadClass; + } + public void setThreadClass(ThreadClass threadClass) { + this.threadClass = threadClass; + } + public ThreadFormat getThreadFormat() { + return threadFormat; + } + public void setThreadFormat(ThreadFormat threadFormat) { + this.threadFormat = threadFormat; + } + public ThreadKind getThreadKind() { + return threadKind; + } + public void setThreadKind(ThreadKind threadKind) { + this.threadKind = threadKind; + } + public int getThreadCrc() { + return threadCrc; + } + public void setThreadCrc(int threadCrc) { + this.threadCrc = threadCrc; + } + public long getThreadEof() { + return threadEof; + } + public void setThreadEof(long threadEof) { + this.threadEof = threadEof; + } + public long getCompThreadEof() { + return compThreadEof; + } + public void setCompThreadEof(long compThreadEof) { + this.compThreadEof = compThreadEof; + } + public byte[] getThreadData() { + return threadData; + } + public void setThreadData(byte[] threadData) { + this.threadData = threadData; + } +} diff --git a/src/com/webcodepro/shrinkit/TimeRec.java b/src/com/webcodepro/shrinkit/TimeRec.java new file mode 100644 index 0000000..70dbc72 --- /dev/null +++ b/src/com/webcodepro/shrinkit/TimeRec.java @@ -0,0 +1,90 @@ +package com.webcodepro.shrinkit; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; + +/** + * Apple IIgs Toolbox TimeRec object. + * + * @author robgreene@users.sourceforge.net + */ +public class TimeRec { + private static final int SECOND = 0; + private static final int MINUTE = 1; + private static final int HOUR = 2; + private static final int YEAR = 3; + private static final int DAY = 4; + private static final int MONTH = 5; + private static final int WEEKDAY = 7; + private static final int LENGTH = 8; + private byte[] data = null; + + /** + * Construct a TimeRec with the current date. + */ + public TimeRec() { + this(new Date()); + } + /** + * Construct a TimeRec with the specified date. You may pass in a null for a null date (all 0x00's). + */ + public TimeRec(Date date) { + setDate(date); + } + /** + * Construct a TimeRec from the given LENGTH byte array. + */ + public TimeRec(byte[] bytes, int offset) { + if (bytes == null || bytes.length - offset < LENGTH) { + throw new IllegalArgumentException("TimeRec requires a " + LENGTH + " byte array."); + } + data = Arrays.copyOfRange(bytes, offset, LENGTH); + } + /** + * Construct a TimeRec from the InputStream. + */ + public TimeRec(InputStream inputStream) throws IOException { + data = new byte[LENGTH]; + for (int i=0; i + * Note that to successfully run this, the classpath must have the samples folder + * added to it. + * + * @author robgreene@users.sourceforge.net + */ +public class NuFileArchiveTest extends TestCase { + public void testReadJoystickShk() throws IOException { + display("/Joystick.SHK"); + } + public void testReadSccShk() throws IOException { + display("/Scc.shk"); + } + + private void display(String archiveName) throws IOException { + System.out.printf("Details for %s\n\n", archiveName); + InputStream is = getClass().getResourceAsStream(archiveName); + if (is == null) { + System.out.printf("*** ERROR: Unable to locate '%s'", archiveName); + fail("Unable to locate archive file"); + } + NuFileArchive a = new NuFileArchive(is); + MasterHeaderBlock m = a.getMasterHeaderBlock(); + System.out.printf("Master Header Block\n==================\n" + + "master_crc=$%x\ntotal_records=%d\narchive_create_when=%tc\narchive_mod_when=%tc\n" + + "master_version=%d\nmaster_eof=$%x\n\n", + m.getMasterCrc(), m.getTotalRecords(), m.getArchiveCreateWhen(), m.getArchiveModWhen(), + m.getMasterVersion(), m.getMasterEof()); + for (HeaderBlock b : a.getHeaderBlocks()) { + System.out.printf("\tHeader Block\n\t============\n"); + System.out.printf("\theader_crc=$%x\n\tattrib_count=%d\n\tversion_number=%d\n\ttotal_threads=%d\n\t" + + "file_sys_id=$%x\n\tfile_sys_info=$%x\n\taccess=$%x\n\tfile_type=$%x\n\textra_type=$%x\n\t" + + "storage_type=$%x\n\tcreate_when=%tc\n\tmod_when=%tc\n\tarchive_when=%tc\n\toption_size=%d\n\t" + + "filename=%s\n\n", + b.getHeaderCrc(), b.getAttribCount(), b.getVersionNumber(), b.getTotalThreads(), b.getFileSysId(), + b.getFileSysInfo(), b.getAccess(), b.getFileType(), b.getExtraType(), b.getStorageType(), + b.getCreateWhen(), b.getModWhen(), b.getArchiveWhen(), b.getOptionSize(), b.getFilename()); + System.out.printf("\t\tThreads\n\t\t=======\n"); + for (ThreadRecord r : b.getThreadRecords()) { + System.out.printf("\t\tthread_class=%s\n\t\tthread_format=%s\n\t\tthread_kind=%s\n\t\t" + + "thread_crc=$%x\n\t\tthread_eof=$%x\n\t\tcompThreadEof=$%x\n", + r.getThreadClass(), r.getThreadFormat(), r.getThreadKind(), r.getThreadCrc(), + r.getThreadEof(), r.getCompThreadEof()); + if (r.getThreadKind() == ThreadKind.FILENAME) { + System.out.printf("\t\tFILENAME=%s\n", r.getText()); + } + System.out.printf("\n"); + } + } + } +} diff --git a/test_src/com/webcodepro/shrinkit/ThreadRecordTest.java b/test_src/com/webcodepro/shrinkit/ThreadRecordTest.java new file mode 100644 index 0000000..03cc13e --- /dev/null +++ b/test_src/com/webcodepro/shrinkit/ThreadRecordTest.java @@ -0,0 +1,131 @@ +package com.webcodepro.shrinkit; + +import java.io.IOException; + +import junit.framework.TestCase; + +/** + * Exercise the Thread Record. + * The source of some these test cases come from the "NuFX + * Documentation Final Revision Three" document. + * @author robgreene@users.sourceforge.net + */ +public class ThreadRecordTest extends TestCase { + /** + * From "NuFX Documentation Final Revision Three", version 0, "Normal Files" sample. + */ + public void testNormalFiles() throws IOException { + byte[] data = { + 0x02, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x20, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00 + }; + ByteSource bs = new ByteSource(data); + ThreadRecord r = new ThreadRecord(bs); + assertEquals(ThreadClass.DATA, r.getThreadClass()); + assertEquals(ThreadFormat.DYNAMIC_LZW1, r.getThreadFormat()); + assertEquals(ThreadKind.DATA_FORK, r.getThreadKind()); + assertEquals(0x0000, r.getThreadCrc()); + assertEquals(0x00002000, r.getThreadEof()); + assertEquals(0x00001000, r.getCompThreadEof()); + } + + /** + * From "NuFX Documentation Final Revision Three", version 0, "Extended Files" sample. + */ + public void testExtendedFiles() throws IOException { + byte[] data = { + 0x02, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x20, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, + 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x00, 0x10, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00 + }; + ByteSource bs = new ByteSource(data); + ThreadRecord r1 = new ThreadRecord(bs); + assertEquals(ThreadClass.DATA, r1.getThreadClass()); + assertEquals(ThreadFormat.DYNAMIC_LZW1, r1.getThreadFormat()); + assertEquals(ThreadKind.DATA_FORK, r1.getThreadKind()); + assertEquals(0x0000, r1.getThreadCrc()); + assertEquals(0x00002000, r1.getThreadEof()); + assertEquals(0x00000800, r1.getCompThreadEof()); + ThreadRecord r2 = new ThreadRecord(bs); + assertEquals(ThreadClass.DATA, r2.getThreadClass()); + assertEquals(ThreadFormat.DYNAMIC_LZW1, r2.getThreadFormat()); + assertEquals(ThreadKind.RESOURCE_FORK, r2.getThreadKind()); + assertEquals(0x0000, r2.getThreadCrc()); + assertEquals(0x00001000, r2.getThreadEof()); + assertEquals(0x00000800, r2.getCompThreadEof()); + } + + /** + * From "NuFX Documentation Final Revision Three", version 0, "Disk" sample. + */ + public void testDiskImage() throws IOException { + byte[] data = { + 0x02, 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x51, 0x45, 0x07, 0x00 + }; + ByteSource bs = new ByteSource(data); + ThreadRecord r = new ThreadRecord(bs); + assertEquals(ThreadClass.DATA, r.getThreadClass()); + assertEquals(ThreadFormat.DYNAMIC_LZW1, r.getThreadFormat()); + assertEquals(ThreadKind.DISK_IMAGE, r.getThreadKind()); + assertEquals(0x0000, r.getThreadCrc()); + assertEquals(0x00000000, r.getThreadEof()); + assertEquals(0x00074551, r.getCompThreadEof()); + } + + /** + * Sample taken from the SCC.SHK file, first header entry. + */ + public void testSccShkHeader1() throws IOException { + byte[] data = { + 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x0b, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, (byte)0xc8, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x03, 0x00, 0x00, 0x00, 0x58, 0x4a, + (byte)0xd6, 0x06, 0x00, 0x00, (byte)0xd4, 0x03, 0x00, 0x00 + }; + ByteSource bs = new ByteSource(data); + ThreadRecord r1 = new ThreadRecord(bs); + assertEquals(ThreadClass.FILENAME, r1.getThreadClass()); + assertEquals(ThreadFormat.UNCOMPRESSED, r1.getThreadFormat()); + assertEquals(ThreadKind.FILENAME, r1.getThreadKind()); + assertEquals(0x0000, r1.getThreadCrc()); + assertEquals(0x0000000b, r1.getThreadEof()); + assertEquals(0x00000020, r1.getCompThreadEof()); + ThreadRecord r2 = new ThreadRecord(bs); + assertEquals(ThreadClass.MESSAGE, r2.getThreadClass()); + assertEquals(ThreadFormat.UNCOMPRESSED, r2.getThreadFormat()); + assertEquals(ThreadKind.ALLOCATED_SPACE, r2.getThreadKind()); + assertEquals(0x0000, r2.getThreadCrc()); + assertEquals(0x00000000, r2.getThreadEof()); + assertEquals(0x000000c8, r2.getCompThreadEof()); + ThreadRecord r3 = new ThreadRecord(bs); + assertEquals(ThreadClass.DATA, r3.getThreadClass()); + assertEquals(ThreadFormat.DYNAMIC_LZW2, r3.getThreadFormat()); + assertEquals(ThreadKind.DATA_FORK, r3.getThreadKind()); + assertEquals(0x4a58, r3.getThreadCrc()); + assertEquals(0x000006d6, r3.getThreadEof()); + assertEquals(0x000003d4, r3.getCompThreadEof()); + } + + /** + * Sample taken from the SCC.SHK file, first header entry. + */ + public void testSccShkHeader1FilenameThread() throws IOException { + byte[] data = { + 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x0b, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, + 0x73, 0x63, 0x63, 0x3a, 0x65, 0x71, 0x75, 0x61, + 0x74, 0x65, 0x73, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + }; + ByteSource bs = new ByteSource(data); + ThreadRecord r = new ThreadRecord(bs); + r.readThreadData(bs); + assertTrue(r.isText()); + assertEquals("scc:equates", r.getText()); + } +}