stegdoc 5.7.0 → 6.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +240 -106
- package/package.json +27 -5
- package/src/commands/decode.js +78 -436
- package/src/commands/encode.js +214 -137
- package/src/commands/info.js +65 -94
- package/src/commands/verify.js +76 -176
- package/src/index.js +9 -6
- package/src/lib/bindings.js +99 -0
- package/src/lib/compression.js +1 -161
- package/src/lib/crypto.js +0 -107
- package/src/lib/docx-handler.js +2 -278
- package/src/lib/log-generator.js +5 -2
- package/src/lib/metadata.js +7 -88
- package/src/lib/native.js +68 -0
- package/src/lib/streams.js +0 -139
- package/src/lib/utils.js +0 -27
- package/src/lib/xlsx-handler.js +1 -272
- package/src/lib/xlsx-writer.js +10 -0
- package/src/lib/file-handler.js +0 -113
- package/src/lib/file-utils.js +0 -160
- package/src/lib/xml-utils.js +0 -115
package/src/commands/verify.js
CHANGED
|
@@ -1,139 +1,102 @@
|
|
|
1
|
-
const path = require('path');
|
|
2
1
|
const chalk = require('chalk');
|
|
3
2
|
const ora = require('ora');
|
|
4
|
-
const {
|
|
5
|
-
const {
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
3
|
+
const { formatBytes } = require('../lib/utils');
|
|
4
|
+
const { loadNative } = require('../lib/native');
|
|
5
|
+
|
|
6
|
+
const NATIVE_REQUIRED =
|
|
7
|
+
'Verifying a file requires the native engine, which is not installed. ' +
|
|
8
|
+
'Reinstall stegdoc, or build it with `pnpm build:native`.';
|
|
9
|
+
|
|
10
|
+
const LEGACY_UNSUPPORTED =
|
|
11
|
+
'The legacy v3/v4 format is no longer supported. Re-encode the file with a current version of stegdoc.';
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
|
-
* Verify
|
|
14
|
+
* Verify a v5/v6 file with the native engine, which also reads v6.
|
|
13
15
|
*/
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
+
function verifyNative(native, inputFile, options, spinner) {
|
|
17
|
+
const info = native.info(inputFile);
|
|
16
18
|
const issues = [];
|
|
17
19
|
|
|
18
|
-
|
|
19
|
-
const format = detectFormat(inputFile);
|
|
20
|
-
if (!format) {
|
|
21
|
-
throw new Error('Unknown file format. Supported formats: .xlsx, .docx');
|
|
22
|
-
}
|
|
20
|
+
spinner.succeed('Metadata valid');
|
|
23
21
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
readResult = await readXlsxBase64(inputFile);
|
|
22
|
+
if (info.encrypted) {
|
|
23
|
+
if (!options.password) {
|
|
24
|
+
issues.push('File is encrypted but no password provided');
|
|
25
|
+
spinner.warn('Encryption check skipped (no password)');
|
|
29
26
|
} else {
|
|
30
|
-
|
|
27
|
+
spinner.text = 'Verifying decryption...';
|
|
28
|
+
try {
|
|
29
|
+
native.decode(inputFile, options.password);
|
|
30
|
+
spinner.succeed('Decryption password valid');
|
|
31
|
+
} catch (e) {
|
|
32
|
+
issues.push('Decryption failed - wrong password or corrupted data');
|
|
33
|
+
spinner.fail('Decryption check failed');
|
|
34
|
+
}
|
|
31
35
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const metadata = extracted.metadata;
|
|
35
|
-
const encryptionMeta = extracted.encryptionMeta;
|
|
36
|
-
|
|
36
|
+
} else {
|
|
37
|
+
// No password needed; a full decode also checks the content hash.
|
|
37
38
|
try {
|
|
38
|
-
|
|
39
|
-
spinner.succeed('
|
|
39
|
+
native.decode(inputFile, undefined);
|
|
40
|
+
spinner.succeed('Content hash valid');
|
|
40
41
|
} catch (e) {
|
|
41
|
-
issues.push(`
|
|
42
|
-
spinner.
|
|
42
|
+
issues.push(`Decode failed: ${e.message}`);
|
|
43
|
+
spinner.fail('Decode check failed');
|
|
43
44
|
}
|
|
45
|
+
}
|
|
44
46
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const totalParts = metadata.totalParts || allParts.length;
|
|
56
|
-
|
|
57
|
-
if (metadata.totalParts !== null && allParts.length !== metadata.totalParts) {
|
|
58
|
-
const missing = [];
|
|
59
|
-
const foundParts = new Set(allParts.map(p => p.partNumber));
|
|
60
|
-
for (let i = 1; i <= metadata.totalParts; i++) {
|
|
61
|
-
if (!foundParts.has(i)) missing.push(i);
|
|
62
|
-
}
|
|
63
|
-
issues.push(`Missing parts: ${missing.join(', ')}`);
|
|
64
|
-
spinner.warn(`Found ${allParts.length}/${metadata.totalParts} parts`);
|
|
65
|
-
} else {
|
|
66
|
-
spinner.succeed(`All ${totalParts} parts found`);
|
|
67
|
-
}
|
|
47
|
+
console.log();
|
|
48
|
+
if (issues.length === 0) {
|
|
49
|
+
console.log(chalk.green.bold('✓ File verification passed!'));
|
|
50
|
+
console.log(chalk.cyan(` Original file: ${info.originalFilename}`));
|
|
51
|
+
console.log(chalk.cyan(` Size: ${formatBytes(info.originalSize)}`));
|
|
52
|
+
console.log(chalk.cyan(` Encrypted: ${info.encrypted ? 'Yes' : 'No'}`));
|
|
53
|
+
console.log(chalk.cyan(` Compressed: ${info.compressed ? 'Yes' : 'No'}`));
|
|
54
|
+
console.log(chalk.cyan(` Format version: ${info.formatVersion} (log-embed)`));
|
|
55
|
+
if (info.partCount > 1) {
|
|
56
|
+
console.log(chalk.cyan(` Parts: ${info.partCount}`));
|
|
68
57
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
spinner.text = 'Verifying decryption...';
|
|
77
|
-
try {
|
|
78
|
-
if (isV5) {
|
|
79
|
-
await verifyV5Encryption(extracted, options.password);
|
|
80
|
-
} else if (isV4) {
|
|
81
|
-
await verifyV4Encryption(inputFile, format, encryptionMeta, options.password);
|
|
82
|
-
} else {
|
|
83
|
-
await verifyV3Encryption(inputFile, format, metadata, extracted.encryptedContent, encryptionMeta, options.password);
|
|
84
|
-
}
|
|
85
|
-
spinner.succeed('Decryption password valid');
|
|
86
|
-
} catch (e) {
|
|
87
|
-
issues.push('Decryption failed - wrong password or corrupted data');
|
|
88
|
-
spinner.fail('Decryption check failed');
|
|
89
|
-
}
|
|
90
|
-
}
|
|
58
|
+
console.log();
|
|
59
|
+
console.log(chalk.green('File is ready to decode.'));
|
|
60
|
+
} else {
|
|
61
|
+
console.log(chalk.yellow.bold('⚠ Verification completed with issues:'));
|
|
62
|
+
console.log();
|
|
63
|
+
for (const issue of issues) {
|
|
64
|
+
console.log(chalk.yellow(` • ${issue}`));
|
|
91
65
|
}
|
|
92
|
-
|
|
93
|
-
// Summary
|
|
94
66
|
console.log();
|
|
67
|
+
console.log(chalk.red('File cannot be decoded until issues are resolved.'));
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
95
71
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
console.log(chalk.cyan(` Compressed: ${metadata.compressed ? 'Yes' : 'No'}`));
|
|
102
|
-
|
|
103
|
-
let versionStr = 'v3 (legacy)';
|
|
104
|
-
if (isV5) versionStr = 'v5 (log-embed)';
|
|
105
|
-
else if (isV4) versionStr = 'v4 (streaming)';
|
|
106
|
-
console.log(chalk.cyan(` Format version: ${versionStr}`));
|
|
107
|
-
|
|
108
|
-
if (hasMultipleParts) {
|
|
109
|
-
const totalParts = metadata.totalParts || 'unknown';
|
|
110
|
-
console.log(chalk.cyan(` Parts: ${totalParts}`));
|
|
111
|
-
}
|
|
72
|
+
/**
|
|
73
|
+
* Verify that a file can be decoded without actually writing output
|
|
74
|
+
*/
|
|
75
|
+
async function verifyCommand(inputFile, options) {
|
|
76
|
+
const spinner = ora('Verifying file...').start();
|
|
112
77
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
for (const issue of issues) {
|
|
119
|
-
console.log(chalk.yellow(` • ${issue}`));
|
|
120
|
-
}
|
|
121
|
-
console.log();
|
|
78
|
+
try {
|
|
79
|
+
const native = loadNative();
|
|
80
|
+
if (!native) {
|
|
81
|
+
throw new Error(NATIVE_REQUIRED);
|
|
82
|
+
}
|
|
122
83
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
84
|
+
let probe;
|
|
85
|
+
try {
|
|
86
|
+
probe = native.probe(inputFile);
|
|
87
|
+
} catch {
|
|
88
|
+
probe = 'unknown';
|
|
89
|
+
}
|
|
90
|
+
if (probe === 'legacy') {
|
|
91
|
+
throw new Error(LEGACY_UNSUPPORTED);
|
|
92
|
+
}
|
|
93
|
+
if (probe !== 'v5' && probe !== 'archive') {
|
|
94
|
+
throw new Error(
|
|
95
|
+
'Unknown file format. Supported formats: .xlsx, .docx, or a zip of them'
|
|
127
96
|
);
|
|
128
|
-
|
|
129
|
-
if (hasBlockingIssue) {
|
|
130
|
-
console.log(chalk.red('File cannot be decoded until issues are resolved.'));
|
|
131
|
-
process.exit(1);
|
|
132
|
-
} else {
|
|
133
|
-
console.log(chalk.yellow('File may still be decodable. Run decode to attempt.'));
|
|
134
|
-
}
|
|
135
97
|
}
|
|
136
98
|
|
|
99
|
+
return verifyNative(native, inputFile, options, spinner);
|
|
137
100
|
} catch (error) {
|
|
138
101
|
spinner.fail('Verification failed');
|
|
139
102
|
console.error(chalk.red(`Error: ${error.message}`));
|
|
@@ -141,67 +104,4 @@ async function verifyCommand(inputFile, options) {
|
|
|
141
104
|
}
|
|
142
105
|
}
|
|
143
106
|
|
|
144
|
-
/**
|
|
145
|
-
* Verify v5 log-embed encryption
|
|
146
|
-
*/
|
|
147
|
-
async function verifyV5Encryption(extracted, password) {
|
|
148
|
-
const { payloadBuffer, encryptionMeta } = extracted;
|
|
149
|
-
const { iv, salt, authTag } = unpackEncryptionMeta(encryptionMeta);
|
|
150
|
-
const decipher = createDecryptStream(password, iv, salt, authTag);
|
|
151
|
-
decipher.update(payloadBuffer);
|
|
152
|
-
decipher.final();
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Verify v4 per-part encryption
|
|
157
|
-
*/
|
|
158
|
-
async function verifyV4Encryption(inputFile, format, encryptionMeta, password) {
|
|
159
|
-
const { iv, salt, authTag } = unpackEncryptionMeta(encryptionMeta);
|
|
160
|
-
|
|
161
|
-
let readResult;
|
|
162
|
-
if (format === 'xlsx') {
|
|
163
|
-
readResult = await readXlsxBase64(inputFile);
|
|
164
|
-
} else {
|
|
165
|
-
readResult = await readDocxBase64(inputFile);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const { encryptedContent } = extractContent(readResult, format);
|
|
169
|
-
const binaryData = Buffer.from(encryptedContent, 'base64');
|
|
170
|
-
|
|
171
|
-
const decipher = createDecryptStream(password, iv, salt, authTag);
|
|
172
|
-
decipher.update(binaryData);
|
|
173
|
-
decipher.final();
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Verify v3 shared encryption
|
|
178
|
-
*/
|
|
179
|
-
async function verifyV3Encryption(inputFile, format, metadata, encryptedContent, encryptionMeta, password) {
|
|
180
|
-
const { iv, salt, authTag } = unpackEncryptionMeta(encryptionMeta);
|
|
181
|
-
|
|
182
|
-
let fullContent = encryptedContent;
|
|
183
|
-
|
|
184
|
-
if (isMultiPart(metadata)) {
|
|
185
|
-
const inputDir = path.dirname(inputFile);
|
|
186
|
-
const allParts = findMultiPartFiles(inputDir, metadata.hash, format);
|
|
187
|
-
|
|
188
|
-
if (allParts.length === metadata.totalParts) {
|
|
189
|
-
const contentParts = [];
|
|
190
|
-
for (const part of allParts) {
|
|
191
|
-
let partResult;
|
|
192
|
-
if (format === 'xlsx') {
|
|
193
|
-
partResult = await readXlsxBase64(part.path);
|
|
194
|
-
} else {
|
|
195
|
-
partResult = await readDocxBase64(part.path);
|
|
196
|
-
}
|
|
197
|
-
const { encryptedContent: partContent } = extractContent(partResult, format);
|
|
198
|
-
contentParts.push(partContent);
|
|
199
|
-
}
|
|
200
|
-
fullContent = contentParts.join('');
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
decrypt(fullContent, password, iv, salt, authTag);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
107
|
module.exports = verifyCommand;
|
package/src/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { program } = require('commander');
|
|
4
4
|
const chalk = require('chalk');
|
|
5
|
+
const { version } = require('../package.json');
|
|
5
6
|
const encodeCommand = require('./commands/encode');
|
|
6
7
|
const decodeCommand = require('./commands/decode');
|
|
7
8
|
const infoCommand = require('./commands/info');
|
|
@@ -11,24 +12,26 @@ const verifyCommand = require('./commands/verify');
|
|
|
11
12
|
program
|
|
12
13
|
.name('stegdoc')
|
|
13
14
|
.description('CLI tool to encode files into Office documents with AES-256 encryption')
|
|
14
|
-
.version(
|
|
15
|
+
.version(version);
|
|
15
16
|
|
|
16
17
|
// Encode command
|
|
17
18
|
program
|
|
18
|
-
.command('encode <
|
|
19
|
-
.description('Encode
|
|
19
|
+
.command('encode <inputs...>')
|
|
20
|
+
.description('Encode one or more files (or folders) into XLSX/DOCX format with compression and optional encryption')
|
|
20
21
|
.option('-o, --output-dir <dir>', 'Output directory for files', process.cwd())
|
|
22
|
+
.option('--bundle-name <name>', 'Filename recorded for a multi-input bundle', 'bundle.zip')
|
|
21
23
|
.option('-s, --chunk-size <size>', 'Maximum size per output file (e.g., "5MB", "25MB")', '5MB')
|
|
22
24
|
.option('-f, --format <format>', 'Output format: xlsx (default) or docx', 'xlsx')
|
|
23
25
|
.option('-p, --password <password>', 'Encryption password (optional, but recommended)')
|
|
24
26
|
.option('--force', 'Overwrite existing files without asking')
|
|
25
|
-
.option('--
|
|
27
|
+
.option('--v5', 'Emit the v5 format (PBKDF2) instead of the v6 default')
|
|
28
|
+
.option('--v6', 'Emit the v6 format (default; Argon2id, authenticated metadata)')
|
|
26
29
|
.option('--no-limit', 'Bypass DOCX 1 MB size limit (large files will produce huge documents)')
|
|
27
30
|
.option('-q, --quiet', 'Minimal output (for scripting)')
|
|
28
31
|
.option('-y, --yes', 'Skip interactive prompts, use defaults')
|
|
29
|
-
.action(async (
|
|
32
|
+
.action(async (inputs, options) => {
|
|
30
33
|
try {
|
|
31
|
-
await encodeCommand(
|
|
34
|
+
await encodeCommand(inputs, options);
|
|
32
35
|
} catch (error) {
|
|
33
36
|
console.error(chalk.red(`Error: ${error.message}`));
|
|
34
37
|
process.exit(1);
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The napi binding target table, shared by the loader and the build scripts.
|
|
5
|
+
*
|
|
6
|
+
* The published package ships no binary. Each target becomes an optional
|
|
7
|
+
* dependency `@stegdoc/binding-<suffix>` carrying one `.node`, selected at
|
|
8
|
+
* require time by `src/lib/native.js`. The shape matches the ecosystem norm
|
|
9
|
+
* (`@swc/core-*`, `@rollup/rollup-*`), which is also what the airgap
|
|
10
|
+
* lockfile's `os`/`cpu`/`libc` filter expects.
|
|
11
|
+
*
|
|
12
|
+
* @typedef {object} BindingTarget
|
|
13
|
+
* @property {string} triple - Rust target triple.
|
|
14
|
+
* @property {string} suffix - npm name suffix, `<os>-<cpu>[-<libc>]`.
|
|
15
|
+
* @property {string} os - npm `os` field value.
|
|
16
|
+
* @property {string} cpu - npm `cpu` field value.
|
|
17
|
+
* @property {string} [libc] - npm `libc` field value.
|
|
18
|
+
* @property {string} lib - Artifact filename Cargo produces.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** Scope the platform packages publish under. Changing it is a one-line rename. */
|
|
22
|
+
const SCOPE = '@stegdoc';
|
|
23
|
+
|
|
24
|
+
/** @type {BindingTarget[]} */
|
|
25
|
+
const TARGETS = [
|
|
26
|
+
{
|
|
27
|
+
triple: 'x86_64-pc-windows-msvc',
|
|
28
|
+
suffix: 'win32-x64-msvc',
|
|
29
|
+
os: 'win32',
|
|
30
|
+
cpu: 'x64',
|
|
31
|
+
lib: 'stegdoc_node.dll',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
triple: 'x86_64-unknown-linux-gnu',
|
|
35
|
+
suffix: 'linux-x64-gnu',
|
|
36
|
+
os: 'linux',
|
|
37
|
+
cpu: 'x64',
|
|
38
|
+
libc: 'glibc',
|
|
39
|
+
lib: 'libstegdoc_node.so',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
triple: 'aarch64-unknown-linux-gnu',
|
|
43
|
+
suffix: 'linux-arm64-gnu',
|
|
44
|
+
os: 'linux',
|
|
45
|
+
cpu: 'arm64',
|
|
46
|
+
libc: 'glibc',
|
|
47
|
+
lib: 'libstegdoc_node.so',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
triple: 'x86_64-apple-darwin',
|
|
51
|
+
suffix: 'darwin-x64',
|
|
52
|
+
os: 'darwin',
|
|
53
|
+
cpu: 'x64',
|
|
54
|
+
lib: 'libstegdoc_node.dylib',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
triple: 'aarch64-apple-darwin',
|
|
58
|
+
suffix: 'darwin-arm64',
|
|
59
|
+
os: 'darwin',
|
|
60
|
+
cpu: 'arm64',
|
|
61
|
+
lib: 'libstegdoc_node.dylib',
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The npm package name for a target.
|
|
67
|
+
* @param {BindingTarget} target
|
|
68
|
+
* @returns {string}
|
|
69
|
+
*/
|
|
70
|
+
function packageName(target) {
|
|
71
|
+
return `${SCOPE}/binding-${target.suffix}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Whether the running Linux is musl-based. No musl package is published, so
|
|
76
|
+
* the loader must not pick the glibc binary for it.
|
|
77
|
+
* @returns {boolean}
|
|
78
|
+
*/
|
|
79
|
+
function isMusl() {
|
|
80
|
+
if (process.platform !== 'linux') return false;
|
|
81
|
+
try {
|
|
82
|
+
return !process.report.getReport().header.glibcVersionRuntime;
|
|
83
|
+
} catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The target matching the running platform, or null when none is published.
|
|
90
|
+
* @returns {BindingTarget|null}
|
|
91
|
+
*/
|
|
92
|
+
function hostTarget() {
|
|
93
|
+
if (isMusl()) return null;
|
|
94
|
+
return (
|
|
95
|
+
TARGETS.find((target) => target.os === process.platform && target.cpu === process.arch) || null
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = { SCOPE, TARGETS, packageName, hostTarget };
|
package/src/lib/compression.js
CHANGED
|
@@ -9,40 +9,6 @@ const zlib = require('zlib');
|
|
|
9
9
|
*/
|
|
10
10
|
const BROTLI_DEFAULT_QUALITY = 6;
|
|
11
11
|
|
|
12
|
-
/**
|
|
13
|
-
* Compress data using Brotli (used in v5+)
|
|
14
|
-
* @param {Buffer} buffer - Data to compress
|
|
15
|
-
* @param {number} [quality] - Compression quality (0-11, default 6)
|
|
16
|
-
* @returns {Promise<Buffer>} Compressed data
|
|
17
|
-
*/
|
|
18
|
-
function compressBrotli(buffer, quality) {
|
|
19
|
-
const q = quality !== undefined ? quality : BROTLI_DEFAULT_QUALITY;
|
|
20
|
-
return new Promise((resolve, reject) => {
|
|
21
|
-
zlib.brotliCompress(buffer, {
|
|
22
|
-
params: {
|
|
23
|
-
[zlib.constants.BROTLI_PARAM_QUALITY]: q,
|
|
24
|
-
},
|
|
25
|
-
}, (err, result) => {
|
|
26
|
-
if (err) reject(err);
|
|
27
|
-
else resolve(result);
|
|
28
|
-
});
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Decompress Brotli data
|
|
34
|
-
* @param {Buffer} buffer - Compressed data
|
|
35
|
-
* @returns {Promise<Buffer>} Decompressed data
|
|
36
|
-
*/
|
|
37
|
-
function decompressBrotli(buffer) {
|
|
38
|
-
return new Promise((resolve, reject) => {
|
|
39
|
-
zlib.brotliDecompress(buffer, (err, result) => {
|
|
40
|
-
if (err) reject(err);
|
|
41
|
-
else resolve(result);
|
|
42
|
-
});
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
|
|
46
12
|
/**
|
|
47
13
|
* Create a streaming Brotli compression transform
|
|
48
14
|
* @param {number} [quality] - Compression quality (0-11, default 6)
|
|
@@ -57,130 +23,4 @@ function createBrotliCompressStream(quality) {
|
|
|
57
23
|
});
|
|
58
24
|
}
|
|
59
25
|
|
|
60
|
-
|
|
61
|
-
* Create a streaming Brotli decompression transform
|
|
62
|
-
* @returns {zlib.BrotliDecompress} Brotli transform stream
|
|
63
|
-
*/
|
|
64
|
-
function createBrotliDecompressStream() {
|
|
65
|
-
return zlib.createBrotliDecompress();
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// ─── Gzip Compression (v3/v4 legacy) ───────────────────────────────────────
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* MIME types that are already compressed - no benefit from additional compression
|
|
72
|
-
*/
|
|
73
|
-
const COMPRESSED_MIMES = new Set([
|
|
74
|
-
// Archives
|
|
75
|
-
'application/zip',
|
|
76
|
-
'application/x-7z-compressed',
|
|
77
|
-
'application/x-rar-compressed',
|
|
78
|
-
'application/gzip',
|
|
79
|
-
'application/x-gzip',
|
|
80
|
-
'application/x-bzip2',
|
|
81
|
-
'application/x-xz',
|
|
82
|
-
'application/x-tar',
|
|
83
|
-
'application/x-lzip',
|
|
84
|
-
'application/x-lzma',
|
|
85
|
-
'application/zstd',
|
|
86
|
-
|
|
87
|
-
// Images (lossy compressed)
|
|
88
|
-
'image/jpeg',
|
|
89
|
-
'image/png',
|
|
90
|
-
'image/gif',
|
|
91
|
-
'image/webp',
|
|
92
|
-
'image/avif',
|
|
93
|
-
'image/heic',
|
|
94
|
-
'image/heif',
|
|
95
|
-
'image/jxl',
|
|
96
|
-
|
|
97
|
-
// Audio
|
|
98
|
-
'audio/mpeg', // mp3
|
|
99
|
-
'audio/ogg',
|
|
100
|
-
'audio/flac',
|
|
101
|
-
'audio/aac',
|
|
102
|
-
'audio/mp4',
|
|
103
|
-
'audio/x-m4a',
|
|
104
|
-
'audio/opus',
|
|
105
|
-
|
|
106
|
-
// Video
|
|
107
|
-
'video/mp4',
|
|
108
|
-
'video/webm',
|
|
109
|
-
'video/x-matroska', // mkv
|
|
110
|
-
'video/quicktime', // mov
|
|
111
|
-
'video/x-msvideo', // avi
|
|
112
|
-
'video/mpeg',
|
|
113
|
-
|
|
114
|
-
// Documents (OOXML are zip-based)
|
|
115
|
-
'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // docx
|
|
116
|
-
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // xlsx
|
|
117
|
-
'application/vnd.openxmlformats-officedocument.presentationml.presentation', // pptx
|
|
118
|
-
'application/pdf',
|
|
119
|
-
'application/epub+zip',
|
|
120
|
-
]);
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Check if a file type is already compressed
|
|
124
|
-
* @param {string|null} mime - MIME type from file-type detection
|
|
125
|
-
* @returns {boolean}
|
|
126
|
-
*/
|
|
127
|
-
function isCompressedMime(mime) {
|
|
128
|
-
return mime ? COMPRESSED_MIMES.has(mime) : false;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Compress data using gzip
|
|
133
|
-
* @param {Buffer} buffer - Data to compress
|
|
134
|
-
* @returns {Promise<Buffer>} Compressed data
|
|
135
|
-
*/
|
|
136
|
-
function compress(buffer) {
|
|
137
|
-
return new Promise((resolve, reject) => {
|
|
138
|
-
zlib.gzip(buffer, { level: 9 }, (err, result) => {
|
|
139
|
-
if (err) reject(err);
|
|
140
|
-
else resolve(result);
|
|
141
|
-
});
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Decompress gzip data
|
|
147
|
-
* @param {Buffer} buffer - Compressed data
|
|
148
|
-
* @returns {Promise<Buffer>} Decompressed data
|
|
149
|
-
*/
|
|
150
|
-
function decompress(buffer) {
|
|
151
|
-
return new Promise((resolve, reject) => {
|
|
152
|
-
zlib.gunzip(buffer, (err, result) => {
|
|
153
|
-
if (err) reject(err);
|
|
154
|
-
else resolve(result);
|
|
155
|
-
});
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Create a streaming gzip compression transform
|
|
161
|
-
* @returns {zlib.Gzip} Gzip transform stream
|
|
162
|
-
*/
|
|
163
|
-
function createCompressStream() {
|
|
164
|
-
return zlib.createGzip({ level: 9 });
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Create a streaming gunzip decompression transform
|
|
169
|
-
* @returns {zlib.Gunzip} Gunzip transform stream
|
|
170
|
-
*/
|
|
171
|
-
function createDecompressStream() {
|
|
172
|
-
return zlib.createGunzip();
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
module.exports = {
|
|
176
|
-
isCompressedMime,
|
|
177
|
-
compress,
|
|
178
|
-
decompress,
|
|
179
|
-
createCompressStream,
|
|
180
|
-
createDecompressStream,
|
|
181
|
-
compressBrotli,
|
|
182
|
-
decompressBrotli,
|
|
183
|
-
createBrotliCompressStream,
|
|
184
|
-
createBrotliDecompressStream,
|
|
185
|
-
COMPRESSED_MIMES,
|
|
186
|
-
};
|
|
26
|
+
module.exports = { createBrotliCompressStream };
|