diff --git a/src/interfaces/ISsnFileEntry.ts b/src/interfaces/ISsnFileEntry.ts index 2c9b4ce..533e1f6 100644 --- a/src/interfaces/ISsnFileEntry.ts +++ b/src/interfaces/ISsnFileEntry.ts @@ -28,6 +28,10 @@ interface ISsnFileEntry { comprSize: number; /** Decryption keys needed to decrypt the file */ decryptionKeys: [number, number, number] | undefined; + /** Number of the disk where the file is stored (0=.z01, 1=.z02 etc.) */ + diskNumberStart: number; + /** Offset */ + offset: number; } export default ISsnFileEntry; diff --git a/src/ssn/decryption/decryptFile.ts b/src/ssn/decryption/decryptFile.ts new file mode 100644 index 0000000..53071a4 --- /dev/null +++ b/src/ssn/decryption/decryptFile.ts @@ -0,0 +1,17 @@ +import getCrc from './getCrc'; +import int32Mul from './int32Mul'; + +export default function decryptFile(dv: DataView, length: number, [key0, key1, key2]: [number, number, number]) { + for (let j = 0; j < length; j += 1) { + let testChar = dv.getUint8(j); + const keyPart = (key2 | 2) & 0xFFFF; + const decryptedByte = (keyPart * (keyPart ^ 1)) >>> 8; + testChar ^= decryptedByte & 0xFF; + key0 = getCrc(key0, testChar); + key1 = ((int32Mul(((key1 + (key0 & 0xFF)) >>> 0), 134775813) >>> 0) + 1) >>> 0; + key2 = getCrc(key2, key1 >>> 24); + dv.setUint8(j, testChar); + } + //if it was decrypted, we skip the first 12 bytes (random encryption header) + return new DataView(dv.buffer, 12); +} diff --git a/src/ssn/extractFile.ts b/src/ssn/extractFile.ts new file mode 100644 index 0000000..b7e5fa3 --- /dev/null +++ b/src/ssn/extractFile.ts @@ -0,0 +1,66 @@ +import * as zlib from 'zlib'; +import ISsnFileEntry from '../interfaces/ISsnFileEntry'; +import decryptFile from './decryption/decryptFile'; + +class ByteReader { + private dvArray: DataView[]; + private dvIndex = 0; + private pos = 0; + constructor(dvArray: DataView[], startDvIndex: number, offset: number) { + this.dvArray = dvArray; + this.dvIndex = startDvIndex; + this.pos = offset; + } + public readByte() { + const curByte = this.dvArray[this.dvIndex].getUint8(this.pos); + this.pos += 1; + if (this.pos >= this.dvArray[this.dvIndex].byteLength) { + this.pos = 0; + this.dvIndex += 1; + if (this.dvIndex >= this.dvArray.length) { throw new Error('Tried to read beyond DataView boundary in extractFile'); } + } + return curByte; + } + public seek(num: number) { + this.pos += num; + if (this.pos >= this.dvArray[this.dvIndex].byteLength) { + this.pos -= this.dvArray[this.dvIndex].byteLength; + this.dvIndex += 1; + if (this.dvIndex >= this.dvArray.length) { throw new Error('Tried to read beyond DataView boundary in extractFile'); } + } + } + public extractDv(length: number) { + const dv = new DataView(new ArrayBuffer(length)); + for (let i = 0; i < length; i += 1) { + dv.setUint8(i, this.readByte()); //TODO: refactor this so it is more optimized + } + return dv; + } +} + +/** Extracts the given file from the given DataView array and returns it as an ArrayBuffer. + * Will throw an error when end of final DataView is reached. + */ +export default function extractFile(file: ISsnFileEntry, dvArray: DataView[]): ArrayBuffer { + //use ByteReader for reading a uint8 and seeking forward across DataView boundaries + const byteReader = new ByteReader(dvArray, file.diskNumberStart, file.offset); + + if (byteReader.readByte() !== 0x50 || byteReader.readByte() !== 0x4B || byteReader.readByte() !== 0x03 || byteReader.readByte() !== 0x04) { + throw new Error('Local file header had wrong magic'); + } + byteReader.seek(22); + const localFilenameSize = byteReader.readByte() + (byteReader.readByte() << 8); + const localExtraSize = byteReader.readByte() + (byteReader.readByte() << 8); + byteReader.seek(localFilenameSize + localExtraSize); + + let dvFinal = byteReader.extractDv(file.comprSize); + + //decrypt file if necessary + if (file.decryptionKeys !== undefined) { + dvFinal = decryptFile(dvFinal, file.comprSize, file.decryptionKeys); + } + + //uncompress file + const uncompressedBuffer = zlib.inflateRawSync(dvFinal); + return uncompressedBuffer.buffer as ArrayBuffer; +} diff --git a/src/ssn/readSsnFile.ts b/src/ssn/readSsnFile.ts index ae03a84..7ab35d9 100644 --- a/src/ssn/readSsnFile.ts +++ b/src/ssn/readSsnFile.ts @@ -60,6 +60,7 @@ export default function readSsnFile(buffer: Buffer): ISsnFileEntry[] { //Go to start of central dir pos -= 20 + centralDirSize; + const posCentralDirStart = pos; for (let i = 0; i < numEntries; i += 1) { { @@ -99,7 +100,9 @@ export default function readSsnFile(buffer: Buffer): ISsnFileEntry[] { const extraFieldLength = dv.getUint16(pos, true); pos += 2; /** file comment length */ const fileCommentLength = dv.getUint16(pos, true); pos += 2; - pos += 8; //skip disk number start, internal file attributes and external file attributes + /** disk number start */ + const diskNumberStart = dv.getUint16(pos, true); pos += 2; //0=.z01, 1=.z02 etc. + pos += 6; //skip internal file attributes and external file attributes /** relative offset of local header */ const relOffset = dv.getUint32(pos, true); pos += 4; /** file name (variable size) */ @@ -149,8 +152,12 @@ export default function readSsnFile(buffer: Buffer): ISsnFileEntry[] { diffDestLength, diffSourceLength, diffType, + diskNumberStart, lastMod: new Date(1980 + (lastModDate >>> 9), (lastModDate & 0x1E0) >>> 5, lastModDate & 0x1F, lastModTime >>> 11, (lastModTime & 0x7E0) >>> 5, (lastModTime & 0x1F) * 2), name: fileName, + offset: (centralDirOffset > 0) ? //If files are included in this archive, the centralDirOffset will not start from the beginning + posCentralDirStart - centralDirOffset + relOffset : //if file is in this archive + relOffset, //if we need to look in a disk (e.g. .z01 for this file) size: uncomprSize, }; @@ -167,11 +174,5 @@ export default function readSsnFile(buffer: Buffer): ISsnFileEntry[] { fileEntries.push(fileEntry); } - //TODO: read encrypted + compressed file - //pos = start of central dir - // - //fseek(fp, pos - centralDirOffset, SEEK_SET); - //... - return fileEntries; }