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:
parent
4d1b790a7c
commit
a8f47aa7fb
|
@ -1,4 +1,15 @@
|
||||||
|
# Extracted files
|
||||||
|
|
||||||
*.ico
|
*.ico
|
||||||
*.icn
|
*.icn
|
||||||
icon.sys
|
icon.sys
|
||||||
|
# Save export files
|
||||||
|
|
||||||
*.psu
|
*.psu
|
||||||
|
*.psv
|
||||||
|
*.xps
|
||||||
|
*.sps
|
||||||
|
|
||||||
|
# Unused data
|
||||||
|
|
||||||
|
tmp/*
|
|
@ -3,6 +3,9 @@ A JavaScript library (sorta) to read PS2 icons, and their related formats.
|
||||||
|
|
||||||
## What it supports
|
## What it supports
|
||||||
* EMS Memory Adapter export files (.psu)
|
* 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)
|
* PS2 icons (.ico, .icn)
|
||||||
* PS2D format (icon.sys)
|
* PS2D format (icon.sys)
|
||||||
|
|
||||||
|
@ -22,4 +25,4 @@ A JavaScript library (sorta) to read PS2 icons, and their related formats.
|
||||||
(todo: write this)
|
(todo: write this)
|
||||||
|
|
||||||
## Why "icondumper2"?
|
## Why "icondumper2"?
|
||||||
Because it replaced what *was* left of icondumper.
|
Because it replaced what *was* left of icondumper (1).
|
||||||
|
|
110
icon.js
110
icon.js
|
@ -1,7 +1,7 @@
|
||||||
//todo: Make this a module/mjs file. C6 compatibility can stay, if needed.
|
//todo: Make this a module/mjs file. C6 compatibility can stay, if needed.
|
||||||
ICONJS_DEBUG = false;
|
ICONJS_DEBUG = false;
|
||||||
ICONJS_STRICT = true;
|
ICONJS_STRICT = true;
|
||||||
ICONJS_VERSION = "0.3.4";
|
ICONJS_VERSION = "0.3.5";
|
||||||
|
|
||||||
function setDebug(value) {
|
function setDebug(value) {
|
||||||
ICONJS_DEBUG = !!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(160), g: f32le(164), b: f32le(168), a: f32le(172)},
|
||||||
{r: f32le(176), g: f32le(180), b: f32le(184), a: f32le(188)}
|
{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 int_title = input.slice(0xc0, 0x100);
|
||||||
const tmp_title16 = new Uint16Array(int_title);
|
const tmp_title16 = new Uint16Array(int_title);
|
||||||
for (let index = 0; index < 32; index++) {
|
for (let index = 0; index < 32; index++) {
|
||||||
|
@ -214,7 +215,6 @@ function readIconFile(input) {
|
||||||
function readEntryBlock(input) {
|
function readEntryBlock(input) {
|
||||||
const view = new DataView(input);
|
const view = new DataView(input);
|
||||||
const u32le = function(i){return view.getUint32(i, 1)};
|
const u32le = function(i){return view.getUint32(i, 1)};
|
||||||
const u16le = function(i){return view.getUint16(i, 1)};
|
|
||||||
const t64le = function(i){return {
|
const t64le = function(i){return {
|
||||||
seconds: view.getUint8(i+1),
|
seconds: view.getUint8(i+1),
|
||||||
minutes: view.getUint8(i+2),
|
minutes: view.getUint8(i+2),
|
||||||
|
@ -254,6 +254,9 @@ function readEntryBlock(input) {
|
||||||
|
|
||||||
function readEmsPsuFile(input){
|
function readEmsPsuFile(input){
|
||||||
const header = readEntryBlock(input.slice(0,0x1ff));
|
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 fsOut = {length: header.size, rootDirectory: header.filename, timestamps: {created: header.createdTime, modified: header.modifiedTime}};
|
||||||
let output = new Object();
|
let output = new Object();
|
||||||
let offset = 512;
|
let offset = 512;
|
||||||
|
@ -346,7 +349,106 @@ function readPsvFile(input){
|
||||||
return {icons, "icon.sys": input.slice(ps2dOffset, ps2dOffset+ps2dSize), timestamps};
|
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") {
|
if(typeof module !== "undefined") {
|
||||||
// for C6JS
|
// for C6JS
|
||||||
module.exports = {readIconFile, readPS2D, readEmsPsuFile, setDebug, setStrictness};
|
module.exports = {readIconFile, readPS2D, readEmsPsuFile, readPsvFile, readSharkXPortSxpsFile, setDebug, setStrictness};
|
||||||
}
|
}
|
11
index.js
11
index.js
|
@ -12,7 +12,6 @@ iconjs.setDebug(false);
|
||||||
|
|
||||||
// node.js client
|
// node.js client
|
||||||
if(processObj.argv[2] === "psu") {
|
if(processObj.argv[2] === "psu") {
|
||||||
let output = new Object();
|
|
||||||
let inputFile = filesystem.readFileSync(processObj.argv[3] ? processObj.argv[3] : "file.psu");
|
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 parsed = iconjs.readEmsPsuFile(inputFile.buffer.slice(inputFile.byteOffset, inputFile.byteOffset + inputFile.byteLength));
|
||||||
const PS2D = iconjs.readPS2D(parsed[parsed.rootDirectory]["icon.sys"].data);
|
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"]);
|
const PS2D = iconjs.readPS2D(parsed["icon.sys"]);
|
||||||
let output = {parsed, PS2D};
|
let output = {parsed, PS2D};
|
||||||
console.log(output);
|
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 {
|
} else {
|
||||||
let inputFile = filesystem.readFileSync(processObj.argv[2] ? processObj.argv[2] : "icon.sys");
|
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));
|
const metadata = iconjs.readPS2D(inputFile.buffer.slice(inputFile.byteOffset, inputFile.byteOffset + inputFile.byteLength));
|
||||||
|
|
58
input.htm
58
input.htm
|
@ -17,6 +17,7 @@
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
#version {position: fixed;bottom:4px;right:4px}
|
#version {position: fixed;bottom:4px;right:4px}
|
||||||
|
#advanced {display: none}
|
||||||
</style>
|
</style>
|
||||||
<script type="text/plain" id="verts1">
|
<script type="text/plain" id="verts1">
|
||||||
attribute vec4 a_position;
|
attribute vec4 a_position;
|
||||||
|
@ -41,10 +42,10 @@
|
||||||
<body>
|
<body>
|
||||||
<label for="strictnessOption">enable strict mode</label>
|
<label for="strictnessOption">enable strict mode</label>
|
||||||
<input id="strictnessOption" type="checkbox" checked></input>
|
<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>
|
<hr>
|
||||||
<label for="input">icon.sys goes here:</label>
|
|
||||||
<input type="file" id="input" name="input" accept=".sys" />
|
|
||||||
<br>
|
<br>
|
||||||
<h1 id="title1">No File</h1>
|
<h1 id="title1">No File</h1>
|
||||||
<h1 id="title2">Loaded</h1>
|
<h1 id="title2">Loaded</h1>
|
||||||
|
@ -54,10 +55,15 @@
|
||||||
<p>Normal: <kbd id="iconn">(no file)</kbd></p>
|
<p>Normal: <kbd id="iconn">(no file)</kbd></p>
|
||||||
<p>Copying: <kbd id="iconc">(no file)</kbd></p>
|
<p>Copying: <kbd id="iconc">(no file)</kbd></p>
|
||||||
<p>Deleting: <kbd id="icond">(no file)</kbd></p>
|
<p>Deleting: <kbd id="icond">(no file)</kbd></p>
|
||||||
<hr>
|
<div id="advanced">
|
||||||
<label for="icon">.ic(n|o) goes here:</label>
|
<hr>
|
||||||
<input type="file" id="icon" name="icon" accept=".icn, .ico" />
|
<label for="input">icon.sys goes here:</label>
|
||||||
<br>
|
<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>
|
<hr>
|
||||||
<label for="psuinput">EMS Memory Adapter export file (.psu) goes here:</label>
|
<label for="psuinput">EMS Memory Adapter export file (.psu) goes here:</label>
|
||||||
<input type="file" id="psuinput" name="psuinput" accept=".psu" />
|
<input type="file" id="psuinput" name="psuinput" accept=".psu" />
|
||||||
|
@ -65,11 +71,17 @@
|
||||||
<label for="psvinput">PS3 export file (.psv) goes here:</label>
|
<label for="psvinput">PS3 export file (.psv) goes here:</label>
|
||||||
<input type="file" id="psvinput" name="psvinput" accept=".psv" />
|
<input type="file" id="psvinput" name="psvinput" accept=".psv" />
|
||||||
<br>
|
<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>
|
<p>
|
||||||
<span>Date created: </span><span id="dateCreated">--:--:-- --/--/----</span><span> UTC+09:00</span>
|
<span>Date created: </span><span id="dateCreated">--:--:-- --/--/----</span><span> UTC+09:00</span>
|
||||||
<br>
|
<br>
|
||||||
<span>Date modified: </span><span id="dateModified">--:--:-- --/--/----</span><span> UTC+09:00</span>
|
<span>Date modified: </span><span id="dateModified">--:--:-- --/--/----</span><span> UTC+09:00</span>
|
||||||
</p>
|
</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 !-->
|
<!-- TODO MAKE NEW DISPLAY BOXES !-->
|
||||||
<script>
|
<script>
|
||||||
// i know this is sinful, but i don't want to load from an event again
|
// 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) {
|
function createShader(gl, type, source) {
|
||||||
var shader = gl.createShader(type);
|
var shader = gl.createShader(type);
|
||||||
gl.shaderSource(shader, source);
|
gl.shaderSource(shader, source);
|
||||||
|
@ -256,6 +297,9 @@
|
||||||
} else {
|
} else {
|
||||||
canvas.style.display = "none";
|
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
|
//todo: Model rendering, animation parsing, animation tweening
|
||||||
</script>
|
</script>
|
||||||
<span id="version">icondumper2 <span id="iconjsVersion">(unknown icon.js version)</span> - © 2023 yellows111</span>
|
<span id="version">icondumper2 <span id="iconjsVersion">(unknown icon.js version)</span> - © 2023 yellows111</span>
|
||||||
|
|
Loading…
Reference in New Issue