//To swap between mjs/esm and c6js, go to the end of this file, and (un)comment your wanted module mode. var ICONJS_DEBUG = false; var ICONJS_STRICT = true; /** * The current version of the library. * @constant {string} * @default */ const ICONJS_VERSION = "0.8.1"; /** * The RC4 key used for ciphering CodeBreaker Saves. * @constant {Uint8Array} */ const ICONJS_CBS_RC4_KEY = new Uint8Array([ 0x5f, 0x1f, 0x85, 0x6f, 0x31, 0xaa, 0x3b, 0x18, 0x21, 0xb9, 0xce, 0x1c, 0x07, 0x4c, 0x9c, 0xb4, 0x81, 0xb8, 0xef, 0x98, 0x59, 0xae, 0xf9, 0x26, 0xe3, 0x80, 0xa3, 0x29, 0x2d, 0x73, 0x51, 0x62, 0x7c, 0x64, 0x46, 0xf4, 0x34, 0x1a, 0xf6, 0xe1, 0xba, 0x3a, 0x0d, 0x82, 0x79, 0x0a, 0x5c, 0x16, 0x71, 0x49, 0x8e, 0xac, 0x8c, 0x9f, 0x35, 0x19, 0x45, 0x94, 0x3f, 0x56, 0x0c, 0x91, 0x00, 0x0b, 0xd7, 0xb0, 0xdd, 0x39, 0x66, 0xa1, 0x76, 0x52, 0x13, 0x57, 0xf3, 0xbb, 0x4e, 0xe5, 0xdc, 0xf0, 0x65, 0x84, 0xb2, 0xd6, 0xdf, 0x15, 0x3c, 0x63, 0x1d, 0x89, 0x14, 0xbd, 0xd2, 0x36, 0xfe, 0xb1, 0xca, 0x8b, 0xa4, 0xc6, 0x9e, 0x67, 0x47, 0x37, 0x42, 0x6d, 0x6a, 0x03, 0x92, 0x70, 0x05, 0x7d, 0x96, 0x2f, 0x40, 0x90, 0xc4, 0xf1, 0x3e, 0x3d, 0x01, 0xf7, 0x68, 0x1e, 0xc3, 0xfc, 0x72, 0xb5, 0x54, 0xcf, 0xe7, 0x41, 0xe4, 0x4d, 0x83, 0x55, 0x12, 0x22, 0x09, 0x78, 0xfa, 0xde, 0xa7, 0x06, 0x08, 0x23, 0xbf, 0x0f, 0xcc, 0xc1, 0x97, 0x61, 0xc5, 0x4a, 0xe6, 0xa0, 0x11, 0xc2, 0xea, 0x74, 0x02, 0x87, 0xd5, 0xd1, 0x9d, 0xb7, 0x7e, 0x38, 0x60, 0x53, 0x95, 0x8d, 0x25, 0x77, 0x10, 0x5e, 0x9b, 0x7f, 0xd8, 0x6e, 0xda, 0xa2, 0x2e, 0x20, 0x4f, 0xcd, 0x8f, 0xcb, 0xbe, 0x5a, 0xe0, 0xed, 0x2c, 0x9a, 0xd4, 0xe2, 0xaf, 0xd0, 0xa9, 0xe8, 0xad, 0x7a, 0xbc, 0xa8, 0xf2, 0xee, 0xeb, 0xf5, 0xa6, 0x99, 0x28, 0x24, 0x6c, 0x2b, 0x75, 0x5d, 0xf8, 0xd3, 0x86, 0x17, 0xfb, 0xc0, 0x7b, 0xb3, 0x58, 0xdb, 0xc7, 0x4b, 0xff, 0x04, 0x50, 0xe9, 0x88, 0x69, 0xc9, 0x2a, 0xab, 0xfd, 0x5b, 0x1b, 0x8a, 0xd9, 0xec, 0x27, 0x44, 0x0e, 0x33, 0xc8, 0x6b, 0x93, 0x32, 0x48, 0xb6, 0x30, 0x43, 0xa5 ]); /** * Extension of DataView to add shortcuts for datatypes that I use often. * @augments DataView * @constructor * @param {ArrayBuffer} buffer ArrayBuffer to base DataView from. * @returns {!Object.} [u16le, f16le, u32le, f32le] * @returns {!Object.>} [t64le] * @access protected */ class yellowDataReader extends DataView { /** Unsigned 16-bit, Little Endian. * @param {number} i Indice offset. * @returns {number} */ u16le(i){return super.getUint16(i, 1)}; /** Fixed-point 16-bit, Little Endian. * @param {number} i Indice offset. * @returns {number} */ f16le(i){return (super.getInt16(i, 1) / 4096)}; /** Unsigned 32-bit, Little Endian. * @param {number} i Indice offset. * @returns {number} */ u32le(i){return super.getUint32(i, 1)}; /** Floating-point 32-bit, Little Endian. * @param {number} i Indice offset. * @returns {number} */ f32le(i){return super.getFloat32(i, 1)}; /** 64-bit Timestamp structure, Little Endian. * Time returned is set for JST (UTC+09:00) instead of UTC. * Time returned is going to be offseted for JST (GMT+09:00). * @param {number} i Indice offset. * @returns {Object.} * @property {number} seconds - Seconds. * @property {number} minutes - Minutes. * @property {number} hours - Hours. * @property {number} day - Day. * @property {number} month - Month. * @property {number} year - Year. */ t64le(i){return { seconds: super.getUint8(i+1), minutes: super.getUint8(i+2), hours: super.getUint8(i+3), day: super.getUint8(i+4), month: super.getUint8(i+5), year: super.getUint16(i+6, 1) }}; constructor(buffer) { super(buffer); return { u16le: this.u16le.bind(this), f16le: this.f16le.bind(this), u32le: this.u32le.bind(this), f32le: this.f32le.bind(this), t64le: this.t64le.bind(this) } } } /** * Implements an RC4 cipher. * @param {TypedArray|Uint8Array} key - 256-byte key * @param {TypedArray|Uint8Array} target - n-length data to cipher * @returns {!Uint8Array} target ciphered by key * @access protected */ function rc4Cipher(key, target) { //todo: support keys that aren't exactly 256-bytes long let myNewKey = new Uint8Array(key); let deciphered = new Uint8Array(target); let temp = 0; for (let index = 0; index < target.length; index++) { let indice = (index + 1) % 256; temp = (temp + myNewKey[indice]) % 256; let backup = myNewKey[indice]; myNewKey[indice] = myNewKey[temp]; myNewKey[temp] = backup; deciphered[index] ^= myNewKey[(myNewKey[indice] + myNewKey[temp]) % 256]; } return deciphered; } /** * Enable or disable use of debugging information in console via console.debug() * @param {boolean} value - Enable/disable this feature * @default false * @public */ function setDebug(value) { ICONJS_DEBUG = !!value; } /** * Select if invalid characters in titles should be replaced with either spaces or nulls * @param {boolean} value - true: with nulls, false: with spaces * @default true * @deprecated Hasn't been needed for a while. Dropping support by ESM transition. * @public */ function setStrictness(value) { console.info("setStrictness is deprecated!"); ICONJS_STRICT = !!value; } /** * Converts a texture format to a generalized texture type character. * @param {number} i - texture format * @returns {string} U: uncompressed, N: none, C: compressed * @access protected */ function getTextureFormat(i) { if (i<8) { if(i==3) { return 'N'; } return 'U'; } else if (i>=8) { return 'C'; } else { return void(0); } } /** * Decompress a compressed texture using RLE. * @param {ArrayBuffer} input - texture * @returns {!Uint16Array} decompressed texture, equivalent to an uncompressed texture * @public */ function uncompressTexture(texData) { // for texture formats 8-15 if (texData.length & 1) { throw "Texture size isn't a multiple of 2 (was ${texData.length})"; } const {u16le} = new yellowDataReader(texData); let uncompressed = new Uint16Array(16384); let offset = 0; for (let index = 0; index < 16384;) { let currentValue = u16le(offset); if(currentValue === 0) { // if this is specifically a katamari 1 or 2 icon, skip this byte // because it's formatted like that for some reason offset += 2; currentValue = u16le(offset); } offset += 2; if (currentValue >= 0xfe00) { // everyone says this is 0xff00 but gauntlet:DL tells me its lower //do a raw copy of the next currentValue bytes let length = ((0x10000 - currentValue)); for (let enumerator = 0; enumerator < length; enumerator++) { uncompressed[index] = u16le(offset); offset += 2; index++; } } else { //repeat next byte currentValue times uncompressed[index] = u16le(offset); for (let indey = 0; indey < currentValue; indey++) { uncompressed[index] = u16le(offset); index++ } offset += 2; } } return uncompressed; } /** * Converts a BGR5A1 texture to a RGB5A1 texture * @param {Uint16Array} bgrData - texture * @returns {!Uint16Array} converted texture * @public */ function convertBGR5A1toRGB5A1(bgrData) { if(bgrData.byteLength !== 32768) { throw `Not a 128x128x16 texture. (length was ${bgrData.length})`; } // converts 5-bit blue, green, red (in that order) with one alpha bit to GL-compatible RGB5A1 let converted = new Uint16Array(16384); for (let index = 0; index < 16384; index++) { let b = ( bgrData[index] & 0b11111); let g = ((bgrData[index] >> 5) & 0b11111); let r = ((bgrData[index] >> 10) & 0b11111); // rrrrrgggggbbbbba (a = 1 because 0 is 127 which is 1.0f opacity for the GS) let newValue = 0b0000000000000001; // mind you, the alpha bit ^ seems to be set randomly on textures, anyway. Maybe i'm reading these wrong? newValue |= (r << 1); newValue |= (g << 6); newValue |= (b << 11); converted[index] = newValue; } return converted; } /** * Takes a string and returns the first part before a null character. * @param {string} dirty - String to slice from first null character. * @returns {string} Substring of dirty before first null character. * @access protected */ function stringScrubber(dirty) { return dirty.replace(/\0/g, "").substring(0, (dirty.indexOf("\x00") === -1) ? dirty.length : dirty.indexOf("\x00")); } /** * Read a PS2D format file (icon.sys) * @param {ArrayBuffer} input - icon.sys formatted file * @returns {Object} (user didn't write a description) * @public */ function readPS2D(input) { const {u32le, f32le} = new yellowDataReader(input); //!pattern ps2d.hexpat const header = u32le(0); if (header !== 0x44325350) { throw `Not a PS2D file (was ${header}, expected ${0x44325350})`; } //:skip 2 const titleOffset = u32le(6); //:skip 2 const bgAlpha = u32le(12); // should read as a u8, (k/127?.5) for float value (255 = a(2.0~)) const bgColors = [ {r: u32le(16), g: u32le(20), b: u32le(24), a: u32le(28)}, {r: u32le(32), g: u32le(36), b: u32le(40), a: u32le(44)}, {r: u32le(48), g: u32le(52), b: u32le(56), a: u32le(60)}, {r: u32le(64), g: u32le(68), b: u32le(72), a: u32le(76)} ]; // top-left, top-right, bottom-left, bottom-right; const lightIndices = [ {x: f32le(80), y: f32le(84), z: f32le(88)}, //:skip 4 {x: f32le(96), y: f32le(100), z: f32le(104)}, //:skip 4 {x: f32le(112), y: f32le(116), z: f32le(120)} //:skip 4 ] const lightColors = [ {r: f32le(128), g: f32le(132), b: f32le(136), a: f32le(140)}, {r: f32le(144), g: f32le(148), b: f32le(152), a: f32le(156)}, {r: f32le(160), g: f32le(164), b: f32le(168), a: f32le(172)}, {r: f32le(176), g: f32le(180), b: f32le(184), a: f32le(188)} ] // P2SB says color 1 is ambient, 2-4 are for 3-point cameras // official HDD icon.sys files (completely different PS2ICON text-based format) also say the same. const int_title = input.slice(0xc0, 0x100); const tmp_title16 = new Uint16Array(int_title); for (let index = 0; index < 32; index++) { //find "bad" shift-jis two-bytes, and convert to spaces (or NULL if strict) if(tmp_title16[index] === 129 || tmp_title16[index] === 33343) { console.warn(`PS2D syntax error: known bad two-byte sequence 0x${tmp_title16[index].toString(16).padEnd(4,"0")} (at ${index}). Replacing with ${(ICONJS_STRICT) ? "NULL" : "0x8140"}.\n${(ICONJS_STRICT) ? "This is console-accurate, so" : "An actual console does not do this."} I recommend patching your icon.sys files!`); tmp_title16[index] = (ICONJS_STRICT) ? 0 : 0x4081; // is a reference, so this'll edit int_title too } } //:skip 4 -- Unless proven, keep 64 bytes for display name. const int_filename_n = input.slice(0x104, 0x143); const int_filename_c = input.slice(0x144, 0x183); const int_filename_d = input.slice(0x184, 0x1C3); //;skip 512 -- rest of this is just padding const rawTitle = (new TextDecoder("shift-jis")).decode(int_title); const title = [ stringScrubber(rawTitle.substring(0,(titleOffset/2))), (rawTitle.indexOf("\x00") < (titleOffset/2)) ? "" : stringScrubber(rawTitle.substring((titleOffset/2))) ]; const filenames = { n: stringScrubber((new TextDecoder("utf-8")).decode(int_filename_n)), c: stringScrubber((new TextDecoder("utf-8")).decode(int_filename_c)), d: stringScrubber((new TextDecoder("utf-8")).decode(int_filename_d)) } if(ICONJS_DEBUG){ console.debug({header, titleOffset, bgAlpha, bgColors, lightIndices, lightColors, title, filenames}); } return {filenames, title, background: {colors: bgColors, alpha: bgAlpha}, lighting: {points: lightIndices, colors: lightColors}}; } /** * Read a Model file ({*}, usually ICN or ICO, however) * @param {ArrayBuffer} input - icon model file * @returns {Object} (user didn't write a description) * @public */ function readIconFile(input) { //!pattern ps2icon-hacked.hexpat const {u32le, f32le, f16le} = new yellowDataReader(input); const u32_rgba8 = function(i) {return { r: (i & 0xff), g: ((i & 0xff00) >> 8), b: ((i & 0xff0000) >> 16), a: (i > 0x7fffffff ? 255 : (((i & 0xff000000) >>> 24) * 2)+1) // I don't think alpha transparency is actually USED in icons?, rendering with it looks strange. }}; const magic = u32le(0); if (magic !== 0x010000) { // USER NOTICE: So far, I have yet to parse an icon that hasn't had 0x00010000 as it's magic. // Can someone provide me pointers to such an icon if one exists? throw `Not a PS2 icon file (was ${magic}, expected ${0x010000})`; } const numberOfShapes = u32le(4); if(numberOfShapes > 16) { throw `Too many defined shapes! Is this a valid file? (file reports ${numberOfShapes} shapes)`; } const textureType = u32le(8); const textureFormat = getTextureFormat(textureType); //:skip 4 const numberOfVertexes = u32le(16); if(!!(numberOfVertexes % 3)){ throw `Not enough vertices to define a triangle (${numberOfVertexes % 3} vertices remained).`; } // format: [xxyyzzaa * numberOfShapes][xxyyzzaa][uuvvrgba], ((8 * numberOfShapes) + 16) [per chunk] let offset = 20; let vertices = new Array(); const chunkLength = ((numberOfShapes * 8) + 16); // for numberOfVertexes, copy chunkLength x times, then append with normal, uv and rgba8 color. Floats are 16-bit. for (let index = 0; index < numberOfVertexes; index++) { let shapes = new Array(); for (let indey = 0; indey < numberOfShapes; indey++) { shapes.push({ x: f16le(offset+((chunkLength*index)+(indey*8))), y: f16le(offset+((chunkLength*index)+(indey*8))+2), z: f16le(offset+((chunkLength*index)+(indey*8))+4) }); //:skip 2 } let normal = { x: f16le(offset+(chunkLength*index)+((numberOfShapes * 8))), y: f16le(offset+(chunkLength*index)+((numberOfShapes * 8))+2), z: f16le(offset+(chunkLength*index)+((numberOfShapes * 8))+4) }; //:skip 2 let uv = { u: f16le(offset+(chunkLength*index)+((numberOfShapes * 8))+8), v: f16le(offset+(chunkLength*index)+((numberOfShapes * 8))+10) }; let color = u32_rgba8(u32le(offset+(chunkLength*index)+((numberOfShapes * 8))+12)); // keep original u32le color? vertices.push({shapes, normal, uv, color}); } offset = (20+(numberOfVertexes * chunkLength)); animationHeader = {id: u32le(offset), length: u32le(offset+4), speed: f32le(offset+8), "offset": u32le(offset+12), keyframes: u32le(offset+16)}; let animData = new Array(); // now we have to enumerate values, so now we introduce an offset value. // format for a keyframe: sssskkkk[ffffvvvv] where [ffffvvvv] repeat based on the value that kkkk(eys) has. // sssskkkk[ffffvvvv] is repeated based on animationHeader.keyframes value. offset += 20; for (let index = 0; index < animationHeader.keyframes; index++) { let frameData = new Array(); let shapeId = u32le(offset); let keys = u32le(offset+4); offset += 8; for (let indey = 0; indey < keys; indey++) { frameData.push({frame: f32le(offset), value: f32le(offset+4)}); offset += 8; } animData.push({shapeId, keys, frameData}); } let texture = null; switch(textureFormat) { case 'N': { break; } case 'U': { //where every 16-bit entry is a BGR5A1 color 0b[bbbbbgggggrrrrra] texture = new Uint16Array(input.slice(offset, (offset+0x8000))); //see convertBGR5A1toRGB5A1() for more info. break; } case 'C': { // compression format is RLE-based, where first u32 is size, and format is defined as: /** * u16 rleType; * if (rleType >= 0xff00) { * //do a raw copy * let length = (0x10000 - rleType); * byte data[length]; * } else { * //repeat next byte rleType times * data = new Uint16Array(rleType); * for (let index = 0; index < rleType; index++) { * data[index] = u16 repeater @ +4; * } * } **/ //output of this will be another u16[0x4000] of the decompressed texture //after that just parse output as-if it was uncompressed. //see uncompressTexture() and convertBGR5A1toRGB5A1() for more info. size = u32le(offset); texture = {size, data: input.slice(offset+4, offset+(4+size))}; } } if(ICONJS_DEBUG){ console.debug({magic, numberOfShapes, textureType, textureFormat, numberOfVertexes, chunkLength, vertices, animationHeader, animData, texture}); } return {numberOfShapes, vertices, textureFormat, texture, animData}; } /** * Read a 512-byte file descriptor that is used on Memory Cards or PSU files. * @param {ArrayBuffer} input - File descriptor segment. * @returns {Object} (user didn't write a description) * @access protected */ function readEntryBlock(input) { const {u32le, t64le} = new yellowDataReader(input); //!pattern psu_file.hexpat const permissions = u32le(0); let type; if (permissions>0xffff) { throw `Not a EMS Memory Adapter (PSU) export file (was ${permissions}, expected less than ${0xffff})`; } if((permissions & 0b00100000)>=1){ type = "directory"; } if((permissions & 0b00010000)>=1){ type = "file"; } if((permissions & 0b0001100000000000)>=1){ throw `I don't parse portable applications or legacy save data. (${permissions} has bits 10 or 11 set)`; } const size = u32le(4); const sectorOffset = u32le(16); const dirEntry = u32le(20); const timestamps = {created: t64le(8), modified: t64le(24)}; const specialSection = input.slice(0x20, 0x40); const int_filename = input.slice(0x40, 512); const filename = stringScrubber((new TextDecoder("utf-8")).decode(int_filename)); if(ICONJS_DEBUG){ console.debug({permissions, type, size, sectorOffset, dirEntry, timestamps, specialSection, filename}); } return {type, size, filename, timestamps}; } /** * Read a EMS Memory Adapter export file (PSU format) * @param {ArrayBuffer} input - PSU formatted file * @returns {Object} (user didn't write a description) * @public */ function readEmsPsuFile(input){ const header = readEntryBlock(input.slice(0,0x1ff)); if(header.size > 0x7f) { throw `Directory is too large! (maximum size: ${0x7f}, was ${header.size})`; } let fsOut = {length: header.size, rootDirectory: header.filename, timestamps: header.timestamps}; let output = new Object(); let offset = 512; for (let index = 0; index < header.size; index++) { fdesc = readEntryBlock(input.slice(offset, offset + 512)); switch(fdesc.type) { case "directory": { offset += 512; output[fdesc.filename] = null; break; } case "file": { if(ICONJS_DEBUG){ console.debug(`PARSING | F: "${fdesc.filename}" O: ${offset} S: ${fdesc.size}`); } offset += 512; const originalOffset = offset; if((fdesc.size % 1024) > 0) { offset += ((fdesc.size & 0b11111111110000000000) + 1024); } else { offset += fdesc.size; // if we're already filling 1k blocks fully, why change the value? } output[fdesc.filename] = { size: fdesc.size, data: input.slice(originalOffset, originalOffset+fdesc.size) }; break; } } } fsOut[header.filename] = output; return fsOut; } /** * Read a PS3 save export file (PSV format) * @param {ArrayBuffer} input - PSV formatted file * @returns {Object} (user didn't write a description) * @public */ function readPsvFile(input){ const {u32le, t64le} = new yellowDataReader(input); //!pattern psv_file.hexpat const magic = u32le(0); if (magic !== 0x50535600) { throw `Not a PS3 export (PSV) file (was ${magic}, expected ${0x50535600})`; } //:skip 4 //:skip 20 // key seed, console ignores this //:skip 20 // sha1 hmac digest, useful for verifying that this is, indeed, save data. //:skip 8 const type1 = u32le(56); const type2 = u32le(60); if(type1 !== 0x2c && type2 !== 2) { throw `Not parsing, this is not in the PS2 save export format (was ${type1}:${type2}, expected 44:2)`; } const displayedSize = u32le(64); const ps2dOffset = u32le(68); const ps2dSize = u32le(72); // don't know why this is included if its always 964 const nModelOffset = u32le(76); const nModelSize = u32le(80); const cModelOffset = u32le(84); const cModelSize = u32le(88); const dModelOffset = u32le(92); const dModelSize = u32le(96); const numberOfFiles = u32le(100); // in-case this library changes stance on other files // file = {t64le created, t64le modified, u32 size, u32 permissions, byte[32] title} // and if it's not the root directory, add another u32 for offset/location const rootDirectoryData = input.slice(104, 162); const timestamps = {created: t64le(104), modified: t64le(112)}; let offset = 162; let fileData = new Array(); for (let index = 0; index < numberOfFiles; index++) { fileData.push(input.slice(offset,offset+0x3c)); offset += 0x3c; }; //then file data after this but we already have pointers to the files we care about const icons = { n: input.slice(nModelOffset, nModelOffset+nModelSize), c: input.slice(cModelOffset, cModelOffset+cModelSize), d: input.slice(dModelOffset, dModelOffset+dModelSize), } if (ICONJS_DEBUG) { console.debug({magic, type1, type2, displayedSize, ps2dOffset, ps2dSize, nModelOffset, nModelSize, cModelOffset, cModelSize, dModelOffset, dModelSize, numberOfFiles, rootDirectoryData, fileData}) } return {icons, "icon.sys": input.slice(ps2dOffset, ps2dOffset+ps2dSize), timestamps}; } /** * Read a SPS or XPS file descriptor. * @param {ArrayBuffer} input - File descriptor segment. * @returns {Object} (user didn't write a description) * @access protected */ function readSxpsDescriptor(input) { const {u32le, t64le} = new yellowDataReader(input); //!pattern sps-xps_file.hexpat //:skip 2 // ... it's the file descriptor block size (including the bytes themselves, so 250) const int_filename = input.slice(2, 66); const filename = stringScrubber((new TextDecoder("utf-8")).decode(int_filename)); const size = u32le(66); const startSector = u32le(70); const endSector = u32le(74); const permissions = u32le(78); // the first two bytes are *swapped*. The comments that ensued were not kept. let type; if (permissions>0xffff) { throw `Not a SharkPort (SPS) or X-Port (XPS) export file (was ${permissions}, expected less than ${0xffff})`; } if((permissions & 0b0010000000000000)>=1){ type = "directory"; } if((permissions & 0b0001000000000000)>=1){ type = "file"; } if((permissions & 0b00011000)>=1){ throw `I don't parse portable applications or legacy save data. (${permissions} has bits 4 or 5 set)`; } const timestamps = {created: t64le(82), modified: t64le(90)}; //:skip 4 //:skip 4 - u32 optional (98) //:skip 8 - t64 optionalTime (102) // I don't know why this is here. const int_asciiName = input.slice(114, 178); const int_shiftjisName = input.slice(178, 242); // Because why parse a PS2D when you can hard-code it? //:skip 8 if(ICONJS_DEBUG) { console.debug({int_filename, size, startSector, endSector, permissions, type, timestamps, int_asciiName, int_shiftjisName}); } return {type, size, filename, timestamps}; } /** * Read a SharkPort or X-Port export file (SPS or XPS format) * @param {ArrayBuffer} input - SPS|XPS formatted file * @returns {Object} (user didn't write a description) * @public */ function readSharkXPortSxpsFile(input) { const {u32le} = new yellowDataReader(input); //!pattern sps-xps_file.hexpat const identLength = u32le(0); if(identLength !== 13) { throw `Not a SharkPort (SPS) or X-Port (XPS) export file (was ${identLength}, expected 13)`; } let offset = 4; const ident = input.slice(offset, offset+identLength); if((new TextDecoder("utf-8")).decode(ident) !== "SharkPortSave") { throw `Unrecognized file identification string. Expected "SharkPortSave".`; } offset += (identLength + 4); const titleLength = u32le(offset); const title = input.slice(offset + 4, (offset + 4) + titleLength); offset += (titleLength + 4); const descriptionLength = u32le(offset); const description = input.slice(offset + 4, (offset + 4) + descriptionLength); offset += (descriptionLength + 4); const description2Length = u32le(offset); let description2; if(description2Length !== 0) { description2 = input.slice(offset + 4, (offset + 4) + description2Length); offset += (description2Length + 4); } else { offset += 4; } const comments = { "game": stringScrubber((new TextDecoder("utf-8")).decode(title)), "name": stringScrubber((new TextDecoder("utf-8")).decode(description)) } if(description2Length !== 0) { comments.desc = stringScrubber((new TextDecoder("utf-8")).decode(description2)); } const totalSize = u32le(offset); offset += 4; const header = readSxpsDescriptor(input.slice(offset, offset + 250)); offset += 250; // alright now lets parse some actual data let fsOut = {length: header.size, rootDirectory: header.filename, timestamps: header.timestamps, comments}; let output = new Object(); for (let index = 0; index < (header.size - 2); index++) { fdesc = readSxpsDescriptor(input.slice(offset, offset + 250)); switch(fdesc.type) { case "directory": { offset += 250; output[fdesc.filename] = null; break; } case "file": { if(ICONJS_DEBUG){ console.debug(`PARSING | F: "${fdesc.filename}" O: ${offset} S: ${fdesc.size}`); } offset += 250; output[fdesc.filename] = { size: fdesc.size, data: input.slice(offset, offset+fdesc.size) }; offset += fdesc.size; break; } } } fsOut[header.filename] = output; //:skip 4 // then here lies, at offset (the end of file), a u32 checksum. return fsOut; } /** * Read a CodeBreaker Save (CBS) file's directory structure * @param {ArrayBuffer} input - Uncompressed, unciphered input * @returns {Object} (user didn't write a description) * @protected */ function readCodeBreakerCbsDirectory(input) { const {u32le, t64le} = new yellowDataReader(input); const virtualFilesystem = new Object(); for (let offset = 0; offset < input.byteLength;) { const timestamps = {created: t64le(offset), modified: t64le(offset+8)}; const dataSize = u32le(offset+16); const permissions = u32le(offset+20); offset += 32; const _filename = input.slice(offset, offset+32); const filename = stringScrubber((new TextDecoder("utf-8")).decode(_filename)); offset += 32; const data = input.slice(offset, offset+dataSize); offset += dataSize; virtualFilesystem[filename] = ({timestamps, dataSize, permissions, data}); } return virtualFilesystem; } /** * Read a CodeBreaker Save (CBS) file. * @param {ArrayBuffer} input - CBS formatted file * @param {function(Uint8Array): ArrayBuffer} inflator - a function which provides a zlib-compatible inflate function. * @returns {Object} (user didn't write a description) * @public */ function readCodeBreakerCbsFile(input, inflator = null) { if(typeof inflator !== "function") { throw `No inflator function passed. Skipping.`; } const {u32le, t64le} = new yellowDataReader(input); const magic = u32le(0); if (magic !== 0x00554643) { throw `Not a CodeBreaker Save (CBS) file (was ${magic}, expected ${0x00554643})`; } //u32le(4); something? it's always 8000 const dataOffset = u32le(8); //const uncompressedSize = u32le(12); const compressedSize = u32le(16); const _dirName = input.slice(20, 52); const dirName = stringScrubber((new TextDecoder("utf-8")).decode(_dirName)); const timestamps = {created: t64le(52), modified: t64le(60)}; const permissions = u32le(72); if (permissions>0xffff) { throw `Not a valid export file (was ${permissions}, expected less than ${0xffff})`; } if((permissions & 0b0001100000000000)>=1){ throw `I don't parse portable applications or legacy save data. (${permissions} has bits 10 or 11 set)`; } const _displayName = input.slice(92, 296); const displayName = stringScrubber((new TextDecoder("utf-8")).decode(_displayName)); const compressedData = input.slice(dataOffset, dataOffset + compressedSize); const decipheredData = rc4Cipher(ICONJS_CBS_RC4_KEY, new Uint8Array(compressedData)); const inflatedData = inflator(decipheredData); const fsOut = {rootDirectory: dirName, timestamps}; fsOut[dirName] = readCodeBreakerCbsDirectory(inflatedData); if(ICONJS_DEBUG) { console.debug({magic, dataOffset, compressedSize, dirName, permissions, displayName}); } return fsOut; } /** * Read a Max Drive (MAX) or PowerSave (PWS) file's directory structure. * @param {ArrayBuffer} input - Uncompressed input * @returns {Object} (user didn't write a description) * @protected */ function readMaxPwsDirectory(input, directorySize) { const {u32le} = new yellowDataReader(input); virtualFilesystem = new Object(); let offset = 0; for (let index = 0; index < directorySize; index++) { const dataSize = u32le(offset); const _filename = input.slice(offset+4, offset+36); const filename = stringScrubber((new TextDecoder("utf-8")).decode(_filename)); if(filename === "") { throw `Unexpected null filename at byte ${offset+4}.`; }; offset += 36; const data = input.slice(offset, offset+dataSize); offset += dataSize; if(index !== directorySize - 1) { while((offset & 15) !== 8) { offset++; } } virtualFilesystem[filename] = ({dataSize, data}); } return virtualFilesystem; } /** * Read a Max Drive (MAX) or PowerSave (PWS) file. * @param {ArrayBuffer} input - MAX/PWS formatted file * @param {function(Uint8Array): ArrayBuffer} unlzari - a function which provides a LZARI-compatible decompression function. * @returns {Object} (user didn't write a description) * @public */ function readMaxPwsFile(input, unlzari) { if(typeof unlzari !== "function") { throw `No decompresser function passed. Skipping.`; } const {u32le} = new yellowDataReader(input); const ident = input.slice(0, 12); if((new TextDecoder("utf-8")).decode(ident) !== "Ps2PowerSave") { throw `Unrecognized file identification string. Expected "Ps2PowerSave".`; } //:skip 4 (u32 checksum) const _dirName = input.slice(0x10, 0x30); const dirName = stringScrubber((new TextDecoder("utf-8")).decode(_dirName)); const _displayName = input.slice(0x30, 0x50); const displayName = stringScrubber((new TextDecoder("utf-8")).decode(_displayName)); const compressedSize = u32le(0x50); if(compressedSize !== (input.byteLength - 88)) { console.warn(`This file says it's larger then it actually is! (Given size: ${compressedSize}, actual size: ${input.byteLength - 88})`); } const size = u32le(0x54); const compressedData = input.slice(88, input.byteLength); const uncompressedData = unlzari(new Uint8Array(compressedData)); // read above why we can't trust given size const fsOut = {rootDirectory: dirName}; fsOut[dirName] = readMaxPwsDirectory(uncompressedData, size); // there's no... timestamps or permissions... this doesn't bode well. if(ICONJS_DEBUG) { console.debug({ident, compressedSize, dirName, displayName}); } return fsOut; } /** * Define (module.)exports with all public functions. * @exports icondumper2/icon */ // start c6js if(typeof exports !== "object") { exports = { readers: {readIconFile, readPS2D, readEmsPsuFile, readPsvFile, readSharkXPortSxpsFile, readCodeBreakerCbsFile, readMaxPwsFile}, helpers: {uncompressTexture, convertBGR5A1toRGB5A1}, options: {setDebug, setStrictness}, version: ICONJS_VERSION }; } else { exports.readers = {readIconFile, readPS2D, readEmsPsuFile, readPsvFile, readSharkXPortSxpsFile, readCodeBreakerCbsFile, readMaxPwsFile}; exports.helpers = {uncompressTexture, convertBGR5A1toRGB5A1}; exports.options = {setDebug, setStrictness}; exports.version = ICONJS_VERSION; } if(typeof module !== "undefined") { module.exports = exports; } //end c6js //start esm /*export { readIconFile, readPS2D, readEmsPsuFile, readPsvFile, readSharkXPortSxpsFile, readCodeBreakerCbsFile, readMaxPwsFile, uncompressTexture, convertBGR5A1toRGB5A1, setDebug, ICONJS_VERSION };*/ //end esm