stegdoc 3.0.2 → 5.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/package.json +2 -2
- package/src/commands/decode.js +485 -215
- package/src/commands/encode.js +567 -346
- package/src/commands/info.js +118 -113
- package/src/commands/verify.js +207 -169
- package/src/index.js +89 -87
- package/src/lib/compression.js +177 -97
- package/src/lib/crypto.js +172 -118
- package/src/lib/decoy-generator.js +306 -306
- package/src/lib/docx-handler.js +587 -161
- package/src/lib/docx-templates.js +355 -0
- package/src/lib/file-handler.js +113 -113
- package/src/lib/file-utils.js +160 -150
- package/src/lib/interactive.js +190 -190
- package/src/lib/log-generator.js +764 -0
- package/src/lib/metadata.js +151 -111
- package/src/lib/streams.js +197 -0
- package/src/lib/utils.js +227 -227
- package/src/lib/xlsx-handler.js +597 -359
- package/src/lib/xml-utils.js +115 -115
package/src/lib/utils.js
CHANGED
|
@@ -1,227 +1,227 @@
|
|
|
1
|
-
const crypto = require('crypto');
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Generate a random hash for file naming
|
|
5
|
-
* @param {number} length - Length of hash in bytes (default 8)
|
|
6
|
-
* @returns {string} Hexadecimal hash string
|
|
7
|
-
*/
|
|
8
|
-
function generateHash(length = 8) {
|
|
9
|
-
return crypto.randomBytes(length).toString('hex');
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Generate SHA-256 hash of a buffer for integrity verification
|
|
14
|
-
* @param {Buffer} buffer - Data to hash
|
|
15
|
-
* @returns {string} Hexadecimal SHA-256 hash
|
|
16
|
-
*/
|
|
17
|
-
function generateContentHash(buffer) {
|
|
18
|
-
return crypto.createHash('sha256').update(buffer).digest('hex');
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Parse size string to bytes
|
|
23
|
-
* @param {string} sizeStr - Size string (e.g., "5MB", "100KB", "1GB")
|
|
24
|
-
* @returns {number} Size in bytes
|
|
25
|
-
*/
|
|
26
|
-
function parseSizeToBytes(sizeStr) {
|
|
27
|
-
if (typeof sizeStr === 'number') {
|
|
28
|
-
return sizeStr;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const units = {
|
|
32
|
-
B: 1,
|
|
33
|
-
KB: 1024,
|
|
34
|
-
MB: 1024 * 1024,
|
|
35
|
-
GB: 1024 * 1024 * 1024,
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
const match = sizeStr.trim().toUpperCase().match(/^(\d+(?:\.\d+)?)\s*(B|KB|MB|GB)?$/);
|
|
39
|
-
|
|
40
|
-
if (!match) {
|
|
41
|
-
throw new Error(`Invalid size format: ${sizeStr}. Use format like "5MB", "100KB", etc.`);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const value = parseFloat(match[1]);
|
|
45
|
-
const unit = match[2] || 'B';
|
|
46
|
-
|
|
47
|
-
return Math.floor(value * units[unit]);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Format bytes to human-readable string
|
|
52
|
-
* @param {number} bytes - Number of bytes
|
|
53
|
-
* @param {number} decimals - Number of decimal places (default 2)
|
|
54
|
-
* @returns {string} Formatted size string
|
|
55
|
-
*/
|
|
56
|
-
function formatBytes(bytes, decimals = 2) {
|
|
57
|
-
if (bytes === 0) return '0 B';
|
|
58
|
-
|
|
59
|
-
const k = 1024;
|
|
60
|
-
const dm = decimals < 0 ? 0 : decimals;
|
|
61
|
-
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
62
|
-
|
|
63
|
-
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
64
|
-
|
|
65
|
-
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Generate a realistic-looking filename that matches the decoy theme
|
|
70
|
-
* Decoy data contains ~4 hours of server logs, so filenames reflect periodic reports
|
|
71
|
-
*
|
|
72
|
-
* @param {string} hash - Hash identifier (used internally for matching parts)
|
|
73
|
-
* @param {number} partNumber - Part number (optional, for split files)
|
|
74
|
-
* @param {number} totalParts - Total number of parts (optional)
|
|
75
|
-
* @param {string} format - Output format ('xlsx' or 'docx'), default 'xlsx'
|
|
76
|
-
* @returns {string} Realistic filename WITH extension
|
|
77
|
-
*/
|
|
78
|
-
function generateFilename(hash, partNumber = null, totalParts = null, format = 'xlsx') {
|
|
79
|
-
const ext = format.toLowerCase() === 'docx' ? 'docx' : 'xlsx';
|
|
80
|
-
|
|
81
|
-
// Generate deterministic timestamp based on hash (so all parts match)
|
|
82
|
-
// Use different parts of hash for day and hour to get better distribution
|
|
83
|
-
const hashNumLow = parseInt(hash.slice(0, 8), 16) >>> 0;
|
|
84
|
-
const hashNumHigh = parseInt(hash.slice(8, 16), 16) >>> 0;
|
|
85
|
-
const daysAgo = hashNumLow % 7; // 0-6 days ago
|
|
86
|
-
const hourBlock = (hashNumHigh % 6) * 4; // 0, 4, 8, 12, 16, 20
|
|
87
|
-
|
|
88
|
-
const now = new Date();
|
|
89
|
-
const date = new Date(now.getTime() - daysAgo * 24 * 60 * 60 * 1000);
|
|
90
|
-
|
|
91
|
-
// Format date in local timezone (YYYYMMDD)
|
|
92
|
-
const year = date.getFullYear();
|
|
93
|
-
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
94
|
-
const day = String(date.getDate()).padStart(2, '0');
|
|
95
|
-
const dateStr = `${year}${month}${day}`;
|
|
96
|
-
const timeStr = String(hourBlock).padStart(2, '0') + '00'; // HH00
|
|
97
|
-
|
|
98
|
-
// Use last 4 chars of hash as a "report ID" to keep files linked
|
|
99
|
-
const reportId = hash.slice(-4).toUpperCase();
|
|
100
|
-
|
|
101
|
-
if (format === 'xlsx') {
|
|
102
|
-
//
|
|
103
|
-
if (partNumber !== null
|
|
104
|
-
return `
|
|
105
|
-
}
|
|
106
|
-
return `
|
|
107
|
-
} else {
|
|
108
|
-
// DOCX - use a different theme
|
|
109
|
-
if (partNumber !== null
|
|
110
|
-
return `system_report_${dateStr}_${timeStr}_${reportId}_part${partNumber}.${ext}`;
|
|
111
|
-
}
|
|
112
|
-
return `system_report_${dateStr}_${timeStr}_${reportId}.${ext}`;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Legacy alias for backward compatibility
|
|
118
|
-
*/
|
|
119
|
-
function generateDocxFilename(hash, partNumber = null, totalParts = null) {
|
|
120
|
-
return generateFilename(hash, partNumber, totalParts, 'docx').replace(/\.docx$/, '');
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Parse filename to extract hash, part information, and format
|
|
125
|
-
* @param {string} filename - Filename to parse
|
|
126
|
-
* @returns {object} Object with hash, partNumber, totalParts, baseHash, format
|
|
127
|
-
*/
|
|
128
|
-
function parseFilename(filename) {
|
|
129
|
-
// Detect format from extension
|
|
130
|
-
let format = 'xlsx';
|
|
131
|
-
let name = filename;
|
|
132
|
-
|
|
133
|
-
if (/\.docx$/i.test(filename)) {
|
|
134
|
-
format = 'docx';
|
|
135
|
-
name = filename.replace(/\.docx$/i, '');
|
|
136
|
-
} else if (/\.xlsx$/i.test(filename)) {
|
|
137
|
-
format = 'xlsx';
|
|
138
|
-
name = filename.replace(/\.xlsx$/i, '');
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// Try new realistic filename format first
|
|
142
|
-
// Pattern: server_metrics_YYYYMMDD_HH00_XXXX[_partN]
|
|
143
|
-
// Or: system_report_YYYYMMDD_HH00_XXXX[_partN]
|
|
144
|
-
const realisticPattern = /^(server_metrics|system_report)_(\d{8})_(\d{4})_([A-F0-9]{4})(?:_part(\d+))?$/i;
|
|
145
|
-
const realisticMatch = name.match(realisticPattern);
|
|
146
|
-
|
|
147
|
-
if (realisticMatch) {
|
|
148
|
-
const [, prefix, dateStr, timeStr, reportId, partStr] = realisticMatch;
|
|
149
|
-
// Create a pseudo-hash from the components for matching purposes
|
|
150
|
-
// The reportId is the last 4 chars of the original hash
|
|
151
|
-
const baseIdentifier = `${dateStr}_${timeStr}_${reportId}`;
|
|
152
|
-
|
|
153
|
-
return {
|
|
154
|
-
hash: name,
|
|
155
|
-
baseHash: baseIdentifier,
|
|
156
|
-
partNumber: partStr ? parseInt(partStr, 10) : null,
|
|
157
|
-
totalParts: null, // Will be determined from metadata
|
|
158
|
-
format,
|
|
159
|
-
// Extra info for matching
|
|
160
|
-
reportId: reportId.toUpperCase(),
|
|
161
|
-
dateStr,
|
|
162
|
-
timeStr,
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// Legacy: Check if it's a hex string (old format)
|
|
167
|
-
if (/^[a-f0-9]+$/i.test(name)) {
|
|
168
|
-
// Could be either single file or multi-part
|
|
169
|
-
// Single files are 16 chars (8 bytes), multi-part are 18+ chars (16 + 2 digit part number)
|
|
170
|
-
|
|
171
|
-
if (name.length === 16) {
|
|
172
|
-
// Single file
|
|
173
|
-
return {
|
|
174
|
-
hash: name,
|
|
175
|
-
baseHash: name,
|
|
176
|
-
partNumber: null,
|
|
177
|
-
totalParts: null,
|
|
178
|
-
format,
|
|
179
|
-
};
|
|
180
|
-
} else if (name.length >= 18) {
|
|
181
|
-
// Multi-part: last 2 chars are the part number
|
|
182
|
-
const baseHash = name.slice(0, -2);
|
|
183
|
-
const partNumber = parseInt(name.slice(-2), 10);
|
|
184
|
-
|
|
185
|
-
return {
|
|
186
|
-
hash: name,
|
|
187
|
-
baseHash: baseHash,
|
|
188
|
-
partNumber: partNumber,
|
|
189
|
-
totalParts: null, // Will be determined from metadata
|
|
190
|
-
format,
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
return null;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Legacy alias for backward compatibility
|
|
200
|
-
*/
|
|
201
|
-
function parseDocxFilename(filename) {
|
|
202
|
-
return parseFilename(filename);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Detect file format from extension
|
|
207
|
-
* @param {string} filename - Filename to check
|
|
208
|
-
* @returns {string} 'xlsx', 'docx', or null if unknown
|
|
209
|
-
*/
|
|
210
|
-
function detectFormat(filename) {
|
|
211
|
-
if (/\.xlsx$/i.test(filename)) return 'xlsx';
|
|
212
|
-
if (/\.docx$/i.test(filename)) return 'docx';
|
|
213
|
-
return null;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
module.exports = {
|
|
217
|
-
generateHash,
|
|
218
|
-
generateContentHash,
|
|
219
|
-
parseSizeToBytes,
|
|
220
|
-
formatBytes,
|
|
221
|
-
generateFilename,
|
|
222
|
-
parseFilename,
|
|
223
|
-
detectFormat,
|
|
224
|
-
// Legacy aliases for backward compatibility
|
|
225
|
-
generateDocxFilename,
|
|
226
|
-
parseDocxFilename,
|
|
227
|
-
};
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generate a random hash for file naming
|
|
5
|
+
* @param {number} length - Length of hash in bytes (default 8)
|
|
6
|
+
* @returns {string} Hexadecimal hash string
|
|
7
|
+
*/
|
|
8
|
+
function generateHash(length = 8) {
|
|
9
|
+
return crypto.randomBytes(length).toString('hex');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Generate SHA-256 hash of a buffer for integrity verification
|
|
14
|
+
* @param {Buffer} buffer - Data to hash
|
|
15
|
+
* @returns {string} Hexadecimal SHA-256 hash
|
|
16
|
+
*/
|
|
17
|
+
function generateContentHash(buffer) {
|
|
18
|
+
return crypto.createHash('sha256').update(buffer).digest('hex');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Parse size string to bytes
|
|
23
|
+
* @param {string} sizeStr - Size string (e.g., "5MB", "100KB", "1GB")
|
|
24
|
+
* @returns {number} Size in bytes
|
|
25
|
+
*/
|
|
26
|
+
function parseSizeToBytes(sizeStr) {
|
|
27
|
+
if (typeof sizeStr === 'number') {
|
|
28
|
+
return sizeStr;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const units = {
|
|
32
|
+
B: 1,
|
|
33
|
+
KB: 1024,
|
|
34
|
+
MB: 1024 * 1024,
|
|
35
|
+
GB: 1024 * 1024 * 1024,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const match = sizeStr.trim().toUpperCase().match(/^(\d+(?:\.\d+)?)\s*(B|KB|MB|GB)?$/);
|
|
39
|
+
|
|
40
|
+
if (!match) {
|
|
41
|
+
throw new Error(`Invalid size format: ${sizeStr}. Use format like "5MB", "100KB", etc.`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const value = parseFloat(match[1]);
|
|
45
|
+
const unit = match[2] || 'B';
|
|
46
|
+
|
|
47
|
+
return Math.floor(value * units[unit]);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Format bytes to human-readable string
|
|
52
|
+
* @param {number} bytes - Number of bytes
|
|
53
|
+
* @param {number} decimals - Number of decimal places (default 2)
|
|
54
|
+
* @returns {string} Formatted size string
|
|
55
|
+
*/
|
|
56
|
+
function formatBytes(bytes, decimals = 2) {
|
|
57
|
+
if (bytes === 0) return '0 B';
|
|
58
|
+
|
|
59
|
+
const k = 1024;
|
|
60
|
+
const dm = decimals < 0 ? 0 : decimals;
|
|
61
|
+
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
62
|
+
|
|
63
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
64
|
+
|
|
65
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Generate a realistic-looking filename that matches the decoy theme
|
|
70
|
+
* Decoy data contains ~4 hours of server logs, so filenames reflect periodic reports
|
|
71
|
+
*
|
|
72
|
+
* @param {string} hash - Hash identifier (used internally for matching parts)
|
|
73
|
+
* @param {number} partNumber - Part number (optional, for split files)
|
|
74
|
+
* @param {number} totalParts - Total number of parts (optional)
|
|
75
|
+
* @param {string} format - Output format ('xlsx' or 'docx'), default 'xlsx'
|
|
76
|
+
* @returns {string} Realistic filename WITH extension
|
|
77
|
+
*/
|
|
78
|
+
function generateFilename(hash, partNumber = null, totalParts = null, format = 'xlsx') {
|
|
79
|
+
const ext = format.toLowerCase() === 'docx' ? 'docx' : 'xlsx';
|
|
80
|
+
|
|
81
|
+
// Generate deterministic timestamp based on hash (so all parts match)
|
|
82
|
+
// Use different parts of hash for day and hour to get better distribution
|
|
83
|
+
const hashNumLow = parseInt(hash.slice(0, 8), 16) >>> 0;
|
|
84
|
+
const hashNumHigh = parseInt(hash.slice(8, 16), 16) >>> 0;
|
|
85
|
+
const daysAgo = hashNumLow % 7; // 0-6 days ago
|
|
86
|
+
const hourBlock = (hashNumHigh % 6) * 4; // 0, 4, 8, 12, 16, 20
|
|
87
|
+
|
|
88
|
+
const now = new Date();
|
|
89
|
+
const date = new Date(now.getTime() - daysAgo * 24 * 60 * 60 * 1000);
|
|
90
|
+
|
|
91
|
+
// Format date in local timezone (YYYYMMDD)
|
|
92
|
+
const year = date.getFullYear();
|
|
93
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
94
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
95
|
+
const dateStr = `${year}${month}${day}`;
|
|
96
|
+
const timeStr = String(hourBlock).padStart(2, '0') + '00'; // HH00
|
|
97
|
+
|
|
98
|
+
// Use last 4 chars of hash as a "report ID" to keep files linked
|
|
99
|
+
const reportId = hash.slice(-4).toUpperCase();
|
|
100
|
+
|
|
101
|
+
if (format === 'xlsx') {
|
|
102
|
+
// Access log theme - looks like periodic nginx log exports
|
|
103
|
+
if (partNumber !== null) {
|
|
104
|
+
return `access_log_${dateStr}_${timeStr}_${reportId}_part${partNumber}.${ext}`;
|
|
105
|
+
}
|
|
106
|
+
return `access_log_${dateStr}_${timeStr}_${reportId}.${ext}`;
|
|
107
|
+
} else {
|
|
108
|
+
// DOCX - use a different theme
|
|
109
|
+
if (partNumber !== null) {
|
|
110
|
+
return `system_report_${dateStr}_${timeStr}_${reportId}_part${partNumber}.${ext}`;
|
|
111
|
+
}
|
|
112
|
+
return `system_report_${dateStr}_${timeStr}_${reportId}.${ext}`;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Legacy alias for backward compatibility
|
|
118
|
+
*/
|
|
119
|
+
function generateDocxFilename(hash, partNumber = null, totalParts = null) {
|
|
120
|
+
return generateFilename(hash, partNumber, totalParts, 'docx').replace(/\.docx$/, '');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Parse filename to extract hash, part information, and format
|
|
125
|
+
* @param {string} filename - Filename to parse
|
|
126
|
+
* @returns {object} Object with hash, partNumber, totalParts, baseHash, format
|
|
127
|
+
*/
|
|
128
|
+
function parseFilename(filename) {
|
|
129
|
+
// Detect format from extension
|
|
130
|
+
let format = 'xlsx';
|
|
131
|
+
let name = filename;
|
|
132
|
+
|
|
133
|
+
if (/\.docx$/i.test(filename)) {
|
|
134
|
+
format = 'docx';
|
|
135
|
+
name = filename.replace(/\.docx$/i, '');
|
|
136
|
+
} else if (/\.xlsx$/i.test(filename)) {
|
|
137
|
+
format = 'xlsx';
|
|
138
|
+
name = filename.replace(/\.xlsx$/i, '');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Try new realistic filename format first
|
|
142
|
+
// Pattern: server_metrics_YYYYMMDD_HH00_XXXX[_partN]
|
|
143
|
+
// Or: system_report_YYYYMMDD_HH00_XXXX[_partN]
|
|
144
|
+
const realisticPattern = /^(server_metrics|system_report|access_log)_(\d{8})_(\d{4})_([A-F0-9]{4})(?:_part(\d+))?$/i;
|
|
145
|
+
const realisticMatch = name.match(realisticPattern);
|
|
146
|
+
|
|
147
|
+
if (realisticMatch) {
|
|
148
|
+
const [, prefix, dateStr, timeStr, reportId, partStr] = realisticMatch;
|
|
149
|
+
// Create a pseudo-hash from the components for matching purposes
|
|
150
|
+
// The reportId is the last 4 chars of the original hash
|
|
151
|
+
const baseIdentifier = `${dateStr}_${timeStr}_${reportId}`;
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
hash: name,
|
|
155
|
+
baseHash: baseIdentifier,
|
|
156
|
+
partNumber: partStr ? parseInt(partStr, 10) : null,
|
|
157
|
+
totalParts: null, // Will be determined from metadata
|
|
158
|
+
format,
|
|
159
|
+
// Extra info for matching
|
|
160
|
+
reportId: reportId.toUpperCase(),
|
|
161
|
+
dateStr,
|
|
162
|
+
timeStr,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Legacy: Check if it's a hex string (old format)
|
|
167
|
+
if (/^[a-f0-9]+$/i.test(name)) {
|
|
168
|
+
// Could be either single file or multi-part
|
|
169
|
+
// Single files are 16 chars (8 bytes), multi-part are 18+ chars (16 + 2 digit part number)
|
|
170
|
+
|
|
171
|
+
if (name.length === 16) {
|
|
172
|
+
// Single file
|
|
173
|
+
return {
|
|
174
|
+
hash: name,
|
|
175
|
+
baseHash: name,
|
|
176
|
+
partNumber: null,
|
|
177
|
+
totalParts: null,
|
|
178
|
+
format,
|
|
179
|
+
};
|
|
180
|
+
} else if (name.length >= 18) {
|
|
181
|
+
// Multi-part: last 2 chars are the part number
|
|
182
|
+
const baseHash = name.slice(0, -2);
|
|
183
|
+
const partNumber = parseInt(name.slice(-2), 10);
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
hash: name,
|
|
187
|
+
baseHash: baseHash,
|
|
188
|
+
partNumber: partNumber,
|
|
189
|
+
totalParts: null, // Will be determined from metadata
|
|
190
|
+
format,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Legacy alias for backward compatibility
|
|
200
|
+
*/
|
|
201
|
+
function parseDocxFilename(filename) {
|
|
202
|
+
return parseFilename(filename);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Detect file format from extension
|
|
207
|
+
* @param {string} filename - Filename to check
|
|
208
|
+
* @returns {string} 'xlsx', 'docx', or null if unknown
|
|
209
|
+
*/
|
|
210
|
+
function detectFormat(filename) {
|
|
211
|
+
if (/\.xlsx$/i.test(filename)) return 'xlsx';
|
|
212
|
+
if (/\.docx$/i.test(filename)) return 'docx';
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
module.exports = {
|
|
217
|
+
generateHash,
|
|
218
|
+
generateContentHash,
|
|
219
|
+
parseSizeToBytes,
|
|
220
|
+
formatBytes,
|
|
221
|
+
generateFilename,
|
|
222
|
+
parseFilename,
|
|
223
|
+
detectFormat,
|
|
224
|
+
// Legacy aliases for backward compatibility
|
|
225
|
+
generateDocxFilename,
|
|
226
|
+
parseDocxFilename,
|
|
227
|
+
};
|