diff --git a/README.md b/README.md
index b495843..ada4456 100644
--- a/README.md
+++ b/README.md
@@ -28,7 +28,7 @@ Continue with [learning about the API](#api).
### Web
- Load HSSP for JavaScript with:
```html
-
+
```
- Create an editor:
```js
@@ -62,6 +62,7 @@ editor.addFile('my-folder/test.txt', fs.readFileSync('test2.txt'));
editor.addFile('my-folder/test.txt', (new TextEncoder()).encode('Hello, world! 2').buffer);
```
- Delete a file:
+
**Note:** _This method will return the file Buffer/ArrayBuffer._
```js
editor.remove('test.txt');
@@ -70,6 +71,7 @@ editor.remove('test.txt');
```js
editor.remove('my-folder');
```
+
**Note:** _This will only remove the folder, not the files in it! If you want to remove the folder with the files in it, use:_
```js
var folderName = 'my-folder';
@@ -85,7 +87,7 @@ editor.remove(folderName); // Remove the folder itself
- Set output file version:
```js
-editor.version = 4; // 4 is set by default, 1-4 are valid version numbers
+editor.version = 5; // 5 is set by default, 1-5 are valid version numbers
```
- Enable output encryption:
```js
@@ -96,6 +98,7 @@ editor.password = 'MySecretPassword'; // write-only
editor.password = null; // Encryption is disabled by default
```
- Enable output compression ([Supported algorithms](#supported-compression-algorithms)):
+
**Note:** _Requires editor.version is 4 or higher._
```js
editor.compression = { algorithm: 'LZMA', level: 9 }; // Level default is 5
@@ -105,6 +108,7 @@ editor.compression = { algorithm: 'LZMA', level: 9 }; // Level default is 5
editor.compression = null; // default
```
- Add a comment:
+
**Note:** _Requires editor.version is 4 or higher. The comment can be up to 16 characters (UTF-8) long._
```js
editor.comment = 'Hello :)';
@@ -112,7 +116,7 @@ editor.comment = 'Hello :)';
#### Importing HSSP files
-Currently supports HSSP 1-4.
+Currently supports HSSP 1-5.
- Importing HSSP files _without_ encryption:
```js
@@ -145,7 +149,7 @@ document.querySelector('input[type=file]').onchange = async (ev) => {
#### Creating HSSP files
-Currently supports HSSP 1-4.
+Currently supports HSSP 1-5.
- Creating _one_ file:
```js
@@ -162,6 +166,7 @@ a.click();
URL.revokeObjectURL(url);
```
- Creating _multiple_ files:
+
**Note:** _This method can only be used if `editor.version` is 4 or higher. You also cannot create more files than bytes included._
```js
// Node
@@ -185,7 +190,7 @@ editor.toBuffers(4).forEach((buf, i) => {
#### Fetching metadata from HSSP file
-Currently supports HSSP 1-4.
+Currently supports HSSP 1-5.
Fetching metadata is as simple as that:
```js
@@ -236,7 +241,7 @@ editor.addFolder(name, options);
Feel free to contribute by [opening an issue](https://github.com/HSSPfile/js/issues/new/choose) and requesting new features, reporting bugs or just asking questions.
-You can also [fork the repository](https://github.com/HSSPfile/js/fork) and opening a [pull request](https://github.com/HSSPfile/js/pulls) after making some changes like fixing bugs.
+You can also [fork the repository](https://github.com/HSSPfile/js/fork) and open a [pull request](https://github.com/HSSPfile/js/pulls) after making some changes like fixing bugs.
## [License](LICENSE)
diff --git a/main.js b/main.js
index 998df40..de39d4c 100644
--- a/main.js
+++ b/main.js
@@ -1175,7 +1175,7 @@ class Editor { // Can hold massive amounts of data in a single file
var fileStart = Buffer.alloc(size);
fileStart.write('HSSP', 0, 'utf8'); // Magic value :) | 4+0
fileStart.writeUint8(4, 4); // File standard version, see https://hssp.leox.dev/docs/versions | 1+4
- out.writeUint8(parseInt([
+ fileStart.writeUint8(parseInt([
this.#pwd !== null, // F1: is encrypted
this.#compAlgo !== 'NONE', // F2: is compressed
true, // F3: is split
@@ -1185,7 +1185,7 @@ class Editor { // Can hold massive amounts of data in a single file
false, // F7: unallocated
false // F8: unallocated
].map(b => +b).join(''), 2), 5); // Flags #1, see https://hssp.leox.dev/docs/flags | 1+5
- out.writeUint8(parseInt([
+ fileStart.writeUint8(parseInt([
false, // F9: unallocated
false, // F10: unallocated
false, // F11: unallocated
@@ -1195,7 +1195,7 @@ class Editor { // Can hold massive amounts of data in a single file
false, // F15: unallocated
false // F16: unallocated
].map(b => +b).join(''), 2), 6); // Flags #2, see https://hssp.leox.dev/docs/flags | 1+6
- out.writeUint8(parseInt([
+ fileStart.writeUint8(parseInt([
false, // F17: unallocated
false, // F18: unallocated
false, // F19: unallocated
@@ -1660,7 +1660,7 @@ module.exports = {
return metadata;
case 5: // v5: Uses flags
- metadata.version = 4;
+ metadata.version = 5;
const inp = buffer.subarray(128, buffer.byteLength);
metadata.hash.valid = true;
const hash = murmurhash.murmur3(inp.toString('utf8'), 0x31082007);
diff --git a/package-lock.json b/package-lock.json
index 05ea57f..3cec934 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "hssp",
- "version": "3.0.0",
+ "version": "4.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "hssp",
- "version": "3.0.0",
+ "version": "4.0.0",
"license": "MIT",
"dependencies": {
"lzma": "^2.3.2",
diff --git a/package.json b/package.json
index 7a4fd08..8082633 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "hssp",
- "version": "3.0.0",
+ "version": "4.0.0",
"description": "Create, edit and read HSSP files in pure JavaScript",
"main": "main.js",
"scripts": {
@@ -11,8 +11,7 @@
"HSSP",
"HugeSizeSupportingPackage",
"Huge",
- "Package",
- "64RiB"
+ "Package"
],
"author": "HSSP",
"license": "MIT",
diff --git a/web.js b/web.js
index 7a8ecef..e386843 100644
--- a/web.js
+++ b/web.js
@@ -181,7 +181,7 @@ const HSSP = {
* @since 3.0.0/v4
*/
set version(int) {
- this.#ver = +int < 5 && 0 < +int ? +int : this.#ver;
+ this.#ver = +int < 6 && 0 < +int ? +int : this.#ver;
}
/**
@@ -1015,7 +1015,163 @@ const HSSP = {
out.set(new TextEncoder().encode(this.#compAlgo), 60); // Used compression algorithm, 0 if not set | 4+60
// this file is not split | 28+68
out.set(new TextEncoder().encode(this.#comment.slice(0, 16)), 96); // Comment | 16+96
- out.set(new TextEncoder().encode('hssp 3.0.0 WEB'), 112); // Used generator | 16+112
+ out.set(new TextEncoder().encode('hssp 4.0.0 WEB'), 112); // Used generator | 16+112
+
+ var offs = 128; // Start
+
+ // index
+ this.#files.forEach(file => {
+ var innerOffs = 0;
+ outDV.setBigUint64(offs, BigInt(file[1].byteLength), true);
+ offs += innerOffs + 8;
+
+ innerOffs = (new TextEncoder().encode(file[0])).byteLength;
+ outDV.setUint16(offs, innerOffs, true);
+ out.set(new TextEncoder().encode(file[0]), offs + 2);
+ offs += innerOffs + 2;
+
+ innerOffs = (new TextEncoder().encode(file[2].owner)).byteLength;
+ outDV.setUint16(offs, innerOffs, true);
+ out.set(new TextEncoder().encode(file[2].owner), offs + 2);
+ offs += innerOffs + 2;
+
+ innerOffs = (new TextEncoder().encode(file[2].group)).byteLength;
+ outDV.setUint16(offs, innerOffs, true);
+ out.set(new TextEncoder().encode(file[2].group), offs + 2);
+ offs += innerOffs + 2;
+
+ innerOffs = (new TextEncoder().encode(file[2].webLink)).byteLength;
+ outDV.setUint32(offs, innerOffs, true);
+ out.set(new TextEncoder().encode(file[2].webLink), offs + 4);
+ offs += innerOffs + 4;
+
+ var u48c = new Uint8Array(8);
+ var u48cDV = new DataView(u48c.buffer);
+ u48cDV.setBigUint64(0, BigInt(file[2].created.getTime()), true);
+ out.set(u48c.slice(0, 6), offs);
+ u48cDV.setBigUint64(0, BigInt(file[2].changed.getTime()), true);
+ out.set(u48c.slice(0, 6), offs + 6);
+ u48cDV.setBigUint64(0, BigInt(file[2].opened.getTime()), true);
+ out.set(u48c.slice(0, 6), offs + 12);
+ offs += 18;
+
+ parseInt(file[2].permissions, 8).toString(2).split('').map(x => parseInt(x)).forEach((bit, addr) => {
+ out[offs + Math.floor(addr / 8)] |= (bit << addr);
+ });
+ out[offs + 1] |= (+!!file[2].isFolder << 1);
+ out[offs + 1] |= (+!!file[2].hidden << 2);
+ out[offs + 1] |= (+!!file[2].system << 3);
+ out[offs + 1] |= (+!!file[2].enableBackup << 4);
+ out[offs + 1] |= (+!!file[2].forceBackup << 5);
+ out[offs + 1] |= (+!!file[2].readOnly << 6);
+ out[offs + 1] |= (+!!file[2].mainFile << 7);
+ offs += 2;
+ });
+
+ // files
+ this.#files.forEach(file => {
+ out.set(new Uint8Array(file[1].buffer), offs); // file
+ offs += file[1].byteLength;
+ });
+ var outBuf = out;
+ var pack = outBuf.subarray(128, size);
+
+ switch (this.#compAlgo) {
+ case 'DFLT':
+ outBuf = HSSP._internal.mergeUint8Arrays(outBuf.subarray(0, 128), pako.deflate(pack, { level: this.#compLvl }));
+ break;
+
+ case 'LZMA':
+ outBuf = HSSP._internal.mergeUint8Arrays(outBuf.subarray(0, 128), LZMA.compress(pack, this.#compLvl));
+ break;
+
+ case 'NONE':
+ break;
+
+ default:
+ throw new Error('COMPRESSION_NOT_SUPPORTED');
+ };
+
+ size = outBuf.byteLength;
+ pack = outBuf.slice(128, size);
+ var outBufDV = new DataView(outBuf.buffer);
+ if (this.#pwd !== null) {
+ const iv = CryptoJS.lib.WordArray.random(16);
+ const encrypted = CryptoJS.AES.encrypt(CryptoJS.lib.WordArray.create(pack), CryptoJS.SHA256(this.#pwd), {
+ iv,
+ padding: CryptoJS.pad.Pkcs7,
+ mode: CryptoJS.mode.CBC
+ }).ciphertext.toUint8Array();
+ outBuf.set(iv.toUint8Array(), 44);
+ outBuf.set(CryptoJS.SHA256(CryptoJS.SHA256(this.#pwd)).toUint8Array(), 12);
+ const eOut = new Uint8Array(128 + encrypted.byteLength);
+ const eOutDV = new DataView(eOut.buffer);
+ eOut.set(outBuf.slice(0, 128), 0);
+ eOut.set(encrypted, 128);
+ eOutDV.setUint32(64, murmurhash3_32_gc(new TextDecoder().decode(encrypted), 0x31082007), true);
+ return eOut;
+ };
+ outBufDV.setUint32(64, murmurhash3_32_gc(new TextDecoder().decode(pack), 0x31082007), true); // checksum
+ return outBuf.buffer;
+ case 5:
+ var size = 128; // Bytes
+ this.#files.forEach(file => size +=
+ 38 + // various constants
+
+ (new TextEncoder().encode(file[0])).byteLength + // File name length
+ (new TextEncoder().encode(file[2].owner)).byteLength + // Owner name length
+ (new TextEncoder().encode(file[2].group)).byteLength + // Group name length
+ (new TextEncoder().encode(file[2].webLink)).byteLength + // Web link length
+
+ file[1].byteLength
+ );
+ var out = new Uint8Array(size);
+ var outDV = new DataView(out.buffer);
+ out.set(new TextEncoder().encode('HSSP'), 0); // Magic value :) | 4+0
+ outDV.setUint8(4, 5); // File standard version, see https://hssp.leox.dev/docs/versions | 1+4
+ outDV.setUint8(5, parseInt([
+ this.#pwd !== null, // F1: is encrypted
+ this.#compAlgo !== 'NONE', // F2: is compressed
+ false, // F3: is split
+ false, // F4: unallocated
+ false, // F5: unallocated
+ false, // F6: unallocated
+ false, // F7: unallocated
+ false // F8: unallocated
+ ].map(b => +b).join(''), 2)); // Flags #1, see https://hssp.leox.dev/docs/flags | 1+5
+ outDV.setUint8(6, parseInt([
+ false, // F9: unallocated
+ false, // F10: unallocated
+ false, // F11: unallocated
+ false, // F12: unallocated
+ false, // F13: unallocated
+ false, // F14: unallocated
+ false, // F15: unallocated
+ false // F16: unallocated
+ ].map(b => +b).join(''), 2)); // Flags #2, see https://hssp.leox.dev/docs/flags | 1+6
+ outDV.setUint8(7, parseInt([
+ false, // F17: unallocated
+ false, // F18: unallocated
+ false, // F19: unallocated
+ false, // F20: unallocated
+ false, // F21: unallocated
+ false, // F22: unallocated
+ false, // F23: unallocated
+ false // F24: unallocated
+ ].map(b => +b).join(''), 2)); // Flags #3, see https://hssp.leox.dev/docs/flags | 1+7
+ outDV.setUint32(8, this.#files.length, true); // File count | 4+8
+ for (var i = 3; i < 11; i++) {
+ outDV.setUint32(i * 4, 0, true); // Password hash, if not set = 0 | 32+12
+ // 12 - 44
+ };
+ for (var i = 0; i < 4; i++) {
+ outDV.setUint32(i * 4 + 44, 0, true); // Encryption initialization vector (iv), if not set = 0 | 16+44
+ // 44 - 60
+ };
+ out.set(new TextEncoder().encode(this.#compAlgo), 60); // Used compression algorithm, 0 if not set | 4+60
+ // this file is not split | 28+68
+ out.set(new TextEncoder().encode(this.#comment.slice(0, 16)), 96); // Comment | 16+96
+ out.set(new TextEncoder().encode('hssp 4.0.0 WEB'), 112); // Used generator | 16+112
var offs = 128; // Start
@@ -1132,7 +1288,7 @@ const HSSP = {
*/
toBuffers(count) {
if (typeof CryptoJS != 'object' && typeof murmurhash3_32_gc != 'function') throw new Error('MISSING_DEPENDENCIES');
- if (this.#ver !== 4) throw new Error('VERSION_NOT_SUPPORTED');
+ if (this.#ver !== 4 || this.#ver !== 5) throw new Error('VERSION_NOT_SUPPORTED');
if (count < 1) throw new Error('DUDE_YOU_CANNOT_SPLIT_A_FILE_INTO_LESS_THAN_ONE_PART');
if (this.#files.length < 1) throw new Error('DUDE_YOU_CANNOT_SPLIT_SOMETHING_THAT_IS_NOT_THERE');
if ((() => {
@@ -1169,138 +1325,311 @@ const HSSP = {
filesInBuffer.push([this.#files[j][0], offsets[j] - (globalOffs - out[i].byteLength), lengths[j], j]);
};
- var size = 128; // Bytes
- filesInBuffer.forEach(file => size +=
- 38 + // various constants
-
- (new TextEncoder().encode(this.#files[file[3]][0])).byteLength + // File name length
- (new TextEncoder().encode(this.#files[file[3]][2].owner)).byteLength + // Owner name length
- (new TextEncoder().encode(this.#files[file[3]][2].group)).byteLength + // Group name length
- (new TextEncoder().encode(this.#files[file[3]][2].webLink)).byteLength // Web link length
- );
-
- var fileStart = new Uint8Array(size);
- var fileStartDV = new DataView(fileStart.buffer);
- fileStart.set(new TextEncoder().encode('HSSP'), 0); // Magic value :) | 4+0
- fileStartDV.setUint8(4, 4); // File standard version, see https://hssp.leox.dev/docs/versions | 1+4
- // these 3 bytes are reserved for future use | 3+5
- fileStartDV.setUint32(8, filesInBuffer.length, true); // File count | 4+8
- for (var j = 3; j < 11; j++) {
- fileStartDV.setUint32(j * 4, 0, true); // Password hash, if not set = 0 | 32+12
- // 12 - 44
- };
- for (var j = 0; j < 4; j++) {
- fileStartDV.setUint32(j * 4 + 44, 0, true); // Encryption initialization vector (iv), if not set = 0 | 16+44
- // 44 - 60
- };
- fileStart.set(new TextEncoder().encode(this.#compAlgo), 60); // Used compression algorithm, 0 if not set | 4+60
+ switch (this.#ver) {
+ case 4:
+ var size = 128; // Bytes
+ filesInBuffer.forEach(file => size +=
+ 38 + // various constants
+
+ (new TextEncoder().encode(this.#files[file[3]][0])).byteLength + // File name length
+ (new TextEncoder().encode(this.#files[file[3]][2].owner)).byteLength + // Owner name length
+ (new TextEncoder().encode(this.#files[file[3]][2].group)).byteLength + // Group name length
+ (new TextEncoder().encode(this.#files[file[3]][2].webLink)).byteLength // Web link length
+ );
+
+ var fileStart = new Uint8Array(size);
+ var fileStartDV = new DataView(fileStart.buffer);
+ fileStart.set(new TextEncoder().encode('HSSP'), 0); // Magic value :) | 4+0
+ fileStartDV.setUint8(4, 4); // File standard version, see https://hssp.leox.dev/docs/versions | 1+4
+ // these 3 bytes are reserved for future use | 3+5
+ fileStartDV.setUint32(8, filesInBuffer.length, true); // File count | 4+8
+ for (var j = 3; j < 11; j++) {
+ fileStartDV.setUint32(j * 4, 0, true); // Password hash, if not set = 0 | 32+12
+ // 12 - 44
+ };
+ for (var j = 0; j < 4; j++) {
+ fileStartDV.setUint32(j * 4 + 44, 0, true); // Encryption initialization vector (iv), if not set = 0 | 16+44
+ // 44 - 60
+ };
+ fileStart.set(new TextEncoder().encode(this.#compAlgo), 60); // Used compression algorithm, 0 if not set | 4+60
+
+ fileStartDV.setBigUint64(68, BigInt(this.#files.length), true); // total file count | 8+68
+ fileStartDV.setBigUint64(76, BigInt(filesInBuffer[0][1] <= 0 ? Math.abs(filesInBuffer[0][1]) : 0), true); // split file offset | 8+76
+ if (out[i - 1]) fileStartDV.setUint32(84, murmurhash3_32_gc(new TextDecoder().decode(out[i - 1].subarray(128, out[i - 1].byteLength)), 0x31082007), true); // Checksum of previous package | 4+84
+ fileStartDV.setUint32(88, 0, true); // Checksum of next package | 4+88
+ fileStartDV.setUint32(92, i, true); // File ID of this package | 4+92
+
+ fileStart.set(new TextEncoder().encode(this.#comment.slice(0, 16)), 96); // Comment | 16+96
+ fileStart.set(new TextEncoder().encode('hssp 4.0.0 WEB'), 112); // Used generator | 16+112
+
+ var offs = 128; // Start
+
+ // index
+ filesInBuffer.forEach(file => {
+ file = this.#files[file[3]];
+
+ var innerOffs = 0;
+ fileStartDV.setBigUint64(offs, BigInt(file[1].byteLength), true);
+ offs += innerOffs + 8;
+
+ innerOffs = (new TextEncoder().encode(file[0])).byteLength;
+ fileStartDV.setUint16(offs, innerOffs, true);
+ fileStart.set(new TextEncoder().encode(file[0]), offs + 2);
+ offs += innerOffs + 2;
+
+ innerOffs = (new TextEncoder().encode(file[2].owner)).byteLength;
+ fileStartDV.setUint16(offs, innerOffs, true);
+ fileStart.set(new TextEncoder().encode(file[2].owner), offs + 2);
+ offs += innerOffs + 2;
+
+ innerOffs = (new TextEncoder().encode(file[2].group)).byteLength;
+ fileStartDV.setUint16(offs, innerOffs, true);
+ fileStart.set(new TextEncoder().encode(file[2].group), offs + 2);
+ offs += innerOffs + 2;
+
+ innerOffs = (new TextEncoder().encode(file[2].webLink)).byteLength;
+ fileStartDV.setUint32(offs, innerOffs, true);
+ fileStart.set(new TextEncoder().encode(file[2].webLink), offs + 4);
+ offs += innerOffs + 4;
+
+ var u48c = new Uint8Array(8);
+ var u48cDV = new DataView(u48c.buffer);
+ u48cDV.setBigUint64(0, BigInt(file[2].created.getTime()), true);
+ fileStart.set(u48c.slice(0, 6), offs);
+ u48cDV.setBigUint64(0, BigInt(file[2].changed.getTime()), true);
+ fileStart.set(u48c.slice(0, 6), offs + 6);
+ u48cDV.setBigUint64(0, BigInt(file[2].opened.getTime()), true);
+ fileStart.set(u48c.slice(0, 6), offs + 12);
+ offs += 18;
+
+ parseInt(file[2].permissions, 8).toString(2).split('').map(x => parseInt(x)).forEach((bit, addr) => {
+ fileStart[offs + Math.floor(addr / 8)] |= (bit << addr);
+ });
+ fileStart[offs + 1] |= (+!!file[2].isFolder << 1);
+ fileStart[offs + 1] |= (+!!file[2].hidden << 2);
+ fileStart[offs + 1] |= (+!!file[2].system << 3);
+ fileStart[offs + 1] |= (+!!file[2].enableBackup << 4);
+ fileStart[offs + 1] |= (+!!file[2].forceBackup << 5);
+ fileStart[offs + 1] |= (+!!file[2].readOnly << 6);
+ fileStart[offs + 1] |= (+!!file[2].mainFile << 7);
+ offs += 2;
+ });
- fileStartDV.setBigUint64(68, BigInt(this.#files.length), true); // total file count | 8+68
- fileStartDV.setBigUint64(76, BigInt(filesInBuffer[0][1] <= 0 ? Math.abs(filesInBuffer[0][1]) : 0), true); // split file offset | 8+76
- if (out[i - 1]) fileStartDV.setUint32(84, murmurhash3_32_gc(new TextDecoder().decode(out[i - 1].subarray(128, out[i - 1].byteLength)), 0x31082007), true); // Checksum of previous package | 4+84
- fileStartDV.setUint32(88, 0, true); // Checksum of next package | 4+88
- fileStartDV.setUint32(92, i, true); // File ID of this package | 4+92
+ var oldOut = out[i];
+ out[i] = new Uint8Array(fileStart.byteLength + oldOut.byteLength);
+ out[i].set(fileStart, 0);
+ out[i].set(oldOut, fileStart.byteLength);
+ var outBuf = out[i];
+ var pack = outBuf.subarray(128, outBuf.byteLength);
- fileStart.set(new TextEncoder().encode(this.#comment.slice(0, 16)), 96); // Comment | 16+96
- fileStart.set(new TextEncoder().encode('hssp 3.0.0 WEB'), 112); // Used generator | 16+112
+ switch (this.#compAlgo) {
+ case 'DFLT':
+ outBuf = HSSP._internal.mergeUint8Arrays(outBuf.subarray(0, 128), pako.deflate(pack, { level: this.#compLvl }));
+ break;
- var offs = 128; // Start
+ case 'LZMA':
+ outBuf = HSSP._internal.mergeUint8Arrays(outBuf.subarray(0, 128), LZMA.compress(pack, this.#compLvl));
+ break;
- // index
- filesInBuffer.forEach(file => {
- file = this.#files[file[3]];
+ case 'NONE':
+ break;
- var innerOffs = 0;
- fileStartDV.setBigUint64(offs, BigInt(file[1].byteLength), true);
- offs += innerOffs + 8;
-
- innerOffs = (new TextEncoder().encode(file[0])).byteLength;
- fileStartDV.setUint16(offs, innerOffs, true);
- fileStart.set(new TextEncoder().encode(file[0]), offs + 2);
- offs += innerOffs + 2;
+ default:
+ throw new Error('COMPRESSION_NOT_SUPPORTED');
+ };
- innerOffs = (new TextEncoder().encode(file[2].owner)).byteLength;
- fileStartDV.setUint16(offs, innerOffs, true);
- fileStart.set(new TextEncoder().encode(file[2].owner), offs + 2);
- offs += innerOffs + 2;
+ size = outBuf.byteLength;
+ pack = outBuf.slice(128, size);
+ var outBufDV = new DataView(outBuf.buffer);
+ if (this.#pwd !== null) {
+ const iv = CryptoJS.lib.WordArray.random(16);
+ const encrypted = CryptoJS.AES.encrypt(CryptoJS.lib.WordArray.create(pack), CryptoJS.SHA256(this.#pwd), {
+ iv,
+ padding: CryptoJS.pad.Pkcs7,
+ mode: CryptoJS.mode.CBC
+ }).ciphertext.toUint8Array();
+ outBuf.set(iv.toUint8Array(), 44);
+ outBuf.set(CryptoJS.SHA256(CryptoJS.SHA256(this.#pwd)).toUint8Array(), 12);
+ const eOut = new Uint8Array(128 + encrypted.byteLength);
+ const eOutDV = new DataView(eOut.buffer);
+ eOut.set(outBuf.slice(0, 128), 0);
+ eOut.set(encrypted, 128);
+ eOutDV.setUint32(64, murmurhash3_32_gc(new TextDecoder().decode(encrypted), 0x31082007), true);
+ out[i] = eOut;
+ } else {
+ outBufDV.setUint32(64, murmurhash3_32_gc(new TextDecoder().decode(pack), 0x31082007), true);
+ out[i] = outBuf;
+ };
+ break;
- innerOffs = (new TextEncoder().encode(file[2].group)).byteLength;
- fileStartDV.setUint16(offs, innerOffs, true);
- fileStart.set(new TextEncoder().encode(file[2].group), offs + 2);
- offs += innerOffs + 2;
+ case 5:
+ var size = 128; // Bytes
+ filesInBuffer.forEach(file => size +=
+ 38 + // various constants
+
+ (new TextEncoder().encode(this.#files[file[3]][0])).byteLength + // File name length
+ (new TextEncoder().encode(this.#files[file[3]][2].owner)).byteLength + // Owner name length
+ (new TextEncoder().encode(this.#files[file[3]][2].group)).byteLength + // Group name length
+ (new TextEncoder().encode(this.#files[file[3]][2].webLink)).byteLength // Web link length
+ );
+
+ var fileStart = new Uint8Array(size);
+ var fileStartDV = new DataView(fileStart.buffer);
+ fileStart.set(new TextEncoder().encode('HSSP'), 0); // Magic value :) | 4+0
+ fileStartDV.setUint8(4, 4); // File standard version, see https://hssp.leox.dev/docs/versions | 1+4
+ fileStartDV.setUint8(4, 5); // File standard version, see https://hssp.leox.dev/docs/versions | 1+4
+ fileStartDV.setUint8(5, parseInt([
+ this.#pwd !== null, // F1: is encrypted
+ this.#compAlgo !== 'NONE', // F2: is compressed
+ true, // F3: is split
+ false, // F4: unallocated
+ false, // F5: unallocated
+ false, // F6: unallocated
+ false, // F7: unallocated
+ false // F8: unallocated
+ ].map(b => +b).join(''), 2)); // Flags #1, see https://hssp.leox.dev/docs/flags | 1+5
+ fileStartDV.setUint8(6, parseInt([
+ false, // F9: unallocated
+ false, // F10: unallocated
+ false, // F11: unallocated
+ false, // F12: unallocated
+ false, // F13: unallocated
+ false, // F14: unallocated
+ false, // F15: unallocated
+ false // F16: unallocated
+ ].map(b => +b).join(''), 2)); // Flags #2, see https://hssp.leox.dev/docs/flags | 1+6
+ fileStartDV.setUint8(7, parseInt([
+ false, // F17: unallocated
+ false, // F18: unallocated
+ false, // F19: unallocated
+ false, // F20: unallocated
+ false, // F21: unallocated
+ false, // F22: unallocated
+ false, // F23: unallocated
+ false // F24: unallocated
+ ].map(b => +b).join(''), 2)); // Flags #3, see https://hssp.leox.dev/docs/flags | 1+7
+ fileStartDV.setUint32(8, filesInBuffer.length, true); // File count | 4+8
+ for (var j = 3; j < 11; j++) {
+ fileStartDV.setUint32(j * 4, 0, true); // Password hash, if not set = 0 | 32+12
+ // 12 - 44
+ };
+ for (var j = 0; j < 4; j++) {
+ fileStartDV.setUint32(j * 4 + 44, 0, true); // Encryption initialization vector (iv), if not set = 0 | 16+44
+ // 44 - 60
+ };
+ fileStart.set(new TextEncoder().encode(this.#compAlgo), 60); // Used compression algorithm, 0 if not set | 4+60
+
+ fileStartDV.setBigUint64(68, BigInt(this.#files.length), true); // total file count | 8+68
+ fileStartDV.setBigUint64(76, BigInt(filesInBuffer[0][1] <= 0 ? Math.abs(filesInBuffer[0][1]) : 0), true); // split file offset | 8+76
+ if (out[i - 1]) fileStartDV.setUint32(84, murmurhash3_32_gc(new TextDecoder().decode(out[i - 1].subarray(128, out[i - 1].byteLength)), 0x31082007), true); // Checksum of previous package | 4+84
+ fileStartDV.setUint32(88, 0, true); // Checksum of next package | 4+88
+ fileStartDV.setUint32(92, i, true); // File ID of this package | 4+92
+
+ fileStart.set(new TextEncoder().encode(this.#comment.slice(0, 16)), 96); // Comment | 16+96
+ fileStart.set(new TextEncoder().encode('hssp 4.0.0 WEB'), 112); // Used generator | 16+112
+
+ var offs = 128; // Start
+
+ // index
+ filesInBuffer.forEach(file => {
+ file = this.#files[file[3]];
+
+ var innerOffs = 0;
+ fileStartDV.setBigUint64(offs, BigInt(file[1].byteLength), true);
+ offs += innerOffs + 8;
+
+ innerOffs = (new TextEncoder().encode(file[0])).byteLength;
+ fileStartDV.setUint16(offs, innerOffs, true);
+ fileStart.set(new TextEncoder().encode(file[0]), offs + 2);
+ offs += innerOffs + 2;
+
+ innerOffs = (new TextEncoder().encode(file[2].owner)).byteLength;
+ fileStartDV.setUint16(offs, innerOffs, true);
+ fileStart.set(new TextEncoder().encode(file[2].owner), offs + 2);
+ offs += innerOffs + 2;
+
+ innerOffs = (new TextEncoder().encode(file[2].group)).byteLength;
+ fileStartDV.setUint16(offs, innerOffs, true);
+ fileStart.set(new TextEncoder().encode(file[2].group), offs + 2);
+ offs += innerOffs + 2;
+
+ innerOffs = (new TextEncoder().encode(file[2].webLink)).byteLength;
+ fileStartDV.setUint32(offs, innerOffs, true);
+ fileStart.set(new TextEncoder().encode(file[2].webLink), offs + 4);
+ offs += innerOffs + 4;
+
+ var u48c = new Uint8Array(8);
+ var u48cDV = new DataView(u48c.buffer);
+ u48cDV.setBigUint64(0, BigInt(file[2].created.getTime()), true);
+ fileStart.set(u48c.slice(0, 6), offs);
+ u48cDV.setBigUint64(0, BigInt(file[2].changed.getTime()), true);
+ fileStart.set(u48c.slice(0, 6), offs + 6);
+ u48cDV.setBigUint64(0, BigInt(file[2].opened.getTime()), true);
+ fileStart.set(u48c.slice(0, 6), offs + 12);
+ offs += 18;
+
+ parseInt(file[2].permissions, 8).toString(2).split('').map(x => parseInt(x)).forEach((bit, addr) => {
+ fileStart[offs + Math.floor(addr / 8)] |= (bit << addr);
+ });
+ fileStart[offs + 1] |= (+!!file[2].isFolder << 1);
+ fileStart[offs + 1] |= (+!!file[2].hidden << 2);
+ fileStart[offs + 1] |= (+!!file[2].system << 3);
+ fileStart[offs + 1] |= (+!!file[2].enableBackup << 4);
+ fileStart[offs + 1] |= (+!!file[2].forceBackup << 5);
+ fileStart[offs + 1] |= (+!!file[2].readOnly << 6);
+ fileStart[offs + 1] |= (+!!file[2].mainFile << 7);
+ offs += 2;
+ });
- innerOffs = (new TextEncoder().encode(file[2].webLink)).byteLength;
- fileStartDV.setUint32(offs, innerOffs, true);
- fileStart.set(new TextEncoder().encode(file[2].webLink), offs + 4);
- offs += innerOffs + 4;
+ var oldOut = out[i];
+ out[i] = new Uint8Array(fileStart.byteLength + oldOut.byteLength);
+ out[i].set(fileStart, 0);
+ out[i].set(oldOut, fileStart.byteLength);
+ var outBuf = out[i];
+ var pack = outBuf.subarray(128, outBuf.byteLength);
- var u48c = new Uint8Array(8);
- var u48cDV = new DataView(u48c.buffer);
- u48cDV.setBigUint64(0, BigInt(file[2].created.getTime()), true);
- fileStart.set(u48c.slice(0, 6), offs);
- u48cDV.setBigUint64(0, BigInt(file[2].changed.getTime()), true);
- fileStart.set(u48c.slice(0, 6), offs + 6);
- u48cDV.setBigUint64(0, BigInt(file[2].opened.getTime()), true);
- fileStart.set(u48c.slice(0, 6), offs + 12);
- offs += 18;
-
- parseInt(file[2].permissions, 8).toString(2).split('').map(x => parseInt(x)).forEach((bit, addr) => {
- fileStart[offs + Math.floor(addr / 8)] |= (bit << addr);
- });
- fileStart[offs + 1] |= (+!!file[2].isFolder << 1);
- fileStart[offs + 1] |= (+!!file[2].hidden << 2);
- fileStart[offs + 1] |= (+!!file[2].system << 3);
- fileStart[offs + 1] |= (+!!file[2].enableBackup << 4);
- fileStart[offs + 1] |= (+!!file[2].forceBackup << 5);
- fileStart[offs + 1] |= (+!!file[2].readOnly << 6);
- fileStart[offs + 1] |= (+!!file[2].mainFile << 7);
- offs += 2;
- });
+ switch (this.#compAlgo) {
+ case 'DFLT':
+ outBuf = HSSP._internal.mergeUint8Arrays(outBuf.subarray(0, 128), pako.deflate(pack, { level: this.#compLvl }));
+ break;
- var oldOut = out[i];
- out[i] = new Uint8Array(fileStart.byteLength + oldOut.byteLength);
- out[i].set(fileStart, 0);
- out[i].set(oldOut, fileStart.byteLength);
- var outBuf = out[i];
- var pack = outBuf.subarray(128, outBuf.byteLength);
+ case 'LZMA':
+ outBuf = HSSP._internal.mergeUint8Arrays(outBuf.subarray(0, 128), LZMA.compress(pack, this.#compLvl));
+ break;
- switch (this.#compAlgo) {
- case 'DFLT':
- outBuf = HSSP._internal.mergeUint8Arrays(outBuf.subarray(0, 128), pako.deflate(pack, { level: this.#compLvl }));
- break;
+ case 'NONE':
+ break;
- case 'LZMA':
- outBuf = HSSP._internal.mergeUint8Arrays(outBuf.subarray(0, 128), LZMA.compress(pack, this.#compLvl));
- break;
+ default:
+ throw new Error('COMPRESSION_NOT_SUPPORTED');
+ };
- case 'NONE':
+ size = outBuf.byteLength;
+ pack = outBuf.slice(128, size);
+ var outBufDV = new DataView(outBuf.buffer);
+ if (this.#pwd !== null) {
+ const iv = CryptoJS.lib.WordArray.random(16);
+ const encrypted = CryptoJS.AES.encrypt(CryptoJS.lib.WordArray.create(pack), CryptoJS.SHA256(this.#pwd), {
+ iv,
+ padding: CryptoJS.pad.Pkcs7,
+ mode: CryptoJS.mode.CBC
+ }).ciphertext.toUint8Array();
+ outBuf.set(iv.toUint8Array(), 44);
+ outBuf.set(CryptoJS.SHA256(CryptoJS.SHA256(this.#pwd)).toUint8Array(), 12);
+ const eOut = new Uint8Array(128 + encrypted.byteLength);
+ const eOutDV = new DataView(eOut.buffer);
+ eOut.set(outBuf.slice(0, 128), 0);
+ eOut.set(encrypted, 128);
+ eOutDV.setUint32(64, murmurhash3_32_gc(new TextDecoder().decode(encrypted), 0x31082007), true);
+ out[i] = eOut;
+ } else {
+ outBufDV.setUint32(64, murmurhash3_32_gc(new TextDecoder().decode(pack), 0x31082007), true);
+ out[i] = outBuf;
+ };
break;
default:
- throw new Error('COMPRESSION_NOT_SUPPORTED');
- };
-
- size = outBuf.byteLength;
- pack = outBuf.slice(128, size);
- var outBufDV = new DataView(outBuf.buffer);
- if (this.#pwd !== null) {
- const iv = CryptoJS.lib.WordArray.random(16);
- const encrypted = CryptoJS.AES.encrypt(CryptoJS.lib.WordArray.create(pack), CryptoJS.SHA256(this.#pwd), {
- iv,
- padding: CryptoJS.pad.Pkcs7,
- mode: CryptoJS.mode.CBC
- }).ciphertext.toUint8Array();
- outBuf.set(iv.toUint8Array(), 44);
- outBuf.set(CryptoJS.SHA256(CryptoJS.SHA256(this.#pwd)).toUint8Array(), 12);
- const eOut = new Uint8Array(128 + encrypted.byteLength);
- const eOutDV = new DataView(eOut.buffer);
- eOut.set(outBuf.slice(0, 128), 0);
- eOut.set(encrypted, 128);
- eOutDV.setUint32(64, murmurhash3_32_gc(new TextDecoder().decode(encrypted), 0x31082007), true);
- out[i] = eOut;
- } else {
- outBufDV.setUint32(64, murmurhash3_32_gc(new TextDecoder().decode(pack), 0x31082007), true);
- out[i] = outBuf;
+ throw new Error('VERSION_NOT_SUPPORTED');
};
};
@@ -1765,6 +2094,158 @@ const HSSP = {
const usedTDU8 = utdu8;
var offs = usedTDU8 ? 0 : 128;
+ for (var i = 0; i < fileCount; i++) {
+ var file = [];
+ file[2] = {};
+
+ var innerOffs = 0;
+ file[2].size = dataDV.getBigUint64(offs, true);
+ offs += innerOffs + 8;
+
+ var innerOffs = dataDV.getUint16(offs, true);
+ file[0] = new TextDecoder().decode(dataU8.subarray(offs - (usedTDU8 ? 0 : 128) + 2, offs - (usedTDU8 ? 0 : 128) + 2 + innerOffs));
+ offs += innerOffs + 2;
+
+ innerOffs = dataDV.getUint16(offs, true);
+ file[2].owner = new TextDecoder().decode(dataU8.subarray(offs - (usedTDU8 ? 0 : 128) + 2, offs - (usedTDU8 ? 0 : 128) + 2 + innerOffs));
+ offs += innerOffs + 2;
+
+ innerOffs = dataDV.getUint16(offs, true);
+ file[2].group = new TextDecoder().decode(dataU8.subarray(offs - (usedTDU8 ? 0 : 128) + 2, offs - (usedTDU8 ? 0 : 128) + 2 + innerOffs));
+ offs += innerOffs + 2;
+
+ innerOffs = dataDV.getUint32(offs, true);
+ file[2].webLink = new TextDecoder().decode(dataU8.subarray(offs - (usedTDU8 ? 0 : 128) + 4, offs - (usedTDU8 ? 0 : 128) + 4 + innerOffs));
+ offs += innerOffs + 4;
+
+ file[2].created = new Date((() => {
+ var rt = 0;
+ for (var i = 0; i < 6; i++) {
+ rt += dataDV.getUint8(offs + i) * Math.pow(256, i);
+ };
+ return rt;
+ })());
+ offs += 6;
+ file[2].changed = new Date((() => {
+ var rt = 0;
+ for (var i = 0; i < 6; i++) {
+ rt += dataDV.getUint8(offs + i) * Math.pow(256, i);
+ };
+ return rt;
+ })());
+ offs += 6;
+ file[2].opened = new Date((() => {
+ var rt = 0;
+ for (var i = 0; i < 6; i++) {
+ rt += dataDV.getUint8(offs + i) * Math.pow(256, i);
+ };
+ return rt;
+ })());
+ offs += 6;
+
+ var permissions = '';
+ for (var j = 0; j < 9; j++) {
+ permissions += (dataU8[offs - (usedTDU8 ? 0 : 128) + Math.floor(j / 8)] >> j % 8) & 1;
+ };
+ file[2].permissions = +parseInt(permissions, 2).toString(8);
+
+ file[2].isFolder = !!((dataU8[offs - (usedTDU8 ? 0 : 128) + 1] >> 1) & 1);
+ file[2].hidden = !!((dataU8[offs - (usedTDU8 ? 0 : 128) + 1] >> 2) & 1);
+ file[2].system = !!((dataU8[offs - (usedTDU8 ? 0 : 128) + 1] >> 3) & 1);
+ file[2].enableBackup = !!((dataU8[offs - (usedTDU8 ? 0 : 128) + 1] >> 4) & 1);
+ file[2].forceBackup = !!((dataU8[offs - (usedTDU8 ? 0 : 128) + 1] >> 5) & 1);
+ file[2].readOnly = !!((dataU8[offs - (usedTDU8 ? 0 : 128) + 1] >> 6) & 1);
+ file[2].mainFile = !!((dataU8[offs - (usedTDU8 ? 0 : 128) + 1] >> 7) & 1);
+ offs += 2;
+
+ metadata.files[file[0]] = file[2];
+ };
+ return metadata;
+ case 5: // v4: Uses flags
+ metadata.version = 5;
+ const inp = bufferU8.subarray(128, bufferU8.length);
+ metadata.hash.valid = true;
+ const hash = murmurhash3_32_gc(new TextDecoder().decode(inp), 0x31082007);
+ metadata.hash.given = bufferDV.getUint32(64, true);
+ metadata.hash.calculated = hash;
+ if (bufferDV.getUint32(64, true) !== hash) metadata.hash.valid = false;
+ const fileCount = bufferDV.getUint32(8, true);
+ bufferDV.getUint8(5).toString(2).split('').map(n => !!n).forEach(b => flags.push(b));
+ bufferDV.getUint8(6).toString(2).split('').map(n => !!n).forEach(b => flags.push(b));
+ bufferDV.getUint8(7).toString(2).split('').map(n => !!n).forEach(b => flags.push(b));
+ metadata.compression = false;
+ if (flags[1]) switch (new TextDecoder().decode(bufferU8.subarray(60, 64))) {
+ case 'DFLT':
+ metadata.compression = 'DEFLATE';
+ break;
+
+ case 'LZMA':
+ metadata.compression = 'LZMA';
+ break;
+
+ default:
+ metadata.compression = null;
+ break;
+ };
+ metadata.password.correct = null;
+ var tempDataU8;
+ if (flags[0]) {
+ metadata.password.correct = false;
+ metadata.password.given.hash = CryptoJS.SHA256(CryptoJS.SHA256(password)).toString(CryptoJS.enc.Hex);
+ metadata.password.given.clear = password;
+ metadata.password.hash = Array.from(bufferU8.subarray(12, 44)).map(e => e.toString(16).length < 2 ? '0' + e.toString(16) : e.toString(16)).join('');
+ if (CryptoJS.SHA256(CryptoJS.SHA256(password)).toString(CryptoJS.enc.Hex) !== bufferU8.subarray(12, 44).reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), '')) return metadata;
+ metadata.password.correct = true;
+ const iv = bufferU8.subarray(44, 60);
+ const encrypted = bufferU8.subarray(128, buffer.byteLength);
+ const decrypted = CryptoJS.AES.decrypt(CryptoJS.lib.CipherParams.create({
+ ciphertext: CryptoJS.lib.WordArray.create(encrypted),
+ salt: CryptoJS.lib.WordArray.create(iv)
+ }), CryptoJS.SHA256(password), {
+ iv: CryptoJS.lib.WordArray.create(iv),
+ padding: CryptoJS.pad.Pkcs7,
+ mode: CryptoJS.mode.CBC
+ });
+
+ tempDataU8 = decrypted.toUint8Array();
+ };
+
+ if (flags[1]) switch (new TextDecoder().decode(bufferU8.subarray(60, 64))) {
+ case 'DFLT':
+ tempDataU8 = pako.inflate(tempDataU8 ?? inp);
+ break;
+
+ case 'LZMA':
+ var decompressed = LZMA.decompress(tempDataU8 ?? inp);
+ tempDataU8 = (typeof decompressed == 'string') ? new TextEncoder().encode(decompressed) : Uint8Array.from(decompressed);
+ break;
+ };
+
+ var utdu8 = true;
+ const dataU8 = (() => {
+ if ((tempDataU8 ?? true) === true) {
+ utdu8 = false;
+ return inp;
+ } else return tempDataU8;
+ })();
+ const data = dataU8.buffer;
+ const dataDV = new DataView(data);
+
+ metadata.split.totalFileCount = Number(bufferDV.getBigUint64(68, true));
+ if (flags[2]) {
+ metadata.split.id = bufferDV.getUint32(92, true);
+ metadata.split.checksums.previous = bufferDV.getUint32(84, true);
+ metadata.split.checksums.next = bufferDV.getUint32(88, true);
+ metadata.split.splitFileOffset = Number(bufferDV.getBigUint64(76, true));
+ };
+
+ metadata.comment = new TextDecoder().decode(bufferU8.subarray(96, 112)).split('\x00', '');
+
+ metadata.generator = new TextDecoder().decode(bufferU8.subarray(112, 128)).split('\x00', '');
+
+ const usedTDU8 = utdu8;
+ var offs = usedTDU8 ? 0 : 128;
+
for (var i = 0; i < fileCount; i++) {
var file = [];
file[2] = {};