stegdoc 5.7.0 → 6.0.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 +231 -106
- package/package.json +25 -2
- package/src/commands/decode.js +97 -149
- package/src/commands/encode.js +150 -9
- package/src/commands/info.js +49 -0
- package/src/commands/verify.js +66 -0
- package/src/index.js +9 -5
- package/src/lib/bindings.js +99 -0
- package/src/lib/log-generator.js +5 -2
- package/src/lib/native.js +68 -0
- package/src/lib/streams.js +0 -107
- package/src/lib/utils.js +20 -0
- package/src/lib/xlsx-writer.js +10 -0
- package/src/lib/file-handler.js +0 -113
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,27 @@ 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
27
|
.option('--legacy', 'Use v4 format (hidden sheet + gzip) for backward compatibility')
|
|
28
|
+
.option('--v5', 'Emit the v5 format (PBKDF2) instead of the v6 default')
|
|
29
|
+
.option('--v6', 'Emit the v6 format (default; Argon2id, authenticated metadata)')
|
|
26
30
|
.option('--no-limit', 'Bypass DOCX 1 MB size limit (large files will produce huge documents)')
|
|
27
31
|
.option('-q, --quiet', 'Minimal output (for scripting)')
|
|
28
32
|
.option('-y, --yes', 'Skip interactive prompts, use defaults')
|
|
29
|
-
.action(async (
|
|
33
|
+
.action(async (inputs, options) => {
|
|
30
34
|
try {
|
|
31
|
-
await encodeCommand(
|
|
35
|
+
await encodeCommand(inputs, options);
|
|
32
36
|
} catch (error) {
|
|
33
37
|
console.error(chalk.red(`Error: ${error.message}`));
|
|
34
38
|
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/log-generator.js
CHANGED
|
@@ -657,9 +657,12 @@ function decodeLogLines(allRows) {
|
|
|
657
657
|
const headerStr = headerPayload.toString('utf8');
|
|
658
658
|
|
|
659
659
|
// Parse header: "STGD05|<metaLen>|<encLen>|{metadataJson}{encryptionMeta}"
|
|
660
|
-
const markerIdx = headerStr.
|
|
660
|
+
const markerIdx = headerStr.search(/STGD0[56]\|/);
|
|
661
661
|
if (markerIdx === -1) {
|
|
662
|
-
throw new Error('Invalid
|
|
662
|
+
throw new Error('Invalid log-embed format: magic marker not found. This may not be a stegdoc file.');
|
|
663
|
+
}
|
|
664
|
+
if (headerStr.startsWith('STGD06|', markerIdx)) {
|
|
665
|
+
throw new Error('This file uses the v6 format, which the JavaScript decoder cannot read. The native engine is required.');
|
|
663
666
|
}
|
|
664
667
|
|
|
665
668
|
const afterMarker = headerStr.slice(markerIdx + 7);
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
const { hostTarget, packageName } = require('./bindings');
|
|
6
|
+
|
|
7
|
+
const NATIVE_DIR = path.join(__dirname, '..', '..', 'native');
|
|
8
|
+
|
|
9
|
+
let binding;
|
|
10
|
+
let resolved = false;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Load the native format engine, or return null when it is not available.
|
|
14
|
+
*
|
|
15
|
+
* Resolution order:
|
|
16
|
+
* 1. `STEGDOC_NATIVE_PATH`, an explicit `.node` file.
|
|
17
|
+
* 2. The platform package (`@stegdoc/binding-<suffix>`) from the published
|
|
18
|
+
* install's optionalDependencies.
|
|
19
|
+
* 3. `native/stegdoc.node`, the local `pnpm build:native` output.
|
|
20
|
+
*
|
|
21
|
+
* The binding is required for log-embed (v5/v6) decode and is the preferred
|
|
22
|
+
* encoder. The CommonJS implementation remains only for the legacy v3/v4
|
|
23
|
+
* formats, which the native engine does not cover. Set
|
|
24
|
+
* `STEGDOC_DISABLE_NATIVE=1` to force the JS paths.
|
|
25
|
+
*
|
|
26
|
+
* @returns {object|null} The binding, or null when unavailable.
|
|
27
|
+
*/
|
|
28
|
+
function loadNative() {
|
|
29
|
+
if (resolved) return binding;
|
|
30
|
+
resolved = true;
|
|
31
|
+
|
|
32
|
+
if (process.env.STEGDOC_DISABLE_NATIVE === '1') {
|
|
33
|
+
binding = null;
|
|
34
|
+
return binding;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const explicit = process.env.STEGDOC_NATIVE_PATH;
|
|
38
|
+
if (explicit) {
|
|
39
|
+
binding = require(explicit);
|
|
40
|
+
return binding;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const target = hostTarget();
|
|
44
|
+
if (target) {
|
|
45
|
+
try {
|
|
46
|
+
binding = require(packageName(target));
|
|
47
|
+
return binding;
|
|
48
|
+
} catch {
|
|
49
|
+
// Not installed; fall through to the development build.
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
binding = require(path.join(NATIVE_DIR, 'stegdoc.node'));
|
|
55
|
+
} catch {
|
|
56
|
+
binding = null;
|
|
57
|
+
}
|
|
58
|
+
return binding;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @returns {boolean} Whether the native engine is loaded.
|
|
63
|
+
*/
|
|
64
|
+
function nativeAvailable() {
|
|
65
|
+
return loadNative() !== null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
module.exports = { loadNative, nativeAvailable };
|
package/src/lib/streams.js
CHANGED
|
@@ -1,65 +1,6 @@
|
|
|
1
1
|
const { Transform, Writable } = require('stream');
|
|
2
2
|
const crypto = require('crypto');
|
|
3
3
|
|
|
4
|
-
/**
|
|
5
|
-
* Transform stream that converts binary input to base64 text output.
|
|
6
|
-
* Buffers incomplete 3-byte groups across chunk boundaries.
|
|
7
|
-
*/
|
|
8
|
-
class Base64EncodeTransform extends Transform {
|
|
9
|
-
constructor() {
|
|
10
|
-
super();
|
|
11
|
-
this._remainder = Buffer.alloc(0);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
_transform(chunk, encoding, callback) {
|
|
15
|
-
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding);
|
|
16
|
-
const combined = this._remainder.length > 0 ? Buffer.concat([this._remainder, buf]) : buf;
|
|
17
|
-
const usable = combined.length - (combined.length % 3);
|
|
18
|
-
if (usable > 0) {
|
|
19
|
-
this.push(combined.slice(0, usable).toString('base64'));
|
|
20
|
-
}
|
|
21
|
-
this._remainder = usable < combined.length ? combined.slice(usable) : Buffer.alloc(0);
|
|
22
|
-
callback();
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
_flush(callback) {
|
|
26
|
-
if (this._remainder.length > 0) {
|
|
27
|
-
this.push(this._remainder.toString('base64'));
|
|
28
|
-
this._remainder = Buffer.alloc(0);
|
|
29
|
-
}
|
|
30
|
-
callback();
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Transform stream that converts base64 text input to binary output.
|
|
36
|
-
* Buffers incomplete 4-char groups across chunk boundaries.
|
|
37
|
-
*/
|
|
38
|
-
class Base64DecodeTransform extends Transform {
|
|
39
|
-
constructor() {
|
|
40
|
-
super();
|
|
41
|
-
this._remainder = '';
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
_transform(chunk, encoding, callback) {
|
|
45
|
-
const str = this._remainder + (Buffer.isBuffer(chunk) ? chunk.toString() : chunk);
|
|
46
|
-
const usable = str.length - (str.length % 4);
|
|
47
|
-
if (usable > 0) {
|
|
48
|
-
this.push(Buffer.from(str.slice(0, usable), 'base64'));
|
|
49
|
-
}
|
|
50
|
-
this._remainder = usable < str.length ? str.slice(usable) : '';
|
|
51
|
-
callback();
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
_flush(callback) {
|
|
55
|
-
if (this._remainder.length > 0) {
|
|
56
|
-
this.push(Buffer.from(this._remainder, 'base64'));
|
|
57
|
-
this._remainder = '';
|
|
58
|
-
}
|
|
59
|
-
callback();
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
4
|
/**
|
|
64
5
|
* Transform stream that passes data through unchanged while computing SHA-256 hash.
|
|
65
6
|
* Access the hex hash via .digest after the stream has ended.
|
|
@@ -90,51 +31,6 @@ class HashPassthrough extends Transform {
|
|
|
90
31
|
}
|
|
91
32
|
}
|
|
92
33
|
|
|
93
|
-
/**
|
|
94
|
-
* Writable stream that collects string output up to maxBytes.
|
|
95
|
-
* Calls an async onChunkReady callback when a chunk is full, applying
|
|
96
|
-
* backpressure to pause upstream until the callback resolves.
|
|
97
|
-
*/
|
|
98
|
-
class ChunkCollector extends Writable {
|
|
99
|
-
constructor(maxBytes, onChunkReady) {
|
|
100
|
-
super({ decodeStrings: false });
|
|
101
|
-
this._maxBytes = maxBytes;
|
|
102
|
-
this._buffer = '';
|
|
103
|
-
this._chunkIndex = 0;
|
|
104
|
-
this._onChunkReady = onChunkReady;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
async _write(chunk, encoding, callback) {
|
|
108
|
-
try {
|
|
109
|
-
this._buffer += typeof chunk === 'string' ? chunk : chunk.toString();
|
|
110
|
-
while (this._buffer.length >= this._maxBytes) {
|
|
111
|
-
const piece = this._buffer.slice(0, this._maxBytes);
|
|
112
|
-
this._buffer = this._buffer.slice(this._maxBytes);
|
|
113
|
-
await this._onChunkReady(piece, this._chunkIndex++);
|
|
114
|
-
}
|
|
115
|
-
callback();
|
|
116
|
-
} catch (err) {
|
|
117
|
-
callback(err);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
async _final(callback) {
|
|
122
|
-
try {
|
|
123
|
-
if (this._buffer.length > 0) {
|
|
124
|
-
await this._onChunkReady(this._buffer, this._chunkIndex++);
|
|
125
|
-
this._buffer = '';
|
|
126
|
-
}
|
|
127
|
-
callback();
|
|
128
|
-
} catch (err) {
|
|
129
|
-
callback(err);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
get totalChunks() {
|
|
134
|
-
return this._chunkIndex;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
34
|
/**
|
|
139
35
|
* Writable stream that collects binary Buffer output up to maxBytes.
|
|
140
36
|
* Calls an async onChunkReady callback with a Buffer when full.
|
|
@@ -221,10 +117,7 @@ class ProgressTransform extends Transform {
|
|
|
221
117
|
}
|
|
222
118
|
|
|
223
119
|
module.exports = {
|
|
224
|
-
Base64EncodeTransform,
|
|
225
|
-
Base64DecodeTransform,
|
|
226
120
|
HashPassthrough,
|
|
227
|
-
ChunkCollector,
|
|
228
121
|
BinaryChunkCollector,
|
|
229
122
|
ProgressTransform,
|
|
230
123
|
};
|
package/src/lib/utils.js
CHANGED
|
@@ -213,6 +213,25 @@ function detectFormat(filename) {
|
|
|
213
213
|
return null;
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
+
/**
|
|
217
|
+
* Reduce a metadata-supplied name to a bare, safe filename.
|
|
218
|
+
*
|
|
219
|
+
* `originalFilename` is unauthenticated, so it is never joined onto a directory
|
|
220
|
+
* unchecked (FORMAT.md 11.1). Any directory component is discarded and the
|
|
221
|
+
* remainder must not be empty, `.`, `..`, or contain NUL.
|
|
222
|
+
*
|
|
223
|
+
* @param {string} name - Metadata-supplied filename
|
|
224
|
+
* @returns {string} A bare filename safe to join onto a directory
|
|
225
|
+
* @throws {Error} If the name has no usable filename component
|
|
226
|
+
*/
|
|
227
|
+
function safeFilename(name) {
|
|
228
|
+
const last = String(name == null ? '' : name).split(/[/\\]/).pop();
|
|
229
|
+
if (!last || last === '.' || last === '..' || last.includes('\0')) {
|
|
230
|
+
throw new Error(`Unsafe output filename in metadata: ${JSON.stringify(name)}`);
|
|
231
|
+
}
|
|
232
|
+
return last;
|
|
233
|
+
}
|
|
234
|
+
|
|
216
235
|
module.exports = {
|
|
217
236
|
generateHash,
|
|
218
237
|
generateContentHash,
|
|
@@ -221,6 +240,7 @@ module.exports = {
|
|
|
221
240
|
generateFilename,
|
|
222
241
|
parseFilename,
|
|
223
242
|
detectFormat,
|
|
243
|
+
safeFilename,
|
|
224
244
|
// Legacy aliases for backward compatibility
|
|
225
245
|
generateDocxFilename,
|
|
226
246
|
parseDocxFilename,
|
package/src/lib/xlsx-writer.js
CHANGED
|
@@ -295,4 +295,14 @@ module.exports = {
|
|
|
295
295
|
buildSheetXml,
|
|
296
296
|
escapeXml,
|
|
297
297
|
colLetter,
|
|
298
|
+
// Raw OOXML parts, exported so spec/tools/generate-xlsx-static.js can emit
|
|
299
|
+
// the Rust writer's constants from this frozen implementation.
|
|
300
|
+
CONTENT_TYPES,
|
|
301
|
+
RELS,
|
|
302
|
+
WORKBOOK_RELS,
|
|
303
|
+
STYLES,
|
|
304
|
+
THEME,
|
|
305
|
+
makeWorkbook,
|
|
306
|
+
makeCoreProps,
|
|
307
|
+
makeAppProps,
|
|
298
308
|
};
|
package/src/lib/file-handler.js
DELETED
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Read a file and encode it to base64
|
|
6
|
-
* @param {string} filePath - Path to the file
|
|
7
|
-
* @returns {object} Object containing base64 string, filename, extension, and size
|
|
8
|
-
*/
|
|
9
|
-
function encodeFileToBase64(filePath) {
|
|
10
|
-
if (!fs.existsSync(filePath)) {
|
|
11
|
-
throw new Error(`File not found: ${filePath}`);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const stats = fs.statSync(filePath);
|
|
15
|
-
|
|
16
|
-
if (!stats.isFile()) {
|
|
17
|
-
throw new Error(`Path is not a file: ${filePath}`);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const fileBuffer = fs.readFileSync(filePath);
|
|
21
|
-
const base64 = fileBuffer.toString('base64');
|
|
22
|
-
const filename = path.basename(filePath);
|
|
23
|
-
const extension = path.extname(filePath);
|
|
24
|
-
|
|
25
|
-
return {
|
|
26
|
-
base64,
|
|
27
|
-
filename,
|
|
28
|
-
extension,
|
|
29
|
-
size: stats.size,
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Decode base64 string and write to file
|
|
35
|
-
* @param {string} base64 - Base64 encoded string
|
|
36
|
-
* @param {string} outputPath - Output file path
|
|
37
|
-
*/
|
|
38
|
-
function decodeBase64ToFile(base64, outputPath) {
|
|
39
|
-
const buffer = Buffer.from(base64, 'base64');
|
|
40
|
-
|
|
41
|
-
// Ensure output directory exists
|
|
42
|
-
const outputDir = path.dirname(outputPath);
|
|
43
|
-
if (!fs.existsSync(outputDir)) {
|
|
44
|
-
fs.mkdirSync(outputDir, { recursive: true });
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
fs.writeFileSync(outputPath, buffer);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Split base64 string into chunks based on size limit
|
|
52
|
-
* @param {string} base64 - Base64 string to split
|
|
53
|
-
* @param {number} chunkSizeBytes - Maximum size per chunk in bytes
|
|
54
|
-
* @returns {Array<string>} Array of base64 chunks
|
|
55
|
-
*/
|
|
56
|
-
function splitBase64(base64, chunkSizeBytes) {
|
|
57
|
-
const chunks = [];
|
|
58
|
-
let offset = 0;
|
|
59
|
-
|
|
60
|
-
while (offset < base64.length) {
|
|
61
|
-
chunks.push(base64.slice(offset, offset + chunkSizeBytes));
|
|
62
|
-
offset += chunkSizeBytes;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return chunks;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Merge base64 chunks back into a single string
|
|
70
|
-
* @param {Array<string>} chunks - Array of base64 chunks
|
|
71
|
-
* @returns {string} Merged base64 string
|
|
72
|
-
*/
|
|
73
|
-
function mergeBase64Chunks(chunks) {
|
|
74
|
-
return chunks.join('');
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Calculate how many chunks will be needed for a file
|
|
79
|
-
* @param {number} fileSize - File size in bytes
|
|
80
|
-
* @param {number} chunkSizeBytes - Chunk size in bytes
|
|
81
|
-
* @returns {number} Number of chunks needed
|
|
82
|
-
*/
|
|
83
|
-
function calculateChunkCount(fileSize, chunkSizeBytes) {
|
|
84
|
-
// Base64 encoding increases size by ~33%
|
|
85
|
-
const base64Size = Math.ceil(fileSize * 4 / 3);
|
|
86
|
-
return Math.ceil(base64Size / chunkSizeBytes);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Validate if a path is writable
|
|
91
|
-
* @param {string} dirPath - Directory path to check
|
|
92
|
-
* @returns {boolean} True if writable
|
|
93
|
-
*/
|
|
94
|
-
function isDirectoryWritable(dirPath) {
|
|
95
|
-
try {
|
|
96
|
-
if (!fs.existsSync(dirPath)) {
|
|
97
|
-
fs.mkdirSync(dirPath, { recursive: true });
|
|
98
|
-
}
|
|
99
|
-
fs.accessSync(dirPath, fs.constants.W_OK);
|
|
100
|
-
return true;
|
|
101
|
-
} catch (error) {
|
|
102
|
-
return false;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
module.exports = {
|
|
107
|
-
encodeFileToBase64,
|
|
108
|
-
decodeBase64ToFile,
|
|
109
|
-
splitBase64,
|
|
110
|
-
mergeBase64Chunks,
|
|
111
|
-
calculateChunkCount,
|
|
112
|
-
isDirectoryWritable,
|
|
113
|
-
};
|