aboutsummaryrefslogtreecommitdiff
path: root/modules/ext/cryptogzip.js
diff options
context:
space:
mode:
Diffstat (limited to 'modules/ext/cryptogzip.js')
-rw-r--r--modules/ext/cryptogzip.js321
1 files changed, 313 insertions, 8 deletions
diff --git a/modules/ext/cryptogzip.js b/modules/ext/cryptogzip.js
index b67e067..200abdd 100644
--- a/modules/ext/cryptogzip.js
+++ b/modules/ext/cryptogzip.js
@@ -1,6 +1,7 @@
class CryptoGzipModule {
- // CryptoGzipModule — VFS-level gzip/encrypt commands for WebX11 newOS4
- // Commands: gzip <path>, gunzip <path.gz>, encrypt <path> <passphrase>, decrypt <path.enc> <passphrase>
+ // CryptoGzipModule — VFS-level gzip/encrypt/tar commands for WebX11 newOS4
+ // Commands: gzip <path>, gunzip <path.gz>, encrypt <path> <passphrase>, decrypt <path.enc> <passphrase>,
+ // tar <src-dir> <out.tar[.gz]>, untar <path.tar[.gz]> [dest-dir]
constructor() {
this.commands = {};
this.terminal = null;
@@ -9,9 +10,12 @@ class CryptoGzipModule {
getHelpInfo() {
return {
'gzip': 'gzip <path> — Compress a VFS file to .gz',
- 'gunzip': 'gunzip <path.gz> — Decompress a .gz file',
- 'encrypt': 'encrypt <path> <passphrase> — AES-256-GCM encrypt a VFS file',
- 'decrypt': 'decrypt <path.enc> <passphrase> — Decrypt a .enc file'
+ 'gunzip': 'gunzip <path.gz> — Decompress a .gz file (binary-safe)',
+ 'encrypt': 'encrypt <path> <passphrase> — AES-256-GCM encrypt a file',
+ 'decrypt': 'decrypt <path.enc> <passphrase> — Decrypt a .enc file',
+ 'tar': 'tar <src-dir> <out.tar[.gz]> — Bundle a VFS directory into a tar archive (gzip if .gz)',
+ 'untar': 'untar <path.tar[.gz]> [dest-dir] — Extract a tar archive into VFS (auto-detects gzip)',
+ 'wget': 'wget <url> [filename] — Download a URL and save to VFS'
};
}
@@ -20,6 +24,17 @@ class CryptoGzipModule {
const normalized = path.replace(/^\/+/, '');
return this.terminal.fs.readFile(normalized) !== null;
}
+ _vfsDirExists(path) {
+ // Check if a path is a directory (or empty string for root)
+ const normalized = path.replace(/^\/+/, '').replace(/\/+$/, '');
+ if (!normalized) return true; // root always exists
+ try {
+ const items = this.terminal.fs.readDir(normalized);
+ return Array.isArray(items);
+ } catch (e) {
+ return false;
+ }
+ }
_vfsRead(path) {
const normalized = path.replace(/^\/+/, '');
@@ -152,6 +167,290 @@ class CryptoGzipModule {
return new TextDecoder().decode(decompressed);
}
+ // Binary-safe variants: return raw Uint8Array, store as base64 in VFS
+ async _gzipBytes(bytes) {
+ const cs = new CompressionStream('gzip');
+ const writer = cs.writable.getWriter();
+ writer.write(bytes);
+ writer.close();
+ const compressed = await new Response(cs.readable).arrayBuffer();
+ return new Uint8Array(compressed);
+ }
+ async _gunzipBytes(bytes) {
+ const ds = new DecompressionStream('gzip');
+ const writer = ds.writable.getWriter();
+ writer.write(bytes);
+ writer.close();
+ const decompressed = await new Response(ds.readable).arrayBuffer();
+ return new Uint8Array(decompressed);
+ }
+ _bytesToBase64(bytes) {
+ let s = '';
+ for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]);
+ return btoa(s);
+ }
+ _base64ToBytes(b64) {
+ const s = atob(b64);
+ const u8 = new Uint8Array(s.length);
+ for (let i = 0; i < s.length; i++) u8[i] = s.charCodeAt(i);
+ return u8;
+ }
+// Save binary file to VFS (stored as Latin-1 string — each byte → one char, round-trips via charCodeAt)
+ _vfsWriteBytes(path, bytes) {
+ const normalized = path.replace(/^\/+/, '');
+ let s = '';
+ for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]);
+ this.terminal.fs.writeFile(normalized, s);
+ }
+ // Load binary file from VFS (auto-detects format: base64 via heuristic, or raw Latin-1 bytes)
+ _vfsReadBytes(path) {
+ const normalized = path.replace(/^\/+/, '');
+ const content = this.terminal.fs.readFile(normalized);
+ if (content === null) throw new Error(`File not found: ${path}`);
+// Heuristic: if content looks like base64 (only [A-Za-z0-9+/\n\r]) and length is multiple of 4, decode
+ const looksLikeBase64 = /^[A-Za-z0-9+/\n\r]+=*$/.test(content.slice(0, 200));
+ if (looksLikeBase64 && content.length % 4 === 0) {
+ try { return this._base64ToBytes(content); } catch (e) {}
+ }
+ // Fallback: treat as Latin-1 bytes (each charCode is a byte)
+ const u8 = new Uint8Array(content.length);
+ for (let i = 0; i < content.length; i++) u8[i] = content.charCodeAt(i);
+ return u8;
+ }
+ // Detect gzip magic bytes (1f 8b)
+ _isGzip(bytes) {
+ return bytes.length >= 2 && bytes[0] === 0x1f && bytes[1] === 0x8b;
+ }
+
+ // ============================================================
+ // TAR / UNTAR — POSIX ustar parser (pure JS, ~120 lines)
+ // Supports: ustar + gnu tar formats, gzip auto-detect
+ // ============================================================
+
+ _parseOctal(str, len) {
+ // Parse octal string (null/space-padded) from tar header field
+ const s = (str || '').replace(/[\x00\s]+$/g, '').replace(/^0+/, '') || '0';
+ return parseInt(s, 8) || 0;
+ }
+ _padOctal(num, len) {
+ // Format number as octal, null-terminated, len bytes
+ const oct = num.toString(8);
+ return oct + '\0'.repeat(len - oct.length - 1);
+ }
+
+ // Parse a tar buffer (Uint8Array) into [{name, type, size, data, mode, mtime}]
+ _tarParse(bytes) {
+ const entries = [];
+ const decoder = new TextDecoder();
+ let off = 0;
+ while (off < bytes.length) {
+ // Read 512-byte header
+ if (off + 512 > bytes.length) break;
+ const header = bytes.slice(off, off + 512);
+ // Empty block = end of archive
+ if (header.every(b => b === 0)) break;
+ const nameRaw = decoder.decode(header.slice(0, 100)).replace(/\0+$/, '');
+ const size = this._parseOctal(decoder.decode(header.slice(124, 136)), 12);
+ const typeFlag = String.fromCharCode(header[156]);
+ const magic = decoder.decode(header.slice(257, 263)).replace(/\0+$/, '');
+ // ustar prefix (POSIX): name split across header.name + header.prefix
+ let name = nameRaw;
+ if (magic === 'ustar') {
+ const prefix = decoder.decode(header.slice(345, 500)).replace(/\0+$/, '');
+ if (prefix) name = prefix + '/' + nameRaw;
+ }
+ off += 512;
+ // Read file data (padded to 512)
+ const data = bytes.slice(off, off + size);
+ off += Math.ceil(size / 512) * 512;
+ entries.push({ name, type: typeFlag, size, data, mode: this._parseOctal(decoder.decode(header.slice(100, 108)), 8) });
+ }
+ return entries;
+ }
+
+ // Build a tar buffer from a list of {name, type, size, data, mode} entries
+ _tarBuild(entries) {
+ const blocks = [];
+ const encoder = new TextEncoder();
+ for (const e of entries) {
+ const header = new Uint8Array(512);
+ // Name (100 bytes, null-padded)
+ const nameBytes = encoder.encode(e.name.slice(0, 99));
+ header.set(nameBytes, 0);
+ // Mode (8 bytes, octal null-terminated) - default 0644
+ const modeStr = (e.mode || 0o644).toString(8).padStart(7, '0') + '\0';
+ header.set(encoder.encode(modeStr), 100);
+ // uid/gid (8 bytes each, "0000000\0")
+ header.set(encoder.encode('0000000\0'), 108);
+ header.set(encoder.encode('0000000\0'), 116);
+ // Size (12 bytes, octal)
+ const sizeStr = e.size.toString(8).padStart(11, '0') + '\0';
+ header.set(encoder.encode(sizeStr), 124);
+ // Mtime (12 bytes, octal) - epoch
+ const mtimeStr = Math.floor(Date.now() / 1000).toString(8).padStart(11, '0') + '\0';
+ header.set(encoder.encode(mtimeStr), 136);
+ // Type flag at offset 156
+ header[156] = e.type.charCodeAt(0);
+ // Magic "ustar\0" at 257
+ header.set(encoder.encode('ustar\0'), 257);
+ // Version "00" at 263
+ header.set(encoder.encode('00'), 263);
+ blocks.push(header);
+ // Data block(s), padded to 512
+ const data = e.data;
+ if (data && data.length > 0) {
+ const padded = new Uint8Array(Math.ceil(data.length / 512) * 512);
+ padded.set(data, 0);
+ blocks.push(padded);
+ }
+ }
+ // End-of-archive: two 512-byte zero blocks
+ blocks.push(new Uint8Array(512));
+ blocks.push(new Uint8Array(512));
+ // Concatenate
+ const total = blocks.reduce((s, b) => s + b.length, 0);
+ const out = new Uint8Array(total);
+ let pos = 0;
+ for (const b of blocks) { out.set(b, pos); pos += b.length; }
+ return out;
+ }
+
+ // Collect all files in a VFS directory recursively
+ _vfsCollectFiles(dirPath) {
+ const normalized = dirPath.replace(/^\/+/, '').replace(/\/+$/, '');
+ const entries = [];
+ const walk = (rel, absParts) => {
+ const items = this.terminal.fs.readDir(absParts.join('/'));
+ for (const item of items) {
+ const itemPath = absParts.concat([item.name]).join('/');
+ const relPath = rel ? `${rel}/${item.name}` : item.name;
+ if (item.type === 'dir') {
+ walk(relPath, absParts.concat([item.name]));
+ } else {
+ const content = this.terminal.fs.readFile(itemPath);
+ entries.push({ vfsPath: itemPath, archivePath: relPath, content: content || '' });
+ }
+ }
+ };
+ // Handle root case
+ if (!normalized) {
+ walk('', []);
+ } else {
+ walk(normalized.split('/').pop() || '', normalized.split('/'));
+ }
+ return entries;
+ }
+
+ // ============================================================
+ // WGET — fetch URL and save to VFS (binary-safe)
+ // ============================================================
+
+ async _cmdWget(args) {
+ if (args.length < 1) {
+ this.terminal.print('Usage: wget <url> [filename] — Download a URL and save to VFS', 'warning');
+ return;
+ }
+ const url = args[0];
+ const filename = args[1] || url.split('/').pop() || 'download.bin';
+ this.terminal.print(`Downloading ${url}...`, 'info');
+ try {
+ const resp = await fetch(url);
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}: ${resp.statusText}`);
+ const buf = await resp.arrayBuffer();
+ const bytes = new Uint8Array(buf);
+ this._vfsWriteBytes(filename, bytes);
+ const sizeKb = (bytes.length / 1024).toFixed(1);
+ this.terminal.print(`✅ wget: ${filename} (${sizeKb} KB, ${bytes.length} bytes)`, 'success');
+ } catch (e) {
+ this.terminal.print(`wget error: ${e.message}`, 'error');
+ }
+ }
+
+ // ============================================================
+ // TAR / UNTAR COMMANDS
+ // ============================================================
+
+ async _cmdTar(args) {
+ if (args.length < 2) {
+ this.terminal.print('Usage: tar <src-dir> <out.tar[.gz]>', 'warning');
+ return;
+ }
+ const [srcPath, outPath] = args;
+ if (!this._vfsDirExists(srcPath)) {
+ this.terminal.print(`Source not found (or not a directory): ${srcPath}`, 'error');
+ return;
+ }
+ this.terminal.print(`Bundling ${srcPath} → ${outPath}...`, 'info');
+ const files = this._vfsCollectFiles(srcPath);
+ if (files.length === 0) {
+ this.terminal.print('No files found in source directory', 'warning');
+ return;
+ }
+ // Build tar entries (all '0' = regular file)
+ const entries = files.map(f => ({
+ name: f.archivePath,
+ type: '0',
+ size: f.content.length,
+ data: new TextEncoder().encode(f.content),
+ mode: 0o644
+ }));
+ let tarBytes = this._tarBuild(entries);
+ // Gzip if .gz suffix
+ if (outPath.endsWith('.gz')) {
+ tarBytes = await this._gzipBytes(tarBytes);
+ }
+ this._vfsWriteBytes(outPath, tarBytes);
+ const sizeKb = (tarBytes.length / 1024).toFixed(1);
+ this.terminal.print(`✅ tar: ${files.length} files → ${outPath} (${sizeKb} KB)`, 'success');
+ }
+
+ async _cmdUntar(args) {
+ if (args.length < 1) {
+ this.terminal.print('Usage: untar <path.tar[.gz]> [dest-dir]', 'warning');
+ return;
+ }
+ const srcPath = args[0];
+ const destDir = (args[1] || '/').replace(/^\/+/, '').replace(/\/+$/, '');
+ if (!this._vfsExists(srcPath)) {
+ this.terminal.print(`File not found: ${srcPath}`, 'error');
+ return;
+ }
+ this.terminal.print(`Extracting ${srcPath} → /${destDir}...`, 'info');
+ let bytes = this._vfsReadBytes(srcPath);
+ // Auto-detect and strip gzip
+ if (this._isGzip(bytes)) {
+ this.terminal.print(' detected gzip wrapper, decompressing...', 'info');
+ bytes = await this._gunzipBytes(bytes);
+ }
+ const entries = this._tarParse(bytes);
+ if (entries.length === 0) {
+ this.terminal.print('No entries found in tar (empty or invalid format)', 'error');
+ return;
+ }
+ // Write to VFS
+ let fileCount = 0, dirCount = 0;
+ for (const e of entries) {
+ // Clean name (strip leading ./, normalize slashes)
+ let cleanName = e.name.replace(/^\.?\//, '').replace(/\/+$/, '');
+ const targetPath = destDir ? `${destDir}/${cleanName}` : cleanName;
+ if (e.type === '5' || e.type === 'd') {
+ // Directory
+ if (cleanName) {
+ this.terminal.fs.mkdirRecursive(targetPath);
+ dirCount++;
+ }
+ } else {
+ // Regular file (or type '0' or '\0')
+ const dir = targetPath.split('/').slice(0, -1).join('/');
+ if (dir) this.terminal.fs.mkdirRecursive(dir);
+ const content = new TextDecoder('utf-8', { fatal: false }).decode(e.data);
+ this.terminal.fs.writeFile(targetPath, content);
+ fileCount++;
+ }
+ }
+ this.terminal.print(`✅ untar: ${fileCount} files, ${dirCount} dirs from ${srcPath}`, 'success');
+ }
+
// ============================================================
// ENCRYPT / DECRYPT (AES-256-GCM)
// ============================================================
@@ -280,14 +579,20 @@ class CryptoGzipModule {
this.commands['gunzip'] = async (args) => self._cmdGunzip(args);
this.commands['encrypt'] = async (args) => self._cmdEncrypt(args);
this.commands['decrypt'] = async (args) => self._cmdDecrypt(args);
+ this.commands['tar'] = async (args) => self._cmdTar(args);
+ this.commands['untar'] = async (args) => self._cmdUntar(args);
+ this.commands['wget'] = async (args) => self._cmdWget(args);
this.commandsMeta = {
gzip: { argType: 'file', description: 'Compress a VFS file to .gz' },
- gunzip: { argType: 'file', description: 'Decompress a .gz file' },
+ gunzip: { argType: 'file', description: 'Decompress a .gz file (binary-safe)' },
encrypt: { argType: 'file', description: 'Encrypt a file with passphrase' },
- decrypt: { argType: 'file', description: 'Decrypt a .enc file' }
+ decrypt: { argType: 'file', description: 'Decrypt a .enc file' },
+ tar: { argType: 'file', description: 'Bundle a VFS directory into a tar archive' },
+ untar: { argType: 'file', description: 'Extract a tar archive into VFS' },
+ wget: { argType: 'url', description: 'Download a URL and save to VFS' }
};
}
}
if (typeof window.webx11 !== 'undefined' && window.webx11) {
- webx11.moduleManager.registerModule(new CryptoGzipModule(), 'https://webx11.duckdns.org/spaces/newos4/modules/ext/cryptogzip.js');
+ webx11.moduleManager.registerModule(new CryptoGzipModule(), 'https://webx11.duckdns.org/spaces/newos4/modules/ext/cryptogzip.js?v=2026-06-08-v4.5.26');
}