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/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
- // Server metrics theme - looks like periodic 4-hour monitoring exports
103
- if (partNumber !== null && totalParts !== null) {
104
- return `server_metrics_${dateStr}_${timeStr}_${reportId}_part${partNumber}.${ext}`;
105
- }
106
- return `server_metrics_${dateStr}_${timeStr}_${reportId}.${ext}`;
107
- } else {
108
- // DOCX - use a different theme
109
- if (partNumber !== null && totalParts !== 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
+ };