♻️ readSsnFile now reads all file entries

This commit is contained in:
C-3PO 2018-06-22 13:13:09 +02:00
parent 1a623aa42d
commit 9899514578
Signed by: c3po
GPG key ID: 62993C4BB4D86F24
4 changed files with 202 additions and 147 deletions

View file

@ -1,10 +1,10 @@
import getSolidpkg from './ssn/getSolidpkg'; import getSolidpkg from './ssn/getSolidpkg';
import readSolidpkg from './ssn/readSolidpkg'; import readSsnFile from './ssn/readSsnFile';
(async () => { (async () => {
const buffer = await getSolidpkg('assets_swtor_de_de', -1, 0); const buffer = await getSolidpkg('assets_swtor_de_de', -1, 0);
console.log(buffer.length, buffer); console.log(buffer.length, buffer);
const output = readSolidpkg(buffer); const fileEntries = readSsnFile(buffer);
console.log(output); console.log(fileEntries);
})(); })();

View file

@ -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;

View file

@ -1,144 +0,0 @@
/**
* The file format used by Solid State Networks is based on the .zip format.
* Check the .ZIP File Format Specification <https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT>.
*/
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 };
}

170
src/ssn/readSsnFile.ts Normal file
View file

@ -0,0 +1,170 @@
/**
* The file format used by Solid State Networks is based on the .zip format.
* Check the .ZIP File Format Specification <https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT>.
*/
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;
}