From 98995145784cbe98804937e70b11bb1c6ee8fd08 Mon Sep 17 00:00:00 2001 From: C-3PO Date: Fri, 22 Jun 2018 13:13:09 +0200 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20readSsnFile=20now=20reads?= =?UTF-8?q?=20all=20file=20entries?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/installPatch.ts | 6 +- src/interfaces/ISsnFileEntry.ts | 29 ++++++ src/ssn/readSolidpkg.ts | 144 --------------------------- src/ssn/readSsnFile.ts | 170 ++++++++++++++++++++++++++++++++ 4 files changed, 202 insertions(+), 147 deletions(-) create mode 100644 src/interfaces/ISsnFileEntry.ts delete mode 100644 src/ssn/readSolidpkg.ts create mode 100644 src/ssn/readSsnFile.ts diff --git a/src/installPatch.ts b/src/installPatch.ts index b78f732..d16a807 100644 --- a/src/installPatch.ts +++ b/src/installPatch.ts @@ -1,10 +1,10 @@ import getSolidpkg from './ssn/getSolidpkg'; -import readSolidpkg from './ssn/readSolidpkg'; +import readSsnFile from './ssn/readSsnFile'; (async () => { const buffer = await getSolidpkg('assets_swtor_de_de', -1, 0); console.log(buffer.length, buffer); - const output = readSolidpkg(buffer); - console.log(output); + const fileEntries = readSsnFile(buffer); + console.log(fileEntries); })(); diff --git a/src/interfaces/ISsnFileEntry.ts b/src/interfaces/ISsnFileEntry.ts new file mode 100644 index 0000000..af80d69 --- /dev/null +++ b/src/interfaces/ISsnFileEntry.ts @@ -0,0 +1,29 @@ +enum SsnDiffType { + /** We included the original file contents, not just the differences, because file is new or it's in a format that's not suitable for diffing. */ + NewFile = 0, + /** File is not included in the .zip archive because it has been deleted. */ + Deleted = 1, + /** File has changed and we include the differences, encoded in vcdiff/xdelta3 */ + Changed = 2, + /** File is not included in the .zip archive because it hasn't changed */ + Unchanged = 3, +} + +interface ISsnFileEntry { + /** CRC-32 checksum of this file. */ + crc: number; + /** If we only store the differences, the size of the destination file. */ + diffDestLength: number; + /** If we only store the differences, the size of the source file. */ + diffSourceLength: number; + /** Whether this file was changed or not, or whether it was newly added or deleted. */ + diffType: SsnDiffType; + /** The date and time when this file was last modified. */ + lastMod: Date; + /** File name */ + name: string; + /** Uncompressed size */ + size: number; +} + +export default ISsnFileEntry; diff --git a/src/ssn/readSolidpkg.ts b/src/ssn/readSolidpkg.ts deleted file mode 100644 index 93602a3..0000000 --- a/src/ssn/readSolidpkg.ts +++ /dev/null @@ -1,144 +0,0 @@ -/** - * The file format used by Solid State Networks is based on the .zip format. - * Check the .ZIP File Format Specification . - */ - -import { TextDecoder } from 'util'; -import modifyPassword from './modifyPassword'; - -const SIGNATURE_END_OF_CENTRAL_DIR = 0x06054b50; -const SIGNATURE_CENTRAL_DIR = 0x02014b50; -const COMPRESSION_DEFLATE = 8; - -const Decoder = new TextDecoder('utf-8'); - -export default function readSolidpkg(buffer: Buffer) { - const arrayBuffer = buffer.buffer; - const dv = new DataView(arrayBuffer); - - //--------------- READ END OF CENTRAL DIR --------------- - - //Go to end of file - let pos = buffer.length - 22; //end of central dir is at least 22 bytes long - - //Find end of central dir - while (pos >= 0 && dv.getUint32(pos, true) !== SIGNATURE_END_OF_CENTRAL_DIR) { - pos -= 1; - } - if (pos < 0) { - throw new Error('Could not find end of central dir.'); - } - pos += 4; - - /** skip 6 bytes: - * number of this disk, - * number of the disk with the start of the central directory - * total number of entries in the central directory on this disk - */ - pos += 6; - - /** Total number of entries in the central directory */ - const numEntries = dv.getUint16(pos, true); pos += 2; - /** Size of the central directory */ - const centralDirSize = dv.getUint32(pos, true); pos += 4; - /** Offset of start of central directory with respect to the starting disk number */ - const centralDirOffset = dv.getUint32(pos, true); pos += 4; - //ignore .ZIP file comment length - - if (numEntries !== 1) { - throw new Error(`Expected numEntries == 1 in end of central dir but it was "${numEntries}"`); - } - if (centralDirSize < 46 * numEntries) { - throw new Error('centralDirSize was smaller than expected in end of central dir.'); - } - if (pos - centralDirSize < 0) { - throw new Error(`Central dir points before file start (0x${(pos - centralDirSize).toString(16)})`); - } - - //--------------- READ CENTRAL DIRECTORY --------------- - - //Go to start of central dir - pos -= 20 + centralDirSize; - - { - const signature = dv.getUint32(pos, true); pos += 4; - if (signature !== SIGNATURE_CENTRAL_DIR) { - throw new Error(`Expected central dir signature but found "0x${signature.toString(16)}"`); - } - } - - pos += 4; //skip version made by and version needed to extract - - /** The general purpose bit flag stores whether the file is encrypted or not. - * Most files are encrypted but there are some exceptions: assets_swtor_test_main_248to249.solidpkg, - * assets_swtor_test_en_us_270to271.solidpkg, and retailclient_publictest_246to247.solidpkg are not encrypted. - */ - const bitFlag = dv.getUint16(pos, true); pos += 2; - - /** Compression method, always set to 8 = DEFLATE. */ - const compression = dv.getUint16(pos, true); pos += 2; - if (compression !== COMPRESSION_DEFLATE) { - throw new Error(`File is not using DEFLATE compression but "${compression}"`); - } - - /** last mod file time */ - const lastMod1 = dv.getUint16(pos, true); pos += 2; - /** last mod file date */ - const lastMod2 = dv.getUint16(pos, true); pos += 2; - /** crc-32 */ - const fileCrc = dv.getUint32(pos, true); pos += 4; - /** compressed size */ - const comprSize = dv.getUint32(pos, true); pos += 4; - /** uncompressed size */ - const uncomprSize = dv.getUint32(pos, true); pos += 4; - /** file name length */ - const fileNameLength = dv.getUint16(pos, true); pos += 2; - /** extra field length */ - 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 - /** relative offset of local header */ - const relOffset = dv.getUint32(pos, true); pos += 4; - /** file name (variable size) */ - const fileName = Decoder.decode(new DataView(arrayBuffer, pos, fileNameLength)); pos += fileNameLength; - if (fileName !== 'metafile.solid') { - throw new Error(`Expected file name to be "metafile.solid" but it was "${fileName}".`); - } - - //read password from extra field - let encodedPassword: Uint8Array | undefined; - const extraFieldEnd = pos + extraFieldLength; - while (pos + 4 <= extraFieldEnd) { - const fieldId = dv.getUint16(pos, true); pos += 2; - const fieldLength = dv.getUint16(pos, true); pos += 2; - switch (fieldId) { - case 0x8810: { //password - if (fieldLength > 120) { - throw new Error(`Password is too long, it should be 120 characters at most but it is ${fieldLength} characters long.`); - } - const passwordLength = fieldLength; - encodedPassword = new Uint8Array(arrayBuffer, pos, fieldLength); - break; - } - case 0x80AE: //unknown, some kind of hash or checksum, ignore it - break; - default: - //unknown field, ignore it - } - pos += fieldLength; - } - if (typeof encodedPassword === 'undefined') { - throw new Error('Could not find password in extra field.'); - } - - const decodedPassword = modifyPassword(encodedPassword); - - //TODO: read encrypted + compressed file - //pos = start of central dir - // - //fseek(fp, pos - centralDirOffset, SEEK_SET); - //... - - return { lastMod1, lastMod2, fileCrc, comprSize, uncomprSize, fileNameLength, extraFieldLength, fileCommentLength, relOffset, fileName, encodedPassword, decodedPassword }; -} diff --git a/src/ssn/readSsnFile.ts b/src/ssn/readSsnFile.ts new file mode 100644 index 0000000..a38a9ff --- /dev/null +++ b/src/ssn/readSsnFile.ts @@ -0,0 +1,170 @@ +/** + * The file format used by Solid State Networks is based on the .zip format. + * Check the .ZIP File Format Specification . + */ + +import { TextDecoder } from 'util'; +import ISsnFileEntry from '../interfaces/ISsnFileEntry'; +import modifyPassword from './modifyPassword'; + +const SIGNATURE_END_OF_CENTRAL_DIR = 0x06054b50; +const SIGNATURE_CENTRAL_DIR = 0x02014b50; +const COMPRESSION_DEFLATE = 8; + +const Decoder = new TextDecoder('utf-8'); + +export default function readSsnFile(buffer: Buffer): ISsnFileEntry[] { + const arrayBuffer = buffer.buffer; + const dv = new DataView(arrayBuffer); + + const fileEntries: ISsnFileEntry[] = []; + + //--------------- READ END OF CENTRAL DIR --------------- + + //Go to end of file + let pos = buffer.length - 22; //end of central dir is at least 22 bytes long + + //Find end of central dir + while (pos >= 0 && dv.getUint32(pos, true) !== SIGNATURE_END_OF_CENTRAL_DIR) { + pos -= 1; + } + if (pos < 0) { + throw new Error('Could not find end of central dir.'); + } + pos += 4; + + /** skip 6 bytes: + * number of this disk, + * number of the disk with the start of the central directory + * total number of entries in the central directory on this disk + */ + pos += 6; + + /** Total number of entries in the central directory */ + const numEntries = dv.getUint16(pos, true); pos += 2; + /** Size of the central directory */ + const centralDirSize = dv.getUint32(pos, true); pos += 4; + /** Offset of start of central directory with respect to the starting disk number */ + const centralDirOffset = dv.getUint32(pos, true); pos += 4; + //ignore .ZIP file comment length + + if (centralDirSize < 46 * numEntries) { + throw new Error('centralDirSize was smaller than expected in end of central dir.'); + } + if (pos - centralDirSize < 0) { + throw new Error(`Central dir points before file start (0x${(pos - centralDirSize).toString(16)})`); + } + + //--------------- READ CENTRAL DIRECTORY --------------- + + //Go to start of central dir + pos -= 20 + centralDirSize; + + for (let i = 0; i < numEntries; i += 1) { + { + const signature = dv.getUint32(pos, true); pos += 4; + if (signature !== SIGNATURE_CENTRAL_DIR) { + throw new Error(`Expected central dir signature but found "0x${signature.toString(16)}"`); + } + } + + pos += 4; //skip version made by and version needed to extract + + /** The general purpose bit flag stores whether the file is encrypted or not. + * Most files are encrypted but there are some exceptions: assets_swtor_test_main_248to249.solidpkg, + * assets_swtor_test_en_us_270to271.solidpkg, and retailclient_publictest_246to247.solidpkg are not encrypted. + */ + const bitFlag = dv.getUint16(pos, true); pos += 2; + + /** Compression method, always set to 8 = DEFLATE. */ + const compression = dv.getUint16(pos, true); pos += 2; + if (compression !== COMPRESSION_DEFLATE) { + throw new Error(`File is not using DEFLATE compression but "${compression}"`); + } + + /** last mod file time */ + const lastModTime = dv.getUint16(pos, true); pos += 2; + /** last mod file date */ + const lastModDate = dv.getUint16(pos, true); pos += 2; + /** crc-32 */ + const fileCrc = dv.getUint32(pos, true); pos += 4; + /** compressed size */ + const comprSize = dv.getUint32(pos, true); pos += 4; + /** uncompressed size */ + const uncomprSize = dv.getUint32(pos, true); pos += 4; + /** file name length */ + const fileNameLength = dv.getUint16(pos, true); pos += 2; + /** extra field length */ + 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 + /** relative offset of local header */ + const relOffset = dv.getUint32(pos, true); pos += 4; + /** file name (variable size) */ + const fileName = Decoder.decode(new DataView(arrayBuffer, pos, fileNameLength)); pos += fileNameLength; + if (fileName !== 'metafile.solid') { + throw new Error(`Expected file name to be "metafile.solid" but it was "${fileName}".`); + } + + //read password from extra field + const extraFieldEnd = pos + extraFieldLength; + let encodedPassword: Uint8Array | undefined; + let diffType: ISsnFileEntry['diffType'] = -1; + let diffSourceLength = -1; + let diffDestLength = -1; + while (pos + 4 <= extraFieldEnd) { + const fieldId = dv.getUint16(pos, true); pos += 2; + const fieldLength = dv.getUint16(pos, true); pos += 2; + switch (fieldId) { + case 0x8810: { //password + if (fieldLength > 120) { + throw new Error(`Password is too long, it should be 120 characters at most but it is ${fieldLength} characters long.`); + } + const passwordLength = fieldLength; + encodedPassword = new Uint8Array(arrayBuffer, pos, fieldLength); + break; + } + case 0x80AE: //diff type + //type: 0 = no diff (usually a new file), 1 = file deleted, 2 = vcdiff/xdelta3, 3=unchanged + diffType = dv.getUint32(pos, true); pos += 4; + //size of source file, is actually a uint64 but we only read 32-bits + diffSourceLength = dv.getUint32(pos, true); pos += 8; + //size of destination file, is actually a uint64 but we only read 32-bits + diffDestLength = dv.getUint32(pos + 12, true); pos += 8; + //skip 20 bytes: hash of old file + //skip 20 bytes: hash of new file + pos += 40; + break; + default: + //unknown field, ignore it + } + pos += fieldLength; + } + if (typeof encodedPassword === 'undefined') {// && (bitFlag & 1) !== 0 TODO: check if file is encrypted, otherwise no password needed + throw new Error('File was encrypted but could not find password in extra field.'); + } + + const decodedPassword = modifyPassword(encodedPassword); + + pos += fileCommentLength; + + fileEntries.push({ + crc: fileCrc, + diffDestLength, + diffSourceLength, + diffType, + lastMod: new Date(1980 + (lastModDate >>> 9), (lastModDate & 0x1E0) >>> 5, lastModDate & 0x1F, lastModTime >>> 11, (lastModTime & 0x7E0) >>> 5, (lastModTime & 0x1F) * 2), + name: fileName, + size: uncomprSize, + }); + } + + //TODO: read encrypted + compressed file + //pos = start of central dir + // + //fseek(fp, pos - centralDirOffset, SEEK_SET); + //... + + return fileEntries; +}