neozipkit 0.1.1
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 +400 -0
- package/contracts/Lock.sol +34 -0
- package/contracts/bytecode.txt +1 -0
- package/contracts/compile-contract.js +168 -0
- package/contracts/deploy/deploy-both-contracts.js +286 -0
- package/contracts/deploy/deploy-enhanced-contract.js +197 -0
- package/contracts/deploy/deploy-private-contract.js +190 -0
- package/contracts/deploy/deploy-public-clean.js +1063 -0
- package/contracts/deploy/deploy-public-contract.js +1107 -0
- package/contracts/deployment.config.example.js +25 -0
- package/contracts/remix-compile.js +222 -0
- package/contracts/setup-deployment.js +51 -0
- package/contracts/src/NZIP-NFT-Private.sol +350 -0
- package/contracts/src/NZIP-NFT.sol +265 -0
- package/contracts/src/ZipFileNFT-DEPRECATED-6field.sol +260 -0
- package/contracts/src/ZipFileNFT-DEPRECATED-Enhanced.sol +289 -0
- package/dist/browser/CertificateGeneratorBrowser.d.ts +75 -0
- package/dist/browser/CertificateGeneratorBrowser.js +433 -0
- package/dist/browser/TokenVerifierBrowser.d.ts +76 -0
- package/dist/browser/TokenVerifierBrowser.js +299 -0
- package/dist/browser/ZipKitBrowser.d.ts +18 -0
- package/dist/browser/ZipKitBrowser.js +250 -0
- package/dist/browser/ZipKitStampBrowser.d.ts +2 -0
- package/dist/browser/ZipKitStampBrowser.js +12 -0
- package/dist/browser-index.d.ts +21 -0
- package/dist/browser-index.js +58 -0
- package/dist/browser.d.ts +21 -0
- package/dist/browser.js +58 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +76 -0
- package/dist/server/blockchain/CertificateToken.d.ts +48 -0
- package/dist/server/blockchain/CertificateToken.js +386 -0
- package/dist/server/blockchain/NFTMinter.d.ts +41 -0
- package/dist/server/blockchain/NFTMinter.js +323 -0
- package/dist/server/blockchain/TokenQuerier.d.ts +31 -0
- package/dist/server/blockchain/TokenQuerier.js +259 -0
- package/dist/server/blockchain/TokenVerifier.d.ts +20 -0
- package/dist/server/blockchain/TokenVerifier.js +395 -0
- package/dist/server/blockchain/WalletAnalyzer.d.ts +81 -0
- package/dist/server/blockchain/WalletAnalyzer.js +356 -0
- package/dist/server/blockchain/WalletFormatter.d.ts +39 -0
- package/dist/server/blockchain/WalletFormatter.js +196 -0
- package/dist/server/blockchain/WalletManager.d.ts +46 -0
- package/dist/server/blockchain/WalletManager.js +294 -0
- package/dist/server/blockchain/ZipMetadata.d.ts +27 -0
- package/dist/server/blockchain/ZipMetadata.js +131 -0
- package/dist/server/blockchain/ZipTokenProcessor.d.ts +44 -0
- package/dist/server/blockchain/ZipTokenProcessor.js +204 -0
- package/dist/server/blockchain/constants.d.ts +102 -0
- package/dist/server/blockchain/constants.js +968 -0
- package/dist/server/blockchain/index.d.ts +9 -0
- package/dist/server/blockchain/index.js +44 -0
- package/dist/server/blockchain/test-certificate.d.ts +8 -0
- package/dist/server/blockchain/test-certificate.js +100 -0
- package/dist/server/blockchain/types.d.ts +69 -0
- package/dist/server/blockchain/types.js +51 -0
- package/dist/server/platform/server/ZipKitServer.d.ts +105 -0
- package/dist/server/platform/server/ZipKitServer.js +437 -0
- package/dist/server/platform/servercomponents/ZipKitOTS.d.ts +91 -0
- package/dist/server/platform/servercomponents/ZipKitOTS.js +271 -0
- package/dist/shared/ZipEntry.d.ts +131 -0
- package/dist/shared/ZipEntry.js +686 -0
- package/dist/shared/ZipKit.d.ts +249 -0
- package/dist/shared/ZipKit.js +789 -0
- package/dist/shared/components/CertificateOTS.d.ts +15 -0
- package/dist/shared/components/CertificateOTS.js +116 -0
- package/dist/shared/components/FileSys.d.ts +2 -0
- package/dist/shared/components/FileSys.js +113 -0
- package/dist/shared/components/HashAccumulator.d.ts +55 -0
- package/dist/shared/components/HashAccumulator.js +100 -0
- package/dist/shared/components/Support.d.ts +63 -0
- package/dist/shared/components/Support.js +70 -0
- package/dist/shared/components/Util.d.ts +21 -0
- package/dist/shared/components/Util.js +115 -0
- package/dist/shared/components/ZipCrypto.d.ts +3 -0
- package/dist/shared/components/ZipCrypto.js +78 -0
- package/dist/shared/components/merkleTree.d.ts +66 -0
- package/dist/shared/components/merkleTree.js +136 -0
- package/dist/shared/constants/Errors.d.ts +53 -0
- package/dist/shared/constants/Errors.js +69 -0
- package/dist/shared/constants/Headers.d.ts +167 -0
- package/dist/shared/constants/Headers.js +191 -0
- package/dist/shared/types/index.d.ts +21 -0
- package/dist/shared/types/index.js +3 -0
- package/dist/shared/types.d.ts +33 -0
- package/dist/shared/types.js +8 -0
- package/dist/shared/version.d.ts +4 -0
- package/dist/shared/version.js +7 -0
- package/dist/types/index.d.ts +21 -0
- package/dist/types/index.js +3 -0
- package/examples/unzip-token.ts +488 -0
- package/examples/zip-token.ts +604 -0
- package/package.json +117 -0
- package/src/browser/CertificateGeneratorBrowser.ts +498 -0
- package/src/browser/TokenVerifierBrowser.ts +344 -0
- package/src/browser/ZipKitBrowser.ts +242 -0
- package/src/browser/ZipKitStampBrowser.ts +9 -0
- package/src/browser-index.ts +36 -0
- package/src/index.ts +48 -0
- package/src/server/blockchain/CertificateToken.ts +444 -0
- package/src/server/blockchain/NFTMinter.ts +357 -0
- package/src/server/blockchain/TokenQuerier.ts +287 -0
- package/src/server/blockchain/TokenVerifier.ts +453 -0
- package/src/server/blockchain/WalletAnalyzer.ts +397 -0
- package/src/server/blockchain/WalletFormatter.ts +206 -0
- package/src/server/blockchain/WalletManager.ts +294 -0
- package/src/server/blockchain/ZipMetadata.ts +117 -0
- package/src/server/blockchain/ZipTokenProcessor.ts +234 -0
- package/src/server/blockchain/constants.ts +969 -0
- package/src/server/blockchain/index.ts +42 -0
- package/src/server/blockchain/test-certificate.ts +137 -0
- package/src/server/blockchain/types.ts +134 -0
- package/src/server/platform/server/ZipKitServer.ts +459 -0
- package/src/server/platform/servercomponents/ZipKitOTS.ts +313 -0
- package/src/server/platform/servercomponents/opentimestamps.d.ts +23 -0
- package/src/shared/ZipEntry.ts +767 -0
- package/src/shared/ZipKit.ts +899 -0
- package/src/shared/components/CertificateOTS.ts +132 -0
- package/src/shared/components/FileSys.ts +87 -0
- package/src/shared/components/HashAccumulator.ts +113 -0
- package/src/shared/components/Support.ts +77 -0
- package/src/shared/components/Util.ts +113 -0
- package/src/shared/components/ZipCrypto.ts +91 -0
- package/src/shared/components/merkleTree.ts +162 -0
- package/src/shared/constants/Errors.ts +82 -0
- package/src/shared/constants/Headers.ts +203 -0
- package/src/shared/types/index.ts +25 -0
- package/src/shared/types/modules.d.ts +22 -0
- package/src/shared/types/opentimestamps.d.ts +23 -0
- package/src/shared/types.d.ts +8 -0
- package/src/shared/types.ts +43 -0
- package/src/shared/version.ts +4 -0
- package/src/types/index.ts +25 -0
- package/src/types/modules.d.ts +22 -0
- package/src/types/opentimestamps.d.ts +23 -0
|
@@ -0,0 +1,767 @@
|
|
|
1
|
+
// ======================================
|
|
2
|
+
// ZipEntry.ts
|
|
3
|
+
// Copyright (c) 2025 NeoWare, Inc. All rights reserved.
|
|
4
|
+
// ======================================
|
|
5
|
+
// Zip Directory Item class
|
|
6
|
+
|
|
7
|
+
import Errors from './constants/Errors';
|
|
8
|
+
import {
|
|
9
|
+
LOCAL_HDR,
|
|
10
|
+
CENTRAL_DIR,
|
|
11
|
+
CMP_METHOD,
|
|
12
|
+
GP_FLAG,
|
|
13
|
+
FILE_SYSTEM,
|
|
14
|
+
HDR_ID,
|
|
15
|
+
DOS_FILE_ATTR,
|
|
16
|
+
TIMESTAMP_SUBMITTED,
|
|
17
|
+
} from './constants/Headers';
|
|
18
|
+
import { getuid, getgid } from 'process';
|
|
19
|
+
import { ZipFileEntry, FileData } from './types';
|
|
20
|
+
import HashAccumulater from './components/HashAccumulator';
|
|
21
|
+
import { crc32 } from './components/Util';
|
|
22
|
+
|
|
23
|
+
const VER_ENCODING = 30;
|
|
24
|
+
const VER_EXTRACT = 10; // Version needed to extract (1.0)
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Class representing a single entry (file or directory) within a ZIP archive
|
|
28
|
+
*/
|
|
29
|
+
export default class ZipEntry implements ZipFileEntry {
|
|
30
|
+
debug = true;
|
|
31
|
+
|
|
32
|
+
verMadeBy: number = 0; // Read Version Made By
|
|
33
|
+
verExtract: number = 0; // Read Version Needed to Extract
|
|
34
|
+
bitFlags: number = 0; // General purpose bit flag
|
|
35
|
+
cmpMethod: number = 0; // Compression method
|
|
36
|
+
timeDateDOS: number = 0; // DOS File Time(2 bytes) & Date(2 bytes)
|
|
37
|
+
crc: number = 0; // CRC-32
|
|
38
|
+
compressedSize: number = 0; // Compressed size
|
|
39
|
+
uncompressedSize: number = 0; // Uncompressed size
|
|
40
|
+
volNumber: number = 0; // Disk number start
|
|
41
|
+
intFileAttr: number = 0; // Internal file attributes
|
|
42
|
+
extFileAttr: number = 0; // External file attributes
|
|
43
|
+
localHdrOffset: number = 0; // Relative offset to local header from File/Disk start
|
|
44
|
+
filename: string = ''; // File name
|
|
45
|
+
extraField: Buffer | null = null; // Extra field
|
|
46
|
+
comment: string | null = null; // Entry comment
|
|
47
|
+
|
|
48
|
+
// File Data
|
|
49
|
+
fileBuffer: Buffer | null = null; // File Data Buffer
|
|
50
|
+
|
|
51
|
+
// Zip Compressed Data
|
|
52
|
+
cmpData: Buffer | null = null; // Compressed Data Buffer
|
|
53
|
+
|
|
54
|
+
isEncrypted: boolean = false; // Zip Entry is encrypted
|
|
55
|
+
isStrongEncrypt: boolean = false; // Zip Entry is strong encrypted
|
|
56
|
+
encryptHdr: Buffer | null = null; // Encrypted Header (12 bytes)
|
|
57
|
+
lastModTimeDate: number = 0; // Data Descriptor File Time & Date
|
|
58
|
+
decrypt: Function | null = null; // Decrypt Class Function
|
|
59
|
+
|
|
60
|
+
isUpdated: boolean = true; // Entry has been updated
|
|
61
|
+
isDirectory: boolean = false; // Entry is a directory
|
|
62
|
+
isMetaData: boolean = false; // Entry is Zip MetaData
|
|
63
|
+
|
|
64
|
+
// Platform specific data
|
|
65
|
+
platform: string | null = null; // Platform
|
|
66
|
+
universalTime: number | null = null; // Universal Time
|
|
67
|
+
uid: number | null = null; // User ID
|
|
68
|
+
gid: number | null = null; // Group ID
|
|
69
|
+
sha256: string | null = null; // SHA-256 hash of the file
|
|
70
|
+
|
|
71
|
+
fileData?: FileData;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Creates a new ZIP entry
|
|
75
|
+
* @param fname - Name of the file within the ZIP
|
|
76
|
+
* @param comment - Optional comment for this entry
|
|
77
|
+
* @param debug - Enable debug logging
|
|
78
|
+
*/
|
|
79
|
+
constructor(fname: string | null, comment?: string | null, debug?: boolean) {
|
|
80
|
+
this.filename = fname || '';
|
|
81
|
+
this.comment = comment || null;
|
|
82
|
+
this.debug = debug || false;
|
|
83
|
+
|
|
84
|
+
// Set the UTF-8 Language Encoding Flag (EFS) to indicate UTF-8 encoding for filenames
|
|
85
|
+
this.bitFlags |= GP_FLAG.EFS;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
isMSDOS = this.platform === null;
|
|
89
|
+
isUnixLike = this.platform != null && this.platform !== 'win32';
|
|
90
|
+
isMacOS = this.platform != null && this.platform === 'darwin';
|
|
91
|
+
isLinux = this.platform != null && this.platform === 'linux';
|
|
92
|
+
|
|
93
|
+
VER_MADE_BY = (() => {
|
|
94
|
+
switch (this.platform) {
|
|
95
|
+
case 'darwin':
|
|
96
|
+
return (FILE_SYSTEM.DARWIN << 8) | VER_ENCODING; // macOS/Darwin
|
|
97
|
+
case 'win32':
|
|
98
|
+
return (FILE_SYSTEM.NTFS << 8) | VER_ENCODING; // Windows
|
|
99
|
+
default:
|
|
100
|
+
return (FILE_SYSTEM.UNIX << 8) | VER_ENCODING; // Unix/Linux
|
|
101
|
+
}
|
|
102
|
+
})();
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Reads ZIP entry data from a central directory buffer
|
|
106
|
+
* @param data - Buffer containing central directory entry data
|
|
107
|
+
* @returns Buffer positioned at start of next entry
|
|
108
|
+
* @throws Error if central directory entry is invalid
|
|
109
|
+
*/
|
|
110
|
+
readZipEntry(data: Buffer): Buffer {
|
|
111
|
+
// Check if buffer is too small before trying to read from it
|
|
112
|
+
if (data.length < CENTRAL_DIR.SIZE) {
|
|
113
|
+
throw new Error('Zip entry data is too small or corrupt');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Verify this is a Central Directory Header
|
|
117
|
+
// data should be 46 bytes and start with "PK 01 02"
|
|
118
|
+
if (data.readUInt32LE(0) !== CENTRAL_DIR.SIGNATURE) {
|
|
119
|
+
throw new Error(Errors.INVALID_CEN);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Read Zip version made by
|
|
123
|
+
this.verMadeBy = data.readUInt16LE(CENTRAL_DIR.VER_MADE);
|
|
124
|
+
// Read Zip version needed to extract
|
|
125
|
+
this.verExtract = data.readUInt16LE(CENTRAL_DIR.VER_EXT);
|
|
126
|
+
// encrypt, decrypt flags
|
|
127
|
+
this.bitFlags = data.readUInt16LE(CENTRAL_DIR.FLAGS);
|
|
128
|
+
// Test if Zip Entry is encrypted
|
|
129
|
+
if ((this.bitFlags & GP_FLAG.ENCRYPTED) != 0) {
|
|
130
|
+
this.isEncrypted = true;
|
|
131
|
+
if ((this.bitFlags & GP_FLAG.STRONG_ENCRYPT) != 0)
|
|
132
|
+
this.isStrongEncrypt = true;
|
|
133
|
+
}
|
|
134
|
+
// compression method
|
|
135
|
+
this.cmpMethod = data.readUInt16LE(CENTRAL_DIR.CMP_METHOD);
|
|
136
|
+
// modification time (2 bytes time, 2 bytes date)
|
|
137
|
+
this.timeDateDOS = data.readUInt32LE(CENTRAL_DIR.TIMEDATE_DOS);
|
|
138
|
+
|
|
139
|
+
// uncompressed file crc-32 value
|
|
140
|
+
this.crc = data.readUInt32LE(CENTRAL_DIR.CRC);
|
|
141
|
+
// compressed size
|
|
142
|
+
this.compressedSize = data.readUInt32LE(CENTRAL_DIR.CMP_SIZE);
|
|
143
|
+
// uncompressed size
|
|
144
|
+
this.uncompressedSize = data.readUInt32LE(CENTRAL_DIR.UNCMP_SIZE);
|
|
145
|
+
// volume number start
|
|
146
|
+
this.volNumber = data.readUInt16LE(CENTRAL_DIR.DISK_NUM);
|
|
147
|
+
// internal file attributes
|
|
148
|
+
this.intFileAttr = data.readUInt16LE(CENTRAL_DIR.INT_FILE_ATTR);
|
|
149
|
+
// external file attributes
|
|
150
|
+
this.extFileAttr = data.readUInt32LE(CENTRAL_DIR.EXT_FILE_ATTR);
|
|
151
|
+
if (this.extFileAttr & DOS_FILE_ATTR.DIRECTORY)
|
|
152
|
+
this.isDirectory = true;
|
|
153
|
+
|
|
154
|
+
// LOC header offset
|
|
155
|
+
this.localHdrOffset = data.readUInt32LE(CENTRAL_DIR.LOCAL_HDR_OFFSET);
|
|
156
|
+
|
|
157
|
+
// Filename Length - 2 bytes
|
|
158
|
+
let fnameLen = data.readUInt16LE(CENTRAL_DIR.FNAME_LEN);
|
|
159
|
+
const filename = data.toString('utf8', CENTRAL_DIR.SIZE, CENTRAL_DIR.SIZE + fnameLen);
|
|
160
|
+
this.filename = filename;
|
|
161
|
+
if (this.filename.endsWith('/'))
|
|
162
|
+
this.isDirectory = true;
|
|
163
|
+
|
|
164
|
+
// Extra Field Length - 2 bytes
|
|
165
|
+
let extraLen = data.readUInt16LE(CENTRAL_DIR.EXTRA_LEN);
|
|
166
|
+
if (extraLen > 0) {
|
|
167
|
+
this.extraField = data.subarray(CENTRAL_DIR.SIZE + fnameLen, CENTRAL_DIR.SIZE + fnameLen + extraLen);
|
|
168
|
+
|
|
169
|
+
// First pass: Check for Unicode Path to ensure correct filename before processing other fields
|
|
170
|
+
for (let i = 0; i < extraLen; ) {
|
|
171
|
+
let _id = this.extraField.readUInt16LE(i);
|
|
172
|
+
let _len = this.extraField.readUInt16LE(i + 2);
|
|
173
|
+
let _data = this.extraField.subarray(i + 4, i + 4 + _len);
|
|
174
|
+
|
|
175
|
+
if (_id === HDR_ID.UNICODE_PATH && _len >= 5) {
|
|
176
|
+
// Unicode Path Extra Field
|
|
177
|
+
const version = _data.readUInt8(0);
|
|
178
|
+
const nameCrc32 = _data.readUInt32LE(1);
|
|
179
|
+
|
|
180
|
+
// Calculate CRC32 of the current filename
|
|
181
|
+
const fnameBuf = Buffer.from(this.filename);
|
|
182
|
+
const calculatedCrc = crc32(fnameBuf);
|
|
183
|
+
|
|
184
|
+
// If CRCs match, use the UTF-8 filename from the extra field
|
|
185
|
+
if (calculatedCrc === nameCrc32) {
|
|
186
|
+
const unicodeName = _data.subarray(5).toString('utf8');
|
|
187
|
+
this.filename = unicodeName;
|
|
188
|
+
if (this.debug) {
|
|
189
|
+
console.log(`Using Unicode Path: ${this.filename}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
i += 4 + _len;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Second pass: Process all other extra fields
|
|
197
|
+
for (let i = 0; i < extraLen; ) {
|
|
198
|
+
let _id = this.extraField.readUInt16LE(i);
|
|
199
|
+
let _len = this.extraField.readUInt16LE(i + 2);
|
|
200
|
+
let _data = this.extraField.subarray(i + 4, i + 4 + _len);
|
|
201
|
+
|
|
202
|
+
if (_id === HDR_ID.SHA256) {
|
|
203
|
+
if (_len === 64)
|
|
204
|
+
// Early versions of NeoZip used a UTF-8 encoded string
|
|
205
|
+
this.sha256 = _data.toString('utf8');
|
|
206
|
+
else
|
|
207
|
+
this.sha256 = _data.toString('hex');
|
|
208
|
+
} else if (_id === HDR_ID.UNV_TIME) {
|
|
209
|
+
// Universal Time field has a flag byte followed by a 4-byte timestamp
|
|
210
|
+
if (_len >= 5) {
|
|
211
|
+
const flags = _data.readUInt8(0);
|
|
212
|
+
// Check if modification time is present (bit 0)
|
|
213
|
+
if (flags & 0x01) {
|
|
214
|
+
this.universalTime = _data.readUInt32LE(1);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
} else if (_id === HDR_ID.UID_GID) {
|
|
218
|
+
// Extract UID/GID if present
|
|
219
|
+
if (_len >= 5) { // Version + UID size + UID + GID size + GID
|
|
220
|
+
const version = _data.readUInt8(0);
|
|
221
|
+
const uidSize = _data.readUInt8(1);
|
|
222
|
+
if (uidSize <= 4 && _len >= 2 + uidSize) {
|
|
223
|
+
// Read UID based on its size (1-4 bytes)
|
|
224
|
+
this.uid = _data.readUIntLE(2, uidSize);
|
|
225
|
+
|
|
226
|
+
// Check if GID is also present
|
|
227
|
+
if (_len >= 2 + uidSize + 1) {
|
|
228
|
+
const gidSize = _data.readUInt8(2 + uidSize);
|
|
229
|
+
if (gidSize <= 4 && _len >= 2 + uidSize + 1 + gidSize) {
|
|
230
|
+
this.gid = _data.readUIntLE(2 + uidSize + 1, gidSize);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
// Skip Unicode Path here as we already processed it
|
|
237
|
+
i += 4 + _len;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// File Comment Length - 2 bytes
|
|
242
|
+
let comLen = data.readUInt16LE(CENTRAL_DIR.COMMENT_LEN);
|
|
243
|
+
if (comLen > 0)
|
|
244
|
+
this.comment = data.toString('utf8', CENTRAL_DIR.SIZE + fnameLen + extraLen, CENTRAL_DIR.SIZE + fnameLen + extraLen + comLen);
|
|
245
|
+
|
|
246
|
+
if (this.debug)
|
|
247
|
+
this.showVerboseInfo();
|
|
248
|
+
|
|
249
|
+
// Calculate the Buffer for the next entry
|
|
250
|
+
let rawSize = CENTRAL_DIR.SIZE + fnameLen + extraLen + comLen;
|
|
251
|
+
return data.subarray(rawSize);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Checks if the filename contains characters that require Unicode handling
|
|
256
|
+
* @returns true if the filename contains non-ASCII characters or special characters
|
|
257
|
+
*/
|
|
258
|
+
needsUnicodeHandling(): boolean {
|
|
259
|
+
// Check if filename contains non-ASCII characters or special characters like apostrophes
|
|
260
|
+
return /[^\x00-\x7E]|['"]/.test(this.filename);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Adds UTF-8 Unicode Path field to a buffer
|
|
265
|
+
* @param buffer - The buffer to write to
|
|
266
|
+
* @param offset - The offset in the buffer to start writing
|
|
267
|
+
* @returns The new offset after writing
|
|
268
|
+
*/
|
|
269
|
+
private addUnicodePathField(buffer: Buffer, offset: number): number {
|
|
270
|
+
// Create a UTF-8 buffer of the filename
|
|
271
|
+
const unicodePathBuf = Buffer.from(this.filename, 'utf8');
|
|
272
|
+
|
|
273
|
+
// Calculate CRC32 of the ASCII version of the filename
|
|
274
|
+
// Create an ASCII version by replacing non-ASCII chars with '?'
|
|
275
|
+
const asciiName = this.filename.replace(/[^\x00-\x7E]/g, '?');
|
|
276
|
+
const asciiNameBuf = Buffer.from(asciiName, 'ascii');
|
|
277
|
+
const nameCrc32 = crc32(asciiNameBuf);
|
|
278
|
+
|
|
279
|
+
// 1 byte version + 4 bytes CRC + filename
|
|
280
|
+
const unicodePathLen = 5 + unicodePathBuf.length;
|
|
281
|
+
|
|
282
|
+
// Write Unicode Path Extra Field (0x7075)
|
|
283
|
+
buffer.writeUInt16LE(HDR_ID.UNICODE_PATH, offset); // "up" header ID
|
|
284
|
+
buffer.writeUInt16LE(unicodePathLen, offset + 2); // data length
|
|
285
|
+
buffer.writeUInt8(1, offset + 4); // version (1)
|
|
286
|
+
buffer.writeUInt32LE(nameCrc32, offset + 5); // CRC-32 of standard filename
|
|
287
|
+
unicodePathBuf.copy(buffer, offset + 9); // UTF-8 version of filename
|
|
288
|
+
|
|
289
|
+
return offset + 4 + unicodePathLen; // 4 bytes for header + data length
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Creates a local header for this ZIP entry
|
|
294
|
+
* @returns Buffer containing the local header data
|
|
295
|
+
*/
|
|
296
|
+
createLocalHdr(): Buffer {
|
|
297
|
+
const commentLen = this.comment?.length ?? 0;
|
|
298
|
+
let extraFieldLen = 0;
|
|
299
|
+
|
|
300
|
+
// Only create Unicode Path field if needed
|
|
301
|
+
const needsUnicode = this.needsUnicodeHandling();
|
|
302
|
+
|
|
303
|
+
if (needsUnicode) {
|
|
304
|
+
// 1 byte version + 4 bytes CRC + filename + 4 bytes header
|
|
305
|
+
const unicodeNameLen = Buffer.from(this.filename, 'utf8').length;
|
|
306
|
+
extraFieldLen = 5 + unicodeNameLen + 4;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const data = Buffer.alloc(LOCAL_HDR.SIZE + this.filename.length + commentLen + extraFieldLen);
|
|
310
|
+
|
|
311
|
+
// "PK\003\004"
|
|
312
|
+
data.writeUInt32LE(LOCAL_HDR.SIGNATURE, 0);
|
|
313
|
+
// version needed to extract
|
|
314
|
+
data.writeUInt16LE(VER_EXTRACT, LOCAL_HDR.VER_EXTRACT);
|
|
315
|
+
// general purpose bit flag
|
|
316
|
+
data.writeUInt16LE(this.bitFlags >>> 0, LOCAL_HDR.FLAGS);
|
|
317
|
+
// compression method
|
|
318
|
+
data.writeUInt16LE(this.cmpMethod, LOCAL_HDR.COMPRESSION);
|
|
319
|
+
// modification time (2 bytes time, 2 bytes date)
|
|
320
|
+
data.writeUInt32LE(this.timeDateDOS >>> 0, LOCAL_HDR.TIMEDATE_DOS);
|
|
321
|
+
// uncompressed file crc-32 value
|
|
322
|
+
data.writeUInt32LE(this.crc, LOCAL_HDR.CRC);
|
|
323
|
+
// compressed size
|
|
324
|
+
data.writeUInt32LE(this.compressedSize, LOCAL_HDR.CMP_SIZE);
|
|
325
|
+
// uncompressed size
|
|
326
|
+
data.writeUInt32LE(this.uncompressedSize, LOCAL_HDR.UNCMP_SIZE);
|
|
327
|
+
// filename length
|
|
328
|
+
data.writeUInt16LE(this.filename.length, LOCAL_HDR.FNAME_LEN);
|
|
329
|
+
// extra field length
|
|
330
|
+
data.writeUInt16LE(extraFieldLen, LOCAL_HDR.EXTRA_LEN);
|
|
331
|
+
|
|
332
|
+
// Write filename - use ASCII filename (replacing non-ASCII with ?)
|
|
333
|
+
// This ensures compatibility with older ZIP readers
|
|
334
|
+
const asciiName = this.filename.replace(/[^\x00-\x7E]/g, '?');
|
|
335
|
+
const fnameBuf = Buffer.from(asciiName);
|
|
336
|
+
fnameBuf.copy(data, LOCAL_HDR.SIZE);
|
|
337
|
+
|
|
338
|
+
let extraOffset = LOCAL_HDR.SIZE + fnameBuf.length;
|
|
339
|
+
|
|
340
|
+
// Add Unicode Path Extra Field only if needed
|
|
341
|
+
if (needsUnicode) {
|
|
342
|
+
extraOffset = this.addUnicodePathField(data, extraOffset);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Add comment if present (after extra field)
|
|
346
|
+
if (commentLen > 0 && this.comment) {
|
|
347
|
+
const commentBuf = Buffer.from(this.comment, 'utf8');
|
|
348
|
+
commentBuf.copy(data, extraOffset);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return data;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Creates a central directory entry for this ZIP entry
|
|
356
|
+
* @returns Buffer containing the central directory entry data
|
|
357
|
+
*/
|
|
358
|
+
centralDirEntry(): Buffer {
|
|
359
|
+
// Calculate the length of the extra fields
|
|
360
|
+
const commentLen = this.comment?.length ?? 0;
|
|
361
|
+
const utfLen = this.universalTime ? 9 : 0; // 4 bytes header + 5 bytes data
|
|
362
|
+
const uidgidLen = this.uid && this.gid ? 11 + 4 : 0; // 1 byte version + 1 byte size + 4 bytes data + 1 byte size + 4 bytes data
|
|
363
|
+
const sha256Buf = this.sha256 ? Buffer.from(this.sha256, 'hex') : null;
|
|
364
|
+
const sha256Len = sha256Buf ? (sha256Buf.length + 4) : 0;
|
|
365
|
+
|
|
366
|
+
// Only add Unicode Path field if needed
|
|
367
|
+
const needsUnicode = this.needsUnicodeHandling();
|
|
368
|
+
let unicodePathLen = 0;
|
|
369
|
+
|
|
370
|
+
if (needsUnicode) {
|
|
371
|
+
// 1 byte version + 4 bytes CRC + filename + 4 bytes header
|
|
372
|
+
const unicodeNameLen = Buffer.from(this.filename, 'utf8').length;
|
|
373
|
+
unicodePathLen = 5 + unicodeNameLen + 4;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const extraLen = utfLen + sha256Len + uidgidLen + (needsUnicode ? unicodePathLen : 0);
|
|
377
|
+
|
|
378
|
+
// Central directory header size (46 Bytes + filename + comment + extra fields)
|
|
379
|
+
const data = Buffer.alloc(CENTRAL_DIR.SIZE + this.filename.length + commentLen + extraLen);
|
|
380
|
+
|
|
381
|
+
// "PK\001\002"
|
|
382
|
+
data.writeUInt32LE(CENTRAL_DIR.SIGNATURE, 0);
|
|
383
|
+
// Version made by - Needs to be set for NeoZip
|
|
384
|
+
data.writeUInt16LE(this.isUpdated ? this.VER_MADE_BY : this.verMadeBy, CENTRAL_DIR.VER_MADE);
|
|
385
|
+
// Version needed to extract
|
|
386
|
+
data.writeInt16LE(this.isUpdated ? VER_EXTRACT : this.verMadeBy, CENTRAL_DIR.VER_EXT);
|
|
387
|
+
// Encrypt, Decrypt Flags
|
|
388
|
+
data.writeInt16LE(this.bitFlags >>> 0, CENTRAL_DIR.FLAGS);
|
|
389
|
+
// Compression method
|
|
390
|
+
data.writeInt16LE(this.cmpMethod, CENTRAL_DIR.CMP_METHOD);
|
|
391
|
+
// Modification time (2 bytes time, 2 bytes date)
|
|
392
|
+
data.writeUInt32LE(this.timeDateDOS >>> 0, CENTRAL_DIR.TIMEDATE_DOS);
|
|
393
|
+
// Uncompressed file CRC-32 value
|
|
394
|
+
data.writeUInt32LE(this.crc, CENTRAL_DIR.CRC);
|
|
395
|
+
// Compressed Size
|
|
396
|
+
data.writeUInt32LE(this.compressedSize, CENTRAL_DIR.CMP_SIZE);
|
|
397
|
+
// Uncompressed Size
|
|
398
|
+
data.writeUInt32LE(this.uncompressedSize, CENTRAL_DIR.UNCMP_SIZE);
|
|
399
|
+
// Filename Length
|
|
400
|
+
data.writeUInt16LE(this.filename.length, CENTRAL_DIR.FNAME_LEN);
|
|
401
|
+
// Extra Field Length
|
|
402
|
+
data.writeUInt16LE(extraLen, CENTRAL_DIR.EXTRA_LEN);
|
|
403
|
+
// File Comment Length
|
|
404
|
+
data.writeUInt16LE(commentLen, CENTRAL_DIR.COMMENT_LEN);
|
|
405
|
+
// Volume Number Start
|
|
406
|
+
data.writeUInt16LE(0, CENTRAL_DIR.DISK_NUM);
|
|
407
|
+
// Internal File Attributes
|
|
408
|
+
data.writeUInt16LE(this.intFileAttr >>> 0, CENTRAL_DIR.INT_FILE_ATTR);
|
|
409
|
+
// External File Attributes
|
|
410
|
+
data.writeUInt32LE(this.extFileAttr >>> 0, CENTRAL_DIR.EXT_FILE_ATTR);
|
|
411
|
+
// Local Header Offset
|
|
412
|
+
data.writeUInt32LE(this.localHdrOffset, CENTRAL_DIR.LOCAL_HDR_OFFSET);
|
|
413
|
+
|
|
414
|
+
// Write filename - use ASCII filename (replacing non-ASCII with ?)
|
|
415
|
+
// This ensures compatibility with older ZIP readers
|
|
416
|
+
const asciiName = this.filename.replace(/[^\x00-\x7E]/g, '?');
|
|
417
|
+
const fnameBuf = Buffer.from(asciiName);
|
|
418
|
+
fnameBuf.copy(data, CENTRAL_DIR.SIZE);
|
|
419
|
+
|
|
420
|
+
if (commentLen > 0 && this.comment) {
|
|
421
|
+
const commentBuf = Buffer.from(this.comment, 'utf8');
|
|
422
|
+
commentBuf.copy(data, CENTRAL_DIR.SIZE + fnameBuf.length);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Add Extra Field data
|
|
426
|
+
let extraOffset = CENTRAL_DIR.SIZE + fnameBuf.length + commentLen;
|
|
427
|
+
|
|
428
|
+
// Add Universal Time field
|
|
429
|
+
if (this.universalTime) {
|
|
430
|
+
data.writeUInt16LE(HDR_ID.UNV_TIME, extraOffset); // 0x5455
|
|
431
|
+
data.writeUInt16LE(5, extraOffset + 2); // Length of data (flags + time)
|
|
432
|
+
data.writeUInt8(0x03, extraOffset + 4); // Flags: 1=Mod time, 2=Access time
|
|
433
|
+
data.writeUInt32LE(this.universalTime, extraOffset + 5); // Unix timestamp
|
|
434
|
+
extraOffset += 9; // 4 bytes header + 1 byte flags + 4 bytes data
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Unix UID/GID Extra Field (0x7875)
|
|
438
|
+
if (this.uid && this.gid) {
|
|
439
|
+
const uidgidLen = 11; // 1 byte version + 1 byte size + 4 bytes data + 1 byte size + 4 bytes data
|
|
440
|
+
data.writeUInt16LE(HDR_ID.UID_GID, extraOffset); // 0x7875
|
|
441
|
+
data.writeUInt16LE(uidgidLen, extraOffset + 2); // Length
|
|
442
|
+
data.writeUInt8(1, extraOffset + 4); // Version
|
|
443
|
+
data.writeUInt8(4, extraOffset + 5); // Size of UID/GID
|
|
444
|
+
data.writeUInt32LE(this.uid, extraOffset + 6); // UID
|
|
445
|
+
data.writeUInt8(4, extraOffset + 10); // Size of UID/GID
|
|
446
|
+
data.writeUInt32LE(this.gid, extraOffset + 11); // GID
|
|
447
|
+
extraOffset += uidgidLen + 4;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Add Unicode Path Extra Field only if needed
|
|
451
|
+
if (needsUnicode) {
|
|
452
|
+
extraOffset = this.addUnicodePathField(data, extraOffset);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Add SHA256 field
|
|
456
|
+
if (sha256Buf) {
|
|
457
|
+
data.writeUInt16LE(HDR_ID.SHA256, extraOffset);
|
|
458
|
+
data.writeUInt16LE(sha256Buf.length, extraOffset + 2);
|
|
459
|
+
sha256Buf.copy(data, extraOffset + 4);
|
|
460
|
+
extraOffset += sha256Buf.length + 4;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return data;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// ======================================
|
|
467
|
+
// Routines to handle the details of the Zip Entry
|
|
468
|
+
// ======================================
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Sets the DOS date/time for this entry
|
|
472
|
+
* @param date - Date to convert to DOS format
|
|
473
|
+
* @returns number - DOS format date/time
|
|
474
|
+
*/
|
|
475
|
+
setDateTime(date: Date): number {
|
|
476
|
+
if (!date) return 0;
|
|
477
|
+
let time =
|
|
478
|
+
(((date.getFullYear() - 1980) & 0x7f) << 25) | // b09-16 years from 1980
|
|
479
|
+
((date.getMonth() + 1) << 21) | // b05-08 month
|
|
480
|
+
(date.getDate() << 16) | // b00-04 hour
|
|
481
|
+
// 2 bytes time
|
|
482
|
+
(date.getHours() << 11) | // b11-15 hour
|
|
483
|
+
(date.getMinutes() << 5) | // b05-10 minute
|
|
484
|
+
(date.getSeconds() >> 1); // b00-04 seconds divided by 2
|
|
485
|
+
return time;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Converts DOS date/time to JavaScript Date
|
|
490
|
+
* @param timeStamp - DOS format date/time
|
|
491
|
+
* @returns Date object or null if timestamp is 0
|
|
492
|
+
*/
|
|
493
|
+
parseDateTime(timeStamp: number): Date | null {
|
|
494
|
+
if (timeStamp == 0)
|
|
495
|
+
return null;
|
|
496
|
+
return new Date(
|
|
497
|
+
((timeStamp >> 25) & 0x3f) + 1980, // Year
|
|
498
|
+
((timeStamp >> 21) & 0x0f) - 1, // Month
|
|
499
|
+
(timeStamp >> 16) & 0x1f, // Day
|
|
500
|
+
(timeStamp >> 11) & 0x1f, // Hour
|
|
501
|
+
(timeStamp >> 5) & 0x3f, // Min
|
|
502
|
+
(timeStamp & 0x1f) << 1); // Sec
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Formats the entry's date in local format
|
|
507
|
+
* @returns String in MM/DD/YYYY format or "--/--/----" if no date
|
|
508
|
+
*/
|
|
509
|
+
toLocalDateString(): string {
|
|
510
|
+
let _timeDate = this.parseDateTime(this.timeDateDOS);
|
|
511
|
+
if (_timeDate === null) return "--/--/----";
|
|
512
|
+
|
|
513
|
+
return _timeDate.toLocaleDateString('en-US', {
|
|
514
|
+
year: 'numeric',
|
|
515
|
+
month: '2-digit',
|
|
516
|
+
day: '2-digit'
|
|
517
|
+
}).slice(0,10);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Formats the entry's time
|
|
522
|
+
* @returns String in HH:MM format or "--:--" if no time
|
|
523
|
+
*/
|
|
524
|
+
toTimeString(): string {
|
|
525
|
+
let _timeDate = this.parseDateTime(this.timeDateDOS);
|
|
526
|
+
if (_timeDate === null) return "--:--";
|
|
527
|
+
|
|
528
|
+
return _timeDate.toTimeString().slice(0, 5);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Formats the entry's date and time in local format
|
|
533
|
+
* @returns String like "Jan 01, 2024 13:45:30" or "--/--/-- --:--" if no date/time
|
|
534
|
+
*/
|
|
535
|
+
toFormattedDateString(): string {
|
|
536
|
+
let _timeDate = this.parseDateTime(this.timeDateDOS);
|
|
537
|
+
if (_timeDate == null) return "--/--/-- --:--";
|
|
538
|
+
|
|
539
|
+
const datePart = _timeDate.toLocaleDateString('en-US', {
|
|
540
|
+
year: 'numeric',
|
|
541
|
+
month: 'short',
|
|
542
|
+
day: '2-digit'
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
const timePart = _timeDate.toLocaleTimeString('en-US', {
|
|
546
|
+
hour: '2-digit',
|
|
547
|
+
minute: '2-digit',
|
|
548
|
+
second: '2-digit',
|
|
549
|
+
hour12: false
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
return `${datePart} ${timePart}`;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Formats the entry's date and time in UTC
|
|
557
|
+
* @returns String like "Jan 01, 2024 13:45:30 UTC" or "--/--/-- --:--" if no date/time
|
|
558
|
+
*/
|
|
559
|
+
toFormattedUTCDateString(): string {
|
|
560
|
+
let _timeDate = this.parseDateTime(this.timeDateDOS);
|
|
561
|
+
if (_timeDate == null) return "--/--/-- --:--";
|
|
562
|
+
|
|
563
|
+
const datePart = _timeDate.toLocaleDateString('en-US', {
|
|
564
|
+
year: 'numeric',
|
|
565
|
+
month: 'short',
|
|
566
|
+
day: '2-digit',
|
|
567
|
+
timeZone: 'UTC'
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
const timePart = _timeDate.toLocaleTimeString('en-US', {
|
|
571
|
+
hour: '2-digit',
|
|
572
|
+
minute: '2-digit',
|
|
573
|
+
second: '2-digit',
|
|
574
|
+
hour12: false,
|
|
575
|
+
timeZone: 'UTC'
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
return `${datePart} ${timePart} UTC`;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Converts compression method code to human-readable string
|
|
583
|
+
* @returns String describing the compression method
|
|
584
|
+
*/
|
|
585
|
+
cmpMethodToString(): string {
|
|
586
|
+
switch (this.cmpMethod) {
|
|
587
|
+
case CMP_METHOD.STORED: return 'Stored';
|
|
588
|
+
case CMP_METHOD.SHRUNK: return 'Shrunk';
|
|
589
|
+
case CMP_METHOD.REDUCED1: return 'Reduced-1';
|
|
590
|
+
case CMP_METHOD.REDUCED2: return 'Reduced-2';
|
|
591
|
+
case CMP_METHOD.REDUCED3: return 'Reduced-3';
|
|
592
|
+
case CMP_METHOD.REDUCED4: return 'Reduced-4';
|
|
593
|
+
case CMP_METHOD.IMPLODED: return 'Imploded';
|
|
594
|
+
case CMP_METHOD.DEFLATED:
|
|
595
|
+
switch (this.bitFlags & 0x6) {
|
|
596
|
+
case 0: return 'Deflate-N'; // Deflate Normal
|
|
597
|
+
case 2: return 'Deflate-M'; // Deflate Maximum
|
|
598
|
+
case 4: return 'Deflate-F'; // Deflate Fast
|
|
599
|
+
case 6: return 'Deflate-S'; // Deflate Super Fast
|
|
600
|
+
}
|
|
601
|
+
case CMP_METHOD.ENHANCED_DEFLATE: return 'Deflate-Enh';
|
|
602
|
+
case CMP_METHOD.IBM_TERSE: return 'PKDCL-LZ77';
|
|
603
|
+
case CMP_METHOD.ZSTD: return 'Zstandard';
|
|
604
|
+
|
|
605
|
+
default: return 'Unknown';
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Converts file system code to human-readable string
|
|
611
|
+
* @returns String describing the file system
|
|
612
|
+
*/
|
|
613
|
+
fileSystemToString(): string {
|
|
614
|
+
switch (this.verMadeBy >> 8) {
|
|
615
|
+
case FILE_SYSTEM.MSDOS: return 'MS-DOS';
|
|
616
|
+
case FILE_SYSTEM.AMIGA: return 'Amiga';
|
|
617
|
+
case FILE_SYSTEM.OPENVMS: return 'OpenVMS';
|
|
618
|
+
case FILE_SYSTEM.UNIX: return 'Unix';
|
|
619
|
+
case FILE_SYSTEM.VM_CMS: return 'VM/CMS';
|
|
620
|
+
case FILE_SYSTEM.ATARI: return 'Atari ST';
|
|
621
|
+
case FILE_SYSTEM.OS2: return 'OS/2 HPFS';
|
|
622
|
+
case FILE_SYSTEM.MAC: return 'Macintosh';
|
|
623
|
+
case FILE_SYSTEM.CP_M: return 'CP/M';
|
|
624
|
+
case FILE_SYSTEM.NTFS: return 'Windows NTFS';
|
|
625
|
+
case FILE_SYSTEM.MVS: return 'MVS (OS/390 - Z/OS)';
|
|
626
|
+
case FILE_SYSTEM.VSE: return 'VSE';
|
|
627
|
+
case FILE_SYSTEM.ACORN: return 'Acorn Risc';
|
|
628
|
+
case FILE_SYSTEM.ALTMVS: return 'Alternate MVS';
|
|
629
|
+
case FILE_SYSTEM.BEOS: return 'BeOS';
|
|
630
|
+
case FILE_SYSTEM.TANDEM: return 'Tandem';
|
|
631
|
+
case FILE_SYSTEM.OS400: return 'OS/400';
|
|
632
|
+
case FILE_SYSTEM.DARWIN: return 'Apple OS/X (Darwin)';
|
|
633
|
+
default: return 'Unknown';
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Converts MS-DOS file attributes to string representation
|
|
639
|
+
* @returns String like "----" where R=readonly, H=hidden, S=system, A=archive
|
|
640
|
+
*/
|
|
641
|
+
private dosAttributesToString(): string {
|
|
642
|
+
let dosAttr = this.extFileAttr & 0xFFFF;
|
|
643
|
+
if (dosAttr === 0) return 'none';
|
|
644
|
+
let attrs = '';
|
|
645
|
+
attrs += (dosAttr & DOS_FILE_ATTR.READONLY) ? 'r' : '-';
|
|
646
|
+
attrs += (dosAttr & DOS_FILE_ATTR.HIDDEN) ? 'h' : '-';
|
|
647
|
+
attrs += (dosAttr & DOS_FILE_ATTR.SYSTEM) ? 's' : '-';
|
|
648
|
+
// attrs += (dosAttr & DOS_FILE_ATTR.VOLUME) ? 'v' : '-';
|
|
649
|
+
attrs += (dosAttr & DOS_FILE_ATTR.DIRECTORY) ? 'd' : '-';
|
|
650
|
+
attrs += (dosAttr & DOS_FILE_ATTR.ARCHIVE) ? 'a' : '-';
|
|
651
|
+
return attrs;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Outputs detailed information about this entry for debugging
|
|
656
|
+
* Includes compression, encryption, timestamps, and extra fields
|
|
657
|
+
*/
|
|
658
|
+
showVerboseInfo() {
|
|
659
|
+
console.log('=== Central Directory Entry (%s) ===', this.filename);
|
|
660
|
+
console.log('unzip from start of archive: ', this.localHdrOffset);
|
|
661
|
+
console.log('File system or operating system of origin: ', this.fileSystemToString());
|
|
662
|
+
console.log('Version of encoding software: ', this.verMadeBy);
|
|
663
|
+
console.log('Compression Method: ', this.cmpMethodToString());
|
|
664
|
+
console.log('File Security Status: ',
|
|
665
|
+
this.bitFlags & GP_FLAG.ENCRYPTED ?
|
|
666
|
+
this.bitFlags & GP_FLAG.STRONG_ENCRYPT ? 'Strong Encrypt' : 'Encrypted' :
|
|
667
|
+
'Not Encrypted');
|
|
668
|
+
console.log('File Modified (DOS date/time) ', this.toFormattedDateString());
|
|
669
|
+
console.log('File Modified (UTC) ', this.toFormattedUTCDateString());
|
|
670
|
+
console.log('Compressed Size: ', this.compressedSize);
|
|
671
|
+
console.log('UnCompressed Size: ', this.uncompressedSize);
|
|
672
|
+
console.log(`32-bit CRC value (hex): ${this.crc.toString(16).padStart(8, '0')}`);
|
|
673
|
+
console.log('Length of extra field: ', this.extraField?.length ?? 0);
|
|
674
|
+
console.log('Length of file comment: ', this.comment?.length ?? 0);
|
|
675
|
+
console.log('Unix File Attributes: ');
|
|
676
|
+
console.log('MS-DOS File Attributes: ', this.dosAttributesToString());
|
|
677
|
+
if (this.extraField) {
|
|
678
|
+
console.log('\nThe Central-Directory Extra Field contains:');
|
|
679
|
+
try {
|
|
680
|
+
for (let i = 0; i < this.extraField.length; ) {
|
|
681
|
+
// Ensure we have at least 4 bytes (header ID + length)
|
|
682
|
+
if (i + 4 > this.extraField.length) {
|
|
683
|
+
console.log(` Warning: Truncated extra field at offset ${i}`);
|
|
684
|
+
break;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
let _id = this.extraField.readUInt16LE(i);
|
|
688
|
+
let _idStr = _id.toString(16).padStart(4, '0');
|
|
689
|
+
let _len = this.extraField.readUInt16LE(i + 2);
|
|
690
|
+
|
|
691
|
+
// Validate the length to ensure it doesn't exceed buffer bounds
|
|
692
|
+
if (_len < 0 || i + 4 + _len > this.extraField.length) {
|
|
693
|
+
console.log(` Warning: Invalid extra field length (${_len}) at offset ${i} for ID ${_idStr}`);
|
|
694
|
+
break;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
let _data = this.extraField.subarray(i + 4, i + 4 + _len);
|
|
698
|
+
|
|
699
|
+
try {
|
|
700
|
+
if (_id === HDR_ID.SHA256) {
|
|
701
|
+
console.log(` ID[0x${_idStr}] NeoZip-SHA256: ${_data.toString('hex')}`);
|
|
702
|
+
} else if (_id === HDR_ID.UNV_TIME) {
|
|
703
|
+
if (_len >= 5) {
|
|
704
|
+
const flags = _data.readUInt8(0);
|
|
705
|
+
const timestamp = _data.readUInt32LE(1);
|
|
706
|
+
const date = new Date(timestamp * 1000);
|
|
707
|
+
console.log(` ID[0x${_idStr}] Universal Time: flags=${flags}, time=${date.toISOString()}`);
|
|
708
|
+
} else {
|
|
709
|
+
console.log(` ID[0x${_idStr}] Universal Time: (invalid length ${_len})`);
|
|
710
|
+
}
|
|
711
|
+
} else if (_id === HDR_ID.UID_GID) {
|
|
712
|
+
if (_len >= 5) {
|
|
713
|
+
const version = _data.readUInt8(0);
|
|
714
|
+
const uidSize = _data.readUInt8(1);
|
|
715
|
+
if (2 + uidSize > _len) {
|
|
716
|
+
console.log(` ID[0x${_idStr}] Unix UID/GID: (invalid UID size ${uidSize})`);
|
|
717
|
+
} else {
|
|
718
|
+
const uid = _data.readUIntLE(2, Math.min(uidSize, 4));
|
|
719
|
+
let gid = 0;
|
|
720
|
+
if (2 + uidSize + 1 < _len) {
|
|
721
|
+
const gidSize = _data.readUInt8(2 + uidSize);
|
|
722
|
+
if (2 + uidSize + 1 + gidSize <= _len) {
|
|
723
|
+
gid = _data.readUIntLE(2 + uidSize + 1, Math.min(gidSize, 4));
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
console.log(` ID[0x${_idStr}] Unix UID/GID: version=${version}, uid=${uid}, gid=${gid}`);
|
|
727
|
+
}
|
|
728
|
+
} else {
|
|
729
|
+
console.log(` ID[0x${_idStr}] Unix UID/GID: (invalid length ${_len})`);
|
|
730
|
+
}
|
|
731
|
+
} else if (_id === HDR_ID.UNICODE_PATH) {
|
|
732
|
+
if (_len >= 5) {
|
|
733
|
+
const version = _data.readUInt8(0);
|
|
734
|
+
const nameCrc32 = _data.readUInt32LE(1);
|
|
735
|
+
const unicodeName = _data.subarray(5).toString('utf8');
|
|
736
|
+
|
|
737
|
+
// Calculate CRC32 of the original filename for verification
|
|
738
|
+
const fnameBuf = Buffer.from(this.filename);
|
|
739
|
+
const calculatedCrc = crc32(fnameBuf);
|
|
740
|
+
|
|
741
|
+
const crcMatch = nameCrc32 === calculatedCrc ? 'MATCH' : 'MISMATCH';
|
|
742
|
+
|
|
743
|
+
console.log(` ID[0x${_idStr}] Unicode Path: version=${version}, CRC32=${nameCrc32.toString(16)} (${crcMatch})`);
|
|
744
|
+
console.log(` Path: "${unicodeName}"`);
|
|
745
|
+
} else {
|
|
746
|
+
console.log(` ID[0x${_idStr}] Unicode Path: (invalid length ${_len})`);
|
|
747
|
+
}
|
|
748
|
+
} else {
|
|
749
|
+
// For unknown fields, show a hex preview of the first few bytes
|
|
750
|
+
const preview = _len > 0
|
|
751
|
+
? _data.slice(0, Math.min(16, _len)).toString('hex')
|
|
752
|
+
: '';
|
|
753
|
+
console.log(` ID[0x${_idStr}] Unknown field: length=${_len} bytes${preview ? ', data=' + preview + '...' : ''}`);
|
|
754
|
+
}
|
|
755
|
+
} catch (error: any) {
|
|
756
|
+
console.log(` Error parsing extra field ID[0x${_idStr}]: ${error.message}`);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
i += 4 + _len;
|
|
760
|
+
}
|
|
761
|
+
} catch (error: any) {
|
|
762
|
+
console.log(` Error parsing extra fields: ${error.message}`);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
console.log('\n');
|
|
766
|
+
}
|
|
767
|
+
}
|