more crash preventions, support X/SharkPort files

Long version:

Putting an SPS|XPS into the PSU input would read as if the file was very long, so added a 127-file limit (I think this is reasonable) to prevent prolonged stalling.

Added SPS|XPS support to the library, which is the third most common contained format besides PSU and PSV.

Updated demos.

Technical changes:

removed unused u16le definition from readEntryBlock(), since permissions are better read as a u32 instead

added PSV and SPS|XPS reader functions to c6 exports, since I forgot in 0.3.4

moved some stuff around in HTML demo, especially the old raw icon.sys and icon input boxes, since I'm finding parsing a collection of files more useful then the actual standalone files themselves.

HTML-demo: added an "show advanced options" checkbox that unhides features deemed too niche for conventional use
This commit is contained in:
yellows111 2023-10-15 20:28:59 +01:00
parent 4d1b790a7c
commit a8f47aa7fb
5 changed files with 184 additions and 15 deletions

13
.gitignore vendored
View File

@ -1,4 +1,15 @@
# Extracted files
*.ico
*.icn
icon.sys
*.psu
# Save export files
*.psu
*.psv
*.xps
*.sps
# Unused data
tmp/*

View File

@ -3,8 +3,11 @@ A JavaScript library (sorta) to read PS2 icons, and their related formats.
## What it supports
* EMS Memory Adapter export files (.psu)
* PS3 virtual memory card export files (.psv)
* SharkPort export files (.sps)
* X-Port export files (.xps)
* PS2 icons (.ico, .icn)
* PS2D format (icon.sys)
* PS2D format (icon.sys)
## What can it do
* Allow any file in a PSU's virtual filesystem to be dumped.
@ -22,4 +25,4 @@ A JavaScript library (sorta) to read PS2 icons, and their related formats.
(todo: write this)
## Why "icondumper2"?
Because it replaced what *was* left of icondumper.
Because it replaced what *was* left of icondumper (1).

110
icon.js
View File

@ -1,7 +1,7 @@
//todo: Make this a module/mjs file. C6 compatibility can stay, if needed.
ICONJS_DEBUG = false;
ICONJS_STRICT = true;
ICONJS_VERSION = "0.3.4";
ICONJS_VERSION = "0.3.5";
function setDebug(value) {
ICONJS_DEBUG = !!value;
@ -65,7 +65,8 @@ function readPS2D(input) {
{r: f32le(160), g: f32le(164), b: f32le(168), a: f32le(172)},
{r: f32le(176), g: f32le(180), b: f32le(184), a: f32le(188)}
]
// save builder says color 1 is ambient, 2-4 are for 3-point cameras
// 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++) {
@ -214,7 +215,6 @@ function readIconFile(input) {
function readEntryBlock(input) {
const view = new DataView(input);
const u32le = function(i){return view.getUint32(i, 1)};
const u16le = function(i){return view.getUint16(i, 1)};
const t64le = function(i){return {
seconds: view.getUint8(i+1),
minutes: view.getUint8(i+2),
@ -254,6 +254,9 @@ function readEntryBlock(input) {
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: {created: header.createdTime, modified: header.modifiedTime}};
let output = new Object();
let offset = 512;
@ -346,7 +349,106 @@ function readPsvFile(input){
return {icons, "icon.sys": input.slice(ps2dOffset, ps2dOffset+ps2dSize), timestamps};
}
function readSxpsDescriptor(input) {
const view = new DataView(input);
const u32le = function(i){return view.getUint32(i, 1)};
const t64le = function(i){return {
seconds: view.getUint8(i+1),
minutes: view.getUint8(i+2),
hours: view.getUint8(i+3),
day: view.getUint8(i+4),
month: view.getUint8(i+5),
year: view.getUint16(i+6, 1)
}}; //NOTE: times are in JST timezone (GMT+09:00), so clients should implement correctly!
//!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.log({int_filename, size, startSector, endSector, permissions, type, timestamps, int_asciiName, int_shiftjisName});
}
return {type, size, filename, timestamps};
}
function readSharkXPortSxpsFile(input) {
const view = new DataView(input);
const u32le = function(i){return view.getUint32(i, 1)};
//!pattern sps-xps_file.hexpat
const identLength = u32le(0);
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 + 8);
const comments = {
"game": stringScrubber((new TextDecoder("utf-8")).decode(title)),
"name": stringScrubber((new TextDecoder("utf-8")).decode(description))
}
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.log(`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;
return fsOut;
}
if(typeof module !== "undefined") {
// for C6JS
module.exports = {readIconFile, readPS2D, readEmsPsuFile, setDebug, setStrictness};
module.exports = {readIconFile, readPS2D, readEmsPsuFile, readPsvFile, readSharkXPortSxpsFile, setDebug, setStrictness};
}

View File

@ -12,7 +12,6 @@ iconjs.setDebug(false);
// node.js client
if(processObj.argv[2] === "psu") {
let output = new Object();
let inputFile = filesystem.readFileSync(processObj.argv[3] ? processObj.argv[3] : "file.psu");
const parsed = iconjs.readEmsPsuFile(inputFile.buffer.slice(inputFile.byteOffset, inputFile.byteOffset + inputFile.byteLength));
const PS2D = iconjs.readPS2D(parsed[parsed.rootDirectory]["icon.sys"].data);
@ -28,6 +27,16 @@ if(processObj.argv[2] === "psu") {
const PS2D = iconjs.readPS2D(parsed["icon.sys"]);
let output = {parsed, PS2D};
console.log(output);
} else if(processObj.argv[2] === "sps") {
let inputFile = filesystem.readFileSync(processObj.argv[3] ? processObj.argv[3] : "file.sps");
const parsed = iconjs.readSharkXPortSxpsFile(inputFile.buffer.slice(inputFile.byteOffset, inputFile.byteOffset + inputFile.byteLength));
console.log(parsed);
const PS2D = iconjs.readPS2D(parsed[parsed.rootDirectory]["icon.sys"].data);
let output = {parsed, PS2D}
Object.keys(PS2D.filenames).forEach(function(file) {
output[file] = iconjs.readIconFile(parsed[parsed.rootDirectory][PS2D.filenames[file]].data);
});
console.log(output);
} else {
let inputFile = filesystem.readFileSync(processObj.argv[2] ? processObj.argv[2] : "icon.sys");
const metadata = iconjs.readPS2D(inputFile.buffer.slice(inputFile.byteOffset, inputFile.byteOffset + inputFile.byteLength));

View File

@ -17,6 +17,7 @@
margin: 0;
}
#version {position: fixed;bottom:4px;right:4px}
#advanced {display: none}
</style>
<script type="text/plain" id="verts1">
attribute vec4 a_position;
@ -41,10 +42,10 @@
<body>
<label for="strictnessOption">enable strict mode</label>
<input id="strictnessOption" type="checkbox" checked></input>
<span>(enables console-accurate title parsing)</span>
<span>(enables console-accurate title parsing) | </span>
<label for="showExtractedInputOption">show other import options</label>
<input id="showExtractedInputOption" type="checkbox"></input>
<hr>
<label for="input">icon.sys goes here:</label>
<input type="file" id="input" name="input" accept=".sys" />
<br>
<h1 id="title1">&#xFF2E;&#xFF4F;&#x3000;&#xFF26;&#xFF49;&#xFF4C;&#xFF45;</h1>
<h1 id="title2">&#xFF2C;&#xFF4F;&#xFF41;&#xFF44;&#xFF45;&#xFF44;</h1>
@ -54,10 +55,15 @@
<p>Normal: <kbd id="iconn">(no file)</kbd></p>
<p>Copying: <kbd id="iconc">(no file)</kbd></p>
<p>Deleting: <kbd id="icond">(no file)</kbd></p>
<hr>
<label for="icon">.ic(n|o) goes here:</label>
<input type="file" id="icon" name="icon" accept=".icn, .ico" />
<br>
<div id="advanced">
<hr>
<label for="input">icon.sys goes here:</label>
<input type="file" id="input" name="input" accept=".sys" />
<br>
<label for="icon">.ic(n|o) goes here:</label>
<input type="file" id="icon" name="icon" accept=".icn, .ico" />
<br>
</div>
<hr>
<label for="psuinput">EMS Memory Adapter export file (.psu) goes here:</label>
<input type="file" id="psuinput" name="psuinput" accept=".psu" />
@ -65,11 +71,17 @@
<label for="psvinput">PS3 export file (.psv) goes here:</label>
<input type="file" id="psvinput" name="psvinput" accept=".psv" />
<br>
<label for="spsinput">SharkPort/X-Port export file (.sps, .xps) goes here:</label>
<input type="file" id="spsinput" name="spsinput" accept=".sps, .xps" />
<br>
<p>
<span>Date created: </span><span id="dateCreated">--:--:-- --/--/----</span><span> UTC+09:00</span>
<br>
<span>Date modified: </span><span id="dateModified">--:--:-- --/--/----</span><span> UTC+09:00</span>
</p>
<p>
<span>File comments: </span><span id="fileCommentGame">(no title)</span></span><span> - </span><span id="fileCommentName">(no description)</span>
</p>
<!-- TODO MAKE NEW DISPLAY BOXES !-->
<script>
// i know this is sinful, but i don't want to load from an event again
@ -197,6 +209,35 @@
}
});
}
spsbox = document.getElementById("spsinput");
spsbox.onchange = function(e) {
resetDisplay();
if(spsbox.files.length === 0) {
return;
}
spsbox.files[0].arrayBuffer().then(function(d){
try {
let vFilesystem = readSharkXPortSxpsFile(d);
let output = readPS2D(vFilesystem[vFilesystem.rootDirectory]["icon.sys"].data);
updateDisplay(output);
let output2 = new Object();
Object.keys(output.filenames).forEach(function(file) {
output2[file] = readIconFile(vFilesystem[vFilesystem.rootDirectory][output.filenames[file]].data);
});
let cTime = vFilesystem.timestamps.created;
let mTime = vFilesystem.timestamps.modified;
//TODO: use Time() to align JST times to user-local timezone
document.getElementById("dateCreated").textContent = `${cTime.hours.toString().padStart("2","0")}:${cTime.minutes.toString().padStart("2","0")}:${cTime.seconds.toString().padStart("2","0")} ${cTime.day.toString().padStart("2","0")}/${cTime.month.toString().padStart("2","0")}/${cTime.year}`;
document.getElementById("dateModified").textContent = `${mTime.hours.toString().padStart("2","0")}:${mTime.minutes.toString().padStart("2","0")}:${mTime.seconds.toString().padStart("2","0")} ${mTime.day.toString().padStart("2","0")}/${mTime.month.toString().padStart("2","0")}/${mTime.year}`;
document.getElementById("fileCommentGame").textContent = vFilesystem.comments.game;
document.getElementById("fileCommentName").textContent = vFilesystem.comments.name;
console.log("model files", output2);
} catch(e) {
if(glContext!==null){glContext.clear(glContext.COLOR_BUFFER_BIT);}
alert(e);
}
});
}
function createShader(gl, type, source) {
var shader = gl.createShader(type);
gl.shaderSource(shader, source);
@ -256,6 +297,9 @@
} else {
canvas.style.display = "none";
}
document.getElementById("showExtractedInputOption").onchange = function(e) {
document.getElementById("advanced").style.display = ((e.target.checked) ? "block" : "none");
}
//todo: Model rendering, animation parsing, animation tweening
</script>
<span id="version">icondumper2 <span id="iconjsVersion">(unknown icon.js version)</span> - &copy; 2023 yellows111</span>