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,899 @@
|
|
|
1
|
+
// ======================================
|
|
2
|
+
// ZipKit.ts
|
|
3
|
+
// Copyright (c) 2025 NeoWare, Inc. All rights reserved.
|
|
4
|
+
// ======================================
|
|
5
|
+
|
|
6
|
+
import ZipEntry from './ZipEntry';
|
|
7
|
+
import Errors from './constants/Errors';
|
|
8
|
+
import { crc32, readBigUInt64LE } from './components/Util';
|
|
9
|
+
import { sha256, decrypter, encrypter } from './components/ZipCrypto';
|
|
10
|
+
import Support from './components/Support';
|
|
11
|
+
import pako from 'pako';
|
|
12
|
+
import {
|
|
13
|
+
LOCAL_HDR,
|
|
14
|
+
ENCRYPT_HDR_SIZE,
|
|
15
|
+
CMP_METHOD,
|
|
16
|
+
CENTRAL_END,
|
|
17
|
+
CENTRAL_DIR,
|
|
18
|
+
ZIP64_CENTRAL_END,
|
|
19
|
+
ZIP64_CENTRAL_DIR ,
|
|
20
|
+
GP_FLAG,
|
|
21
|
+
TIMESTAMP_SUBMITTED,
|
|
22
|
+
TIMESTAMP_METADATA,
|
|
23
|
+
TOKENIZED_METADATA,
|
|
24
|
+
LOCAL_FILE_HEADER,
|
|
25
|
+
CENTRAL_FILE_HEADER,
|
|
26
|
+
CENTRAL_DIRECTORY_END,
|
|
27
|
+
HDR_ID
|
|
28
|
+
} from './constants/Headers';
|
|
29
|
+
import HashAccumulater from './components/HashAccumulator';
|
|
30
|
+
import { ZstdInit, ZstdSimple } from '@oneidentity/zstd-js';
|
|
31
|
+
import { NEOZIPKIT_INFO } from './types';
|
|
32
|
+
|
|
33
|
+
// We'll initialize zstd when needed
|
|
34
|
+
let zstdCodec: { ZstdSimple: typeof ZstdSimple } | null = null;
|
|
35
|
+
|
|
36
|
+
// Async initialization function for zstd
|
|
37
|
+
async function initZstd(): Promise<{ ZstdSimple: typeof ZstdSimple }> {
|
|
38
|
+
if (!zstdCodec) {
|
|
39
|
+
zstdCodec = await ZstdInit();
|
|
40
|
+
}
|
|
41
|
+
return zstdCodec;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export {
|
|
45
|
+
ZipEntry,
|
|
46
|
+
Errors,
|
|
47
|
+
CMP_METHOD,
|
|
48
|
+
TIMESTAMP_METADATA,
|
|
49
|
+
TIMESTAMP_SUBMITTED,
|
|
50
|
+
TOKENIZED_METADATA,
|
|
51
|
+
LOCAL_HDR,
|
|
52
|
+
ENCRYPT_HDR_SIZE,
|
|
53
|
+
CENTRAL_END,
|
|
54
|
+
CENTRAL_DIR,
|
|
55
|
+
ZIP64_CENTRAL_END,
|
|
56
|
+
ZIP64_CENTRAL_DIR ,
|
|
57
|
+
GP_FLAG,
|
|
58
|
+
LOCAL_FILE_HEADER,
|
|
59
|
+
CENTRAL_FILE_HEADER,
|
|
60
|
+
CENTRAL_DIRECTORY_END,
|
|
61
|
+
HDR_ID
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Options for compressing files in a ZIP archive
|
|
66
|
+
*/
|
|
67
|
+
export interface CompressOptions {
|
|
68
|
+
level?: number; // Compression level (1-9, 0=store)
|
|
69
|
+
password?: string | null; // Password for encryption
|
|
70
|
+
useSHA256?: boolean; // Whether to calculate SHA256 hash default is false
|
|
71
|
+
useZstd?: boolean; // Whether to use Zstandard compression default is true
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Callbacks for ZIP file creation process
|
|
76
|
+
*/
|
|
77
|
+
export interface CreateZipOptions {
|
|
78
|
+
onError?: (error: Error) => void; // Called when an error occurs
|
|
79
|
+
onEntryDone?: (entry: ZipEntry, status: string) => void; // Called when entry processing completes
|
|
80
|
+
onReadEntryData?: (entry: ZipEntry) => Buffer; // Called to read entry data
|
|
81
|
+
onOutputBuffer?: (data: Buffer) => void; // Called to write output data
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Main class for ZIP file operations including compression, extraction, and encryption
|
|
86
|
+
*/
|
|
87
|
+
export default class ZipKit {
|
|
88
|
+
static readonly version = NEOZIPKIT_INFO.version;
|
|
89
|
+
static readonly releaseDate = NEOZIPKIT_INFO.releaseDate;
|
|
90
|
+
|
|
91
|
+
private inBuffer: Buffer | null = null;
|
|
92
|
+
private zipEntries: ZipEntry[] = [];
|
|
93
|
+
private sortName = false;
|
|
94
|
+
|
|
95
|
+
// Central End Directory values for existing Zip file
|
|
96
|
+
private centralDirSize: number = 0;
|
|
97
|
+
private centralDirOffset: number = 0;
|
|
98
|
+
|
|
99
|
+
// Timestamp values
|
|
100
|
+
private stampEntry: ZipEntry | null = null;
|
|
101
|
+
|
|
102
|
+
zipComment: string | null = null;
|
|
103
|
+
|
|
104
|
+
static support = Support;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Creates a new ZipKit instance
|
|
108
|
+
*/
|
|
109
|
+
constructor() {
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Resets all ZIP-related data to initial state
|
|
114
|
+
*/
|
|
115
|
+
resetZipData() {
|
|
116
|
+
this.inBuffer = null;
|
|
117
|
+
this.zipEntries = [];
|
|
118
|
+
this.centralDirSize = 0;
|
|
119
|
+
this.centralDirOffset = 0;
|
|
120
|
+
this.zipComment = null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Loads a ZIP file from a buffer
|
|
125
|
+
* @param inBuf - Buffer containing ZIP file data
|
|
126
|
+
*/
|
|
127
|
+
loadZip(inBuf: Buffer) {
|
|
128
|
+
this.resetZipData();
|
|
129
|
+
this.inBuffer = inBuf;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Tests if data matches the CRC32 checksum
|
|
134
|
+
* @param entry - ZIP entry to check
|
|
135
|
+
* @param data - Data buffer to verify
|
|
136
|
+
* @returns boolean indicating if CRC matches
|
|
137
|
+
*/
|
|
138
|
+
testCRC32(entry: ZipEntry, data: Buffer): boolean {
|
|
139
|
+
// if bit 3 (0x08) of the general-purpose flags field is set, then the CRC-32 and file sizes are not known when the header is written
|
|
140
|
+
if ((entry.bitFlags & 0x8) !== 0x8) {
|
|
141
|
+
const _crc = crc32(data);
|
|
142
|
+
// console.log(' CRC: '+ sprintf("%08x", _crc));
|
|
143
|
+
// console.log('ent.CRC: ' + sprintf("%08x", entry.crc));
|
|
144
|
+
if (_crc !== entry.crc) {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
} else {
|
|
148
|
+
// @TODO: load and check data descriptor header
|
|
149
|
+
// The fields in the local header are filled with zero, and the CRC-32 and size are appended in a 12-byte structure
|
|
150
|
+
// (optionally preceded by a 4-byte signature) immediately after the compressed data:
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Tests if data matches the SHA256 hash
|
|
158
|
+
* @param entry - ZIP entry to check
|
|
159
|
+
* @param data - Data buffer to verify
|
|
160
|
+
* @returns boolean indicating if SHA256 matches
|
|
161
|
+
* @throws Error if entry has no SHA256 hash
|
|
162
|
+
*/
|
|
163
|
+
testSHA256(entry: ZipEntry, data: Buffer): boolean {
|
|
164
|
+
if (!entry.sha256) {
|
|
165
|
+
throw new Error(Errors.UNKNOWN_SHA256);
|
|
166
|
+
}
|
|
167
|
+
const _sha256 = sha256(data);
|
|
168
|
+
return _sha256 === entry.sha256;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Locates and parses the central end directory
|
|
173
|
+
* @param inBuffer - Buffer containing ZIP file data
|
|
174
|
+
* @returns Number of entries in ZIP file
|
|
175
|
+
* @throws Error if central directory is invalid
|
|
176
|
+
*/
|
|
177
|
+
getCentralEnd(inBuffer: Buffer): number {
|
|
178
|
+
let i = inBuffer.length - CENTRAL_END.SIZE;
|
|
179
|
+
let max = Math.max(0, i - 0xffff); // 0xFFFF is the max zip file comment length
|
|
180
|
+
let endStart = inBuffer.length;
|
|
181
|
+
let endOffset = -1; // Start offset of the END header
|
|
182
|
+
let commentEnd = 0;
|
|
183
|
+
let volEntries = 0;
|
|
184
|
+
let totalEntries = 0;
|
|
185
|
+
|
|
186
|
+
for (let n=max; i >= n; i--) {
|
|
187
|
+
if (inBuffer[i] !== 0x50) continue; // quick check that the byte is 'P'
|
|
188
|
+
if (inBuffer.readUInt32LE(i) === CENTRAL_END.SIGNATURE) {
|
|
189
|
+
// "PK\005\006"
|
|
190
|
+
endOffset = i;
|
|
191
|
+
commentEnd = i;
|
|
192
|
+
endStart = i + CENTRAL_END.SIZE;
|
|
193
|
+
// We already found a regular signature, let's look just a bit further to check if there's any zip64 signature
|
|
194
|
+
n = i - ZIP64_CENTRAL_END.SIZE;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (inBuffer.readUInt32LE(i) === ZIP64_CENTRAL_END.SIGNATURE) {
|
|
199
|
+
// Found a zip64 signature, let's continue reading the whole zip64 record
|
|
200
|
+
n = max;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (inBuffer.readUInt32LE(i) === ZIP64_CENTRAL_DIR.SIGNATURE) {
|
|
205
|
+
// Found the zip64 record, let's determine it's size
|
|
206
|
+
endOffset = i;
|
|
207
|
+
endStart = i + readBigUInt64LE(inBuffer, i + ZIP64_CENTRAL_DIR.SIZE) + ZIP64_CENTRAL_DIR.LEAD;
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (!~endOffset) throw new Error(Errors.INVALID_FORMAT);
|
|
213
|
+
|
|
214
|
+
let data = inBuffer.subarray(endOffset, endStart);
|
|
215
|
+
|
|
216
|
+
if (data.readUInt32LE(0) == CENTRAL_END.SIGNATURE) {
|
|
217
|
+
// number of entries on this volume
|
|
218
|
+
volEntries = data.readUInt16LE(CENTRAL_END.VOL_ENTRIES);
|
|
219
|
+
// total number of entries
|
|
220
|
+
totalEntries = data.readUInt16LE(CENTRAL_END.TOTAL_ENTRIES);
|
|
221
|
+
// central directory size in bytes
|
|
222
|
+
this.centralDirSize = data.readUInt32LE(CENTRAL_END.CENTRAL_DIR_SIZE);
|
|
223
|
+
// offset of first CEN header
|
|
224
|
+
this.centralDirOffset = data.readUInt32LE(CENTRAL_END.CENTRAL_DIR_OFFSET);
|
|
225
|
+
} else {
|
|
226
|
+
// number of entries on this volume
|
|
227
|
+
volEntries = readBigUInt64LE(data, ZIP64_CENTRAL_DIR.VOL_ENTRIES);
|
|
228
|
+
// total number of entries
|
|
229
|
+
totalEntries = readBigUInt64LE(data, ZIP64_CENTRAL_DIR.TOTAL_ENTRIES);
|
|
230
|
+
// central directory size in bytes
|
|
231
|
+
this.centralDirSize = readBigUInt64LE(data, ZIP64_CENTRAL_DIR.CENTRAL_DIR_SIZE);
|
|
232
|
+
// offset of first CEN header
|
|
233
|
+
this.centralDirOffset = readBigUInt64LE(data, ZIP64_CENTRAL_DIR.CENTRAL_DIR_OFFSET);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// zip file comment length
|
|
237
|
+
let commentLength = data.readUInt16LE(CENTRAL_END.ZIP_COMMENT_LEN);
|
|
238
|
+
if (commentLength > 0) {
|
|
239
|
+
this.zipComment = inBuffer.subarray(endStart, endStart + commentLength).toString();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// console.log('totalEntries: ', totalEntries);
|
|
243
|
+
// console.log('centralDirSize; ', this.centralDirSize);
|
|
244
|
+
// console.log('centralDirOffset: ', this.centralDirOffset);
|
|
245
|
+
// console.log('commentLength: ', commentLength);
|
|
246
|
+
// console.log('zipComment: ', this.zipComment);
|
|
247
|
+
|
|
248
|
+
return totalEntries;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Creates the central end directory header
|
|
253
|
+
* @param centralDirSize - Size of central directory
|
|
254
|
+
* @param centralDirOffset - Offset to central directory
|
|
255
|
+
* @returns Buffer containing end directory header
|
|
256
|
+
*/
|
|
257
|
+
centralEndHdr(centralDirSize: number, centralDirOffset: number): Buffer {
|
|
258
|
+
const commentLen = this.zipComment ? this.zipComment.length : 0;
|
|
259
|
+
const data = Buffer.alloc(CENTRAL_END.SIZE + commentLen);
|
|
260
|
+
|
|
261
|
+
// "PK\005\006"
|
|
262
|
+
data.writeUInt32LE(CENTRAL_END.SIGNATURE, 0);
|
|
263
|
+
// number of this disk
|
|
264
|
+
data.writeUInt16LE(0, CENTRAL_END.VOL_NUM);
|
|
265
|
+
// number of the volume/disk with start of the Central Directory
|
|
266
|
+
data.writeUInt16LE(0, CENTRAL_END.VOLDIR_START);
|
|
267
|
+
// number of entries on this volume/disk
|
|
268
|
+
data.writeUInt16LE(0, CENTRAL_END.VOL_ENTRIES);
|
|
269
|
+
// total number of entries on this disk
|
|
270
|
+
data.writeUInt16LE(this.zipEntries.length, CENTRAL_END.TOTAL_ENTRIES);
|
|
271
|
+
// central directory size in bytes
|
|
272
|
+
data.writeUInt32LE(centralDirSize, CENTRAL_END.CENTRAL_DIR_SIZE);
|
|
273
|
+
// offset of first Central Directory Entry
|
|
274
|
+
data.writeUInt32LE(centralDirOffset, CENTRAL_END.CENTRAL_DIR_OFFSET);
|
|
275
|
+
// Zip file comment length
|
|
276
|
+
data.writeUInt16LE(commentLen, CENTRAL_END.ZIP_COMMENT_LEN);
|
|
277
|
+
|
|
278
|
+
if (commentLen > 0 && this.zipComment) {
|
|
279
|
+
const zipCommentBuf = Buffer.from(this.zipComment, 'utf8');
|
|
280
|
+
zipCommentBuf.copy(data, CENTRAL_END.SIZE);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return data;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Reads and parses the ZIP file directory
|
|
288
|
+
* @param debug - Enable debug logging
|
|
289
|
+
* @returns Array of ZIP entries
|
|
290
|
+
*/
|
|
291
|
+
getDirectory(debug?: boolean): ZipEntry[] {
|
|
292
|
+
if (this.zipEntries.length > 0) {
|
|
293
|
+
return this.zipEntries;
|
|
294
|
+
}
|
|
295
|
+
const buffer = this.ensureBuffer();
|
|
296
|
+
|
|
297
|
+
// Add this check for empty buffers - if empty, there's no need to try parsing
|
|
298
|
+
if (buffer.length === 0) {
|
|
299
|
+
// This is a new ZIP file, so we return an empty array
|
|
300
|
+
return this.zipEntries;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const entries = this.getCentralEnd(buffer);
|
|
304
|
+
if (entries === 0) return [];
|
|
305
|
+
|
|
306
|
+
var entryData = buffer.subarray(this.centralDirOffset, this.centralDirOffset + this.centralDirSize);
|
|
307
|
+
if (entryData.readUInt32LE(0) !== CENTRAL_DIR.SIGNATURE) throw new Error(Errors.INVALID_CEN);
|
|
308
|
+
|
|
309
|
+
const entryList: ZipEntry[] = [];
|
|
310
|
+
for (var i=0; i < entries; i++) {
|
|
311
|
+
let entry = new ZipEntry(null, null, debug);
|
|
312
|
+
entryData = entry.readZipEntry(entryData);
|
|
313
|
+
entryList.push(entry);
|
|
314
|
+
// entry.outputZipEntry();
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
this.zipEntries = entryList;
|
|
318
|
+
|
|
319
|
+
// Sort Zip Entries if sortName option set
|
|
320
|
+
if (this.sortName && this.zipEntries.length > 1)
|
|
321
|
+
this.zipEntries.sort((a,b) => {
|
|
322
|
+
return (a.filename || '').toLowerCase()
|
|
323
|
+
.localeCompare((b.filename || '').toLowerCase());
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
return this.zipEntries;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Gets a ZIP entry by filename
|
|
331
|
+
* @param fname - Filename to search for
|
|
332
|
+
* @returns ZipEntry if found, null otherwise
|
|
333
|
+
*/
|
|
334
|
+
getZipEntry(fname: string): ZipEntry | null {
|
|
335
|
+
try {
|
|
336
|
+
const centralDir = this.getDirectory();
|
|
337
|
+
if (centralDir) {
|
|
338
|
+
for (const entry of centralDir) {
|
|
339
|
+
if (entry.filename === fname) {
|
|
340
|
+
return entry;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return null;
|
|
345
|
+
} catch (error) {
|
|
346
|
+
// If we get here, treat as a new file (no existing entry)
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Creates a new ZIP entry
|
|
353
|
+
* @param fname - Filename for new entry
|
|
354
|
+
* @param comment - Optional entry comment
|
|
355
|
+
* @returns Created ZipEntry
|
|
356
|
+
*/
|
|
357
|
+
createZipEntry(fname: string, comment?: string): ZipEntry {
|
|
358
|
+
const entry = new ZipEntry(fname, comment);
|
|
359
|
+
this.zipEntries.push(entry);
|
|
360
|
+
return entry;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Deletes an entry from the ZIP file
|
|
365
|
+
* @param fname - Filename to delete
|
|
366
|
+
* @returns boolean indicating if file was deleted
|
|
367
|
+
*/
|
|
368
|
+
deleteFile(fname: string): boolean {
|
|
369
|
+
const entries = this.ensureZipEntries();
|
|
370
|
+
const index = entries.findIndex(zipEntry => zipEntry.filename === fname);
|
|
371
|
+
if (index < 0) return false;
|
|
372
|
+
entries.splice(index, 1);
|
|
373
|
+
return true;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Parses local header and returns entry data
|
|
378
|
+
* @param entry - ZIP entry to parse
|
|
379
|
+
* @param withLocalHdrData - Include header in returned data
|
|
380
|
+
* @returns Buffer containing entry data
|
|
381
|
+
* @throws Error if header is invalid
|
|
382
|
+
*/
|
|
383
|
+
parseLocalHeader(entry: ZipEntry, withLocalHdrData?: boolean): Buffer {
|
|
384
|
+
const buffer = this.ensureBuffer();
|
|
385
|
+
const localData = buffer.subarray(entry.localHdrOffset);
|
|
386
|
+
|
|
387
|
+
// data should be 30 bytes and start with "PK 03 04"
|
|
388
|
+
if (localData.readUInt32LE(0) !== LOCAL_HDR.SIGNATURE) {
|
|
389
|
+
throw new Error(Errors. INVALID_CEN);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Check for Data Descriptor Flag
|
|
393
|
+
let _encryptLen = 0;
|
|
394
|
+
const _bitFlags = localData.readUInt16LE(LOCAL_HDR.FLAGS);
|
|
395
|
+
if (_bitFlags & GP_FLAG.ENCRYPTED) {
|
|
396
|
+
_encryptLen = ENCRYPT_HDR_SIZE;
|
|
397
|
+
}
|
|
398
|
+
if (_bitFlags & GP_FLAG.DATA_DESC) {
|
|
399
|
+
entry.bitFlags |= GP_FLAG.DATA_DESC;
|
|
400
|
+
entry.lastModTimeDate = localData.readUInt32LE(LOCAL_HDR.TIMEDATE_DOS);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
let _fnameLen = localData.readUInt16LE(LOCAL_HDR.FNAME_LEN);
|
|
404
|
+
let _extraLen = localData.readUInt16LE(LOCAL_HDR.EXTRA_LEN);
|
|
405
|
+
let _localSize = LOCAL_HDR.SIZE + _fnameLen + _extraLen;
|
|
406
|
+
if (_encryptLen != 0) {
|
|
407
|
+
entry.encryptHdr = Buffer.from(localData.subarray(_localSize, _localSize + _encryptLen));
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (withLocalHdrData)
|
|
411
|
+
return localData.subarray(0, _localSize + entry.compressedSize);
|
|
412
|
+
|
|
413
|
+
return localData.subarray(_localSize + _encryptLen, _localSize + entry.compressedSize);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Copies an entry's data from source ZIP
|
|
418
|
+
* @param entry - Entry to copy
|
|
419
|
+
* @returns Buffer containing entry data with headers
|
|
420
|
+
*/
|
|
421
|
+
copyEntry(entry: ZipEntry): Buffer {
|
|
422
|
+
return this.parseLocalHeader(entry, true);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Compresses data for a ZIP entry
|
|
427
|
+
* @param entry - ZIP entry to compress
|
|
428
|
+
* @param data - Data to compress
|
|
429
|
+
* @param options - Compression options
|
|
430
|
+
* @returns Buffer containing compressed data
|
|
431
|
+
*/
|
|
432
|
+
async compressData(entry: ZipEntry, data: Buffer, options?: CompressOptions) : Promise<Buffer> {
|
|
433
|
+
// Initialize with empty buffer instead of null
|
|
434
|
+
let buffer: Buffer = Buffer.alloc(0);
|
|
435
|
+
|
|
436
|
+
// Determine compression method based on options
|
|
437
|
+
if (options?.level === 0) {
|
|
438
|
+
// Level 0 means "store" (no compression)
|
|
439
|
+
entry.cmpMethod = CMP_METHOD.STORED;
|
|
440
|
+
buffer = data;
|
|
441
|
+
} else if (options?.useZstd) {
|
|
442
|
+
// Use Zstandard compression
|
|
443
|
+
if (data.length < 100) {
|
|
444
|
+
// ZStd requires at least 100 bytes, store it without compression
|
|
445
|
+
entry.cmpMethod = CMP_METHOD.STORED;
|
|
446
|
+
buffer = data;
|
|
447
|
+
} else {
|
|
448
|
+
entry.cmpMethod = CMP_METHOD.ZSTD;
|
|
449
|
+
buffer = await this.zstdCompress(data, options);
|
|
450
|
+
}
|
|
451
|
+
} else {
|
|
452
|
+
// Default to deflate
|
|
453
|
+
entry.cmpMethod = CMP_METHOD.DEFLATED;
|
|
454
|
+
buffer = this.deflate(data, options);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
entry.crc = crc32(data);
|
|
458
|
+
// console.log('CRC: '+ sprintf("%08x", entry.crc));
|
|
459
|
+
|
|
460
|
+
if (options?.useSHA256) {
|
|
461
|
+
entry.sha256 = sha256(data);
|
|
462
|
+
console.log('SHA256: ', entry.sha256);
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
if (options?.password) {
|
|
466
|
+
entry.bitFlags = GP_FLAG.ENCRYPTED;
|
|
467
|
+
const _check = (entry.crc >> 24) & 0xff;
|
|
468
|
+
const encryptedBuffer = encrypter(buffer, _check, options.password);
|
|
469
|
+
// Handle potential null return from encrypter
|
|
470
|
+
if (!encryptedBuffer) {
|
|
471
|
+
throw new Error(Errors.ENCRYPT_FAILED);
|
|
472
|
+
}
|
|
473
|
+
buffer = encryptedBuffer;
|
|
474
|
+
}
|
|
475
|
+
entry.compressedSize = buffer.length;
|
|
476
|
+
|
|
477
|
+
return buffer;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Compresses data using deflate algorithm
|
|
482
|
+
* @param inbuf - Data to compress
|
|
483
|
+
* @param options - Compression options
|
|
484
|
+
* @returns Compressed data buffer
|
|
485
|
+
*/
|
|
486
|
+
private deflate(inbuf: Buffer, options?: CompressOptions): Buffer {
|
|
487
|
+
if (options?.level == 0) {
|
|
488
|
+
return inbuf; // Store without compression
|
|
489
|
+
}
|
|
490
|
+
try {
|
|
491
|
+
const level = options?.level as 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | undefined;
|
|
492
|
+
const result = pako.deflateRaw(inbuf, {
|
|
493
|
+
level: level ?? 6
|
|
494
|
+
});
|
|
495
|
+
return Buffer.from(result.buffer, result.byteOffset, result.byteLength);
|
|
496
|
+
} catch (e) {
|
|
497
|
+
console.error('Error during compression:', e);
|
|
498
|
+
throw new Error(Errors.COMPRESSION_ERROR);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Compresses data using Zstandard (zstd) algorithm
|
|
504
|
+
* @param inbuf - Data to compress
|
|
505
|
+
* @param options - Compression options
|
|
506
|
+
* @returns Compressed data buffer
|
|
507
|
+
*/
|
|
508
|
+
private async zstdCompress(inbuf: Buffer, options?: CompressOptions): Promise<Buffer> {
|
|
509
|
+
if (options?.level == 0) {
|
|
510
|
+
return inbuf; // Store without compression
|
|
511
|
+
}
|
|
512
|
+
try {
|
|
513
|
+
// Ensure zstd is initialized
|
|
514
|
+
const codec = await initZstd();
|
|
515
|
+
|
|
516
|
+
// Zstd compression levels range from 1 (fastest) to 22 (highest compression)
|
|
517
|
+
// Map our 1-9 level to a reasonable zstd range (1-19)
|
|
518
|
+
const level = options?.level ?? 6;
|
|
519
|
+
const zstdLevel = Math.min(Math.max(1, Math.floor(level * 2.1)), 19);
|
|
520
|
+
|
|
521
|
+
// Compress the data with zstd
|
|
522
|
+
const compressedData = codec.ZstdSimple.compress(inbuf, zstdLevel);
|
|
523
|
+
return Buffer.from(compressedData);
|
|
524
|
+
} catch (e) {
|
|
525
|
+
console.error('Error during zstd compression:', e);
|
|
526
|
+
throw new Error(Errors.COMPRESSION_ERROR);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Decompresses data using Zstandard (zstd) algorithm
|
|
532
|
+
* @param inbuf - Compressed data buffer
|
|
533
|
+
* @returns Decompressed data buffer
|
|
534
|
+
*/
|
|
535
|
+
async zstdDecompress(inbuf: Buffer): Promise<Buffer> {
|
|
536
|
+
try {
|
|
537
|
+
// Ensure zstd is initialized
|
|
538
|
+
const codec = await initZstd();
|
|
539
|
+
|
|
540
|
+
const decompressedData = codec.ZstdSimple.decompress(inbuf);
|
|
541
|
+
return Buffer.from(decompressedData);
|
|
542
|
+
} catch (e) {
|
|
543
|
+
console.error('Error during zstd decompression:', e);
|
|
544
|
+
throw new Error(Errors.DECOMPRESSION_ERROR);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// ======================================
|
|
549
|
+
// deflateAsync(inbuf: Buffer, callback: (result: Buffer) => void) {
|
|
550
|
+
// var opts = { chunkSize: ((inbuf.length >> 10) + 1) * 1024 };
|
|
551
|
+
|
|
552
|
+
// var tmp = zlib.createDeflateRaw(opts),
|
|
553
|
+
// parts = [],
|
|
554
|
+
// total = 0;
|
|
555
|
+
// tmp.on("data", function (data) {
|
|
556
|
+
// parts.push(data);
|
|
557
|
+
// total += data.length;
|
|
558
|
+
// });
|
|
559
|
+
// tmp.on("end", function () {
|
|
560
|
+
// var buf = Buffer.alloc(total),
|
|
561
|
+
// written = 0;
|
|
562
|
+
// buf.fill(0);
|
|
563
|
+
// for (var i = 0; i < parts.length; i++) {
|
|
564
|
+
// var part = parts[i];
|
|
565
|
+
// part.copy(buf, written);
|
|
566
|
+
// written += part.length;
|
|
567
|
+
// }
|
|
568
|
+
// callback && callback(buf);
|
|
569
|
+
// });
|
|
570
|
+
// tmp.end(inbuf);
|
|
571
|
+
// }
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Creates new Zip data file with the passed ZipEntries
|
|
575
|
+
* @param cmpOptions - Compression options
|
|
576
|
+
* @param zipOptions - Create ZIP options
|
|
577
|
+
* @returns Promise<number> - The offset of the last entry
|
|
578
|
+
*/
|
|
579
|
+
async createZipFileData(cmpOptions?: CompressOptions, zipOptions?: CreateZipOptions): Promise<number> {
|
|
580
|
+
const _onError = zipOptions?.onError || ((error: Error) => { throw error; });
|
|
581
|
+
const _onEntryDone = zipOptions?.onEntryDone || (() => {});
|
|
582
|
+
const _onReadEntryData = zipOptions?.onReadEntryData ||
|
|
583
|
+
(() => { throw new Error('onReadEntryData callback is required'); });
|
|
584
|
+
const _onOutputBuffer = zipOptions?.onOutputBuffer || (() => {});
|
|
585
|
+
|
|
586
|
+
if (this.zipEntries.length === 0) {
|
|
587
|
+
_onError(new Error(Errors.NO_FILES));
|
|
588
|
+
return 0; // Return 0 instead of undefined
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// console.log('Creating Zip File Data with ZipEntries: ', this.zipEntries);
|
|
592
|
+
let offset = 0;
|
|
593
|
+
let entryCnt = this.zipEntries.length;
|
|
594
|
+
for (let index = 0; index < entryCnt; index++) {
|
|
595
|
+
const entry = this.zipEntries[index];
|
|
596
|
+
|
|
597
|
+
// Remove any existing Timestamp Metadata entries
|
|
598
|
+
if (entry.filename === TIMESTAMP_METADATA || entry.filename === TIMESTAMP_SUBMITTED) {
|
|
599
|
+
this.zipEntries.splice(index, 1);
|
|
600
|
+
entryCnt--;
|
|
601
|
+
index--;
|
|
602
|
+
console.log('Deleting Timestamp Metadata entry: ', entry.filename);
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if (!entry.isUpdated) {
|
|
607
|
+
console.log('Copying Entry: ', entry.filename);
|
|
608
|
+
const inData = this.copyEntry(entry);
|
|
609
|
+
entry.localHdrOffset = offset
|
|
610
|
+
offset += inData.length;
|
|
611
|
+
_onOutputBuffer(inData);
|
|
612
|
+
_onEntryDone(entry, 'Copied ');
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Compress the file data to set all zipEntry fields correctly
|
|
617
|
+
const fileBuffer = _onReadEntryData(entry);
|
|
618
|
+
const cmpData = await this.compressData(entry, fileBuffer, cmpOptions);
|
|
619
|
+
|
|
620
|
+
// Create a local header data buffer
|
|
621
|
+
const localHdr = entry.createLocalHdr();
|
|
622
|
+
|
|
623
|
+
// Save local header offset for entry and calculate next offset
|
|
624
|
+
entry.localHdrOffset = offset;
|
|
625
|
+
|
|
626
|
+
offset += localHdr.length + cmpData.length;
|
|
627
|
+
|
|
628
|
+
// Write the data to the output file
|
|
629
|
+
_onOutputBuffer(localHdr);
|
|
630
|
+
_onOutputBuffer(cmpData);
|
|
631
|
+
_onEntryDone(entry, 'Written');
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
return offset;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Finishes Zip data file by adding any Metadata entries and the Central End Directory
|
|
639
|
+
* @param offset - The current offset in the zip data file
|
|
640
|
+
* @param onOutputBuffer - The callback function to output the data buffer
|
|
641
|
+
* @param metaEntry - The Metadata ZipEntry to add to the Zip file
|
|
642
|
+
*/
|
|
643
|
+
async finishZipFileData(offset: number, onOutputBuffer: (data: Buffer) => void, metaEntry?: ZipEntry) {
|
|
644
|
+
if (metaEntry && metaEntry.fileBuffer) {
|
|
645
|
+
const localHdr = metaEntry.createLocalHdr();
|
|
646
|
+
metaEntry.localHdrOffset = offset;
|
|
647
|
+
offset += localHdr.length + metaEntry.fileBuffer.length;
|
|
648
|
+
|
|
649
|
+
onOutputBuffer(localHdr);
|
|
650
|
+
onOutputBuffer(metaEntry.fileBuffer);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// Output ZipEntries as Central End Directory
|
|
654
|
+
const centralDirOffset = offset;
|
|
655
|
+
for (const entry of this.zipEntries) {
|
|
656
|
+
const centralDirEntry = entry.centralDirEntry();
|
|
657
|
+
offset += centralDirEntry.length;
|
|
658
|
+
onOutputBuffer(centralDirEntry);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const centralDirSize = offset - centralDirOffset;
|
|
662
|
+
const ceBuf = this.centralEndHdr(centralDirSize, centralDirOffset);
|
|
663
|
+
onOutputBuffer(ceBuf);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Unzips the data buffer using pako inflate method
|
|
668
|
+
* @param inbuf - Data buffer to inflate
|
|
669
|
+
* @returns Inflated data buffer
|
|
670
|
+
* @throws Error if extraction fails
|
|
671
|
+
*/
|
|
672
|
+
inflate(inbuf: Buffer): Buffer {
|
|
673
|
+
const result = pako.inflateRaw(inbuf);
|
|
674
|
+
return Buffer.from(result.buffer, result.byteOffset, result.byteLength);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* Extracts the data from the ZipEntry and returns the data as a Buffer
|
|
679
|
+
* @param entry - ZIP entry to extract
|
|
680
|
+
* @param skipHashCheck - Whether to skip hash checking
|
|
681
|
+
* @returns Buffer containing extracted data or null if extraction fails
|
|
682
|
+
*/
|
|
683
|
+
async extract(entry: ZipEntry, skipHashCheck?: boolean): Promise<Buffer | null> {
|
|
684
|
+
const fdata = this.parseLocalHeader(entry);
|
|
685
|
+
if (fdata.length == 0) {
|
|
686
|
+
return null;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
let outBuf = null;
|
|
690
|
+
switch (entry.cmpMethod) {
|
|
691
|
+
case CMP_METHOD.STORED:
|
|
692
|
+
outBuf = fdata;
|
|
693
|
+
break;
|
|
694
|
+
case CMP_METHOD.DEFLATED:
|
|
695
|
+
outBuf = this.inflate(fdata);
|
|
696
|
+
break;
|
|
697
|
+
case CMP_METHOD.ZSTD:
|
|
698
|
+
outBuf = await this.zstdDecompress(fdata);
|
|
699
|
+
break;
|
|
700
|
+
default:
|
|
701
|
+
throw new Error(Errors.UNKNOWN_METHOD);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// If no hash checking is requested, return the unmodified data
|
|
705
|
+
if (skipHashCheck) {
|
|
706
|
+
return outBuf;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// Verify integrity - use SHA256 if available, otherwise CRC32
|
|
710
|
+
if (entry.sha256) {
|
|
711
|
+
if (!this.testSHA256(entry, outBuf)) {
|
|
712
|
+
throw new Error(Errors.INVALID_SHA256);
|
|
713
|
+
}
|
|
714
|
+
} else {
|
|
715
|
+
if (!this.testCRC32(entry, outBuf)) {
|
|
716
|
+
throw new Error(Errors.INVALID_CRC);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
return outBuf;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* Decrypts the entry data
|
|
725
|
+
* @param entry - ZIP entry to decrypt
|
|
726
|
+
* @param password - Password for decryption
|
|
727
|
+
* @returns Decrypted data buffer or null if decryption fails
|
|
728
|
+
*/
|
|
729
|
+
decryptEntry(entry: ZipEntry, password: string): Buffer | null {
|
|
730
|
+
if (!entry.isEncrypted) return null;
|
|
731
|
+
if (entry.isStrongEncrypt)
|
|
732
|
+
throw new Error(Errors.STRONG_ENCRYPT_UNSUPPORTED);
|
|
733
|
+
if (!password) return null;
|
|
734
|
+
|
|
735
|
+
// Read the encryption header at start of data
|
|
736
|
+
let _localData = this.parseLocalHeader(entry);
|
|
737
|
+
const _decrypt = decrypter(password);
|
|
738
|
+
|
|
739
|
+
const _encHdr = entry.encryptHdr;
|
|
740
|
+
if (!_encHdr) return null;
|
|
741
|
+
|
|
742
|
+
const decryptedHeader = _decrypt(_encHdr);
|
|
743
|
+
|
|
744
|
+
const _check = (entry.bitFlags & GP_FLAG.DATA_DESC) ?
|
|
745
|
+
(entry.lastModTimeDate >> 8) & 0xff :
|
|
746
|
+
((entry.crc >> 24) & 0xff);
|
|
747
|
+
|
|
748
|
+
if (decryptedHeader[11] !== _check)
|
|
749
|
+
return null;
|
|
750
|
+
|
|
751
|
+
_localData = _decrypt(_localData);
|
|
752
|
+
return _localData;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/**
|
|
756
|
+
* Checks if a filename is a blockchain metadata file that should be excluded from Merkle Root calculation
|
|
757
|
+
* @param filename - The filename to check
|
|
758
|
+
* @returns boolean - True if the file is a metadata file
|
|
759
|
+
*/
|
|
760
|
+
private isMetadataFile(filename: string): boolean {
|
|
761
|
+
return filename === TIMESTAMP_SUBMITTED ||
|
|
762
|
+
filename === TIMESTAMP_METADATA ||
|
|
763
|
+
filename === TOKENIZED_METADATA;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* Calculates the Merkle Root of the ZIP file
|
|
768
|
+
* Excludes blockchain metadata files (META-INF/*) to ensure consistent calculation
|
|
769
|
+
* @returns string - The Merkle Root of the ZIP file
|
|
770
|
+
*/
|
|
771
|
+
getMerkleRoot(): string | null {
|
|
772
|
+
const zipEntries = this.getDirectory();
|
|
773
|
+
if (zipEntries.length === 0) return null;
|
|
774
|
+
|
|
775
|
+
let _hashAccumulator = new HashAccumulater();
|
|
776
|
+
|
|
777
|
+
// Filter out blockchain metadata files to ensure consistent Merkle Root calculation
|
|
778
|
+
const contentEntries = zipEntries.filter(entry => !this.isMetadataFile(entry.filename));
|
|
779
|
+
|
|
780
|
+
for (const entry of contentEntries) {
|
|
781
|
+
if (entry.sha256) {
|
|
782
|
+
_hashAccumulator.addHash(entry.sha256);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
const merkleRoot = _hashAccumulator.merkleRoot();
|
|
787
|
+
if (!merkleRoot) return null;
|
|
788
|
+
|
|
789
|
+
return merkleRoot;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* Checks if a ZIP file contains valid timestamp metadata
|
|
794
|
+
* @returns { merkleRoot: string | null, ots: Buffer | null }
|
|
795
|
+
* Returns the merkle root hash and timestamp metadata if found, null otherwise
|
|
796
|
+
*/
|
|
797
|
+
checkForTimestamp(): string | null {
|
|
798
|
+
const zipEntries = this.getDirectory();
|
|
799
|
+
if (zipEntries.length === 0) return null;
|
|
800
|
+
|
|
801
|
+
this.stampEntry = null;
|
|
802
|
+
let _hashAccumulator = new HashAccumulater();
|
|
803
|
+
for (const entry of zipEntries) {
|
|
804
|
+
if (entry.filename === TIMESTAMP_METADATA) {
|
|
805
|
+
this.stampEntry = entry;
|
|
806
|
+
} else if (entry.filename === TIMESTAMP_SUBMITTED) {
|
|
807
|
+
if (this.stampEntry == null)
|
|
808
|
+
this.stampEntry = entry;
|
|
809
|
+
} else if (!this.isMetadataFile(entry.filename) && entry.sha256) {
|
|
810
|
+
// Only include content files (non-metadata) in Merkle Root calculation
|
|
811
|
+
_hashAccumulator.addHash(entry.sha256);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
if (!this.stampEntry) return null;
|
|
815
|
+
|
|
816
|
+
const merkleRoot = _hashAccumulator.merkleRoot();
|
|
817
|
+
return merkleRoot;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/**
|
|
821
|
+
* Gets the OTS from the timestamp metadata entry
|
|
822
|
+
* @returns Promise<Buffer | null> - The OTS from the timestamp metadata entry or null if not found
|
|
823
|
+
*/
|
|
824
|
+
async getOts(): Promise<Buffer | null> {
|
|
825
|
+
if (!this.stampEntry) return null;
|
|
826
|
+
const ots = await this.extract(this.stampEntry);
|
|
827
|
+
return ots;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* Creates a new timestamp metadata entry for the ZIP file
|
|
832
|
+
* Calculates merkle root from file hashes and obtains blockchain timestamp
|
|
833
|
+
* @returns Promise<ZipEntry | null> The created timestamp metadata entry or null if failed
|
|
834
|
+
*/
|
|
835
|
+
createTimestampMetadataEntry(ots: Buffer | null): ZipEntry | null {
|
|
836
|
+
if (!ots) return null;
|
|
837
|
+
const tsEntry = this.createZipEntry(TIMESTAMP_SUBMITTED);
|
|
838
|
+
tsEntry.crc = crc32(ots);
|
|
839
|
+
tsEntry.uncompressedSize = ots.length;
|
|
840
|
+
tsEntry.compressedSize = ots.length;
|
|
841
|
+
tsEntry.cmpMethod = CMP_METHOD.STORED;
|
|
842
|
+
tsEntry.fileBuffer = ots;
|
|
843
|
+
tsEntry.isMetaData = true;
|
|
844
|
+
|
|
845
|
+
return tsEntry;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* Sets the ZIP file comment
|
|
850
|
+
* @param msg - Comment to set
|
|
851
|
+
*/
|
|
852
|
+
setZipComment(msg: string | null) {
|
|
853
|
+
this.zipComment = msg;
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* Gets the ZIP file comment
|
|
857
|
+
* @returns string | null - The ZIP file comment
|
|
858
|
+
*/
|
|
859
|
+
getZipComment(): string | null {
|
|
860
|
+
return this.zipComment;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
/**
|
|
864
|
+
* Converts the compression method to a string
|
|
865
|
+
* @param cmpMethod - Compression method number
|
|
866
|
+
* @returns string - Compression method as a string
|
|
867
|
+
*/
|
|
868
|
+
cmpMethodToString(cmpMethod: number): string {
|
|
869
|
+
switch (cmpMethod) {
|
|
870
|
+
case 0: return 'Stored';
|
|
871
|
+
case 8: return 'Deflate';
|
|
872
|
+
case 93: return 'Zstd';
|
|
873
|
+
default: return 'Unknown';
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* Ensures ZIP entries are loaded
|
|
879
|
+
* @returns Array of ZIP entries
|
|
880
|
+
* @throws Error if no ZIP entries are loaded
|
|
881
|
+
*/
|
|
882
|
+
private ensureZipEntries(): ZipEntry[] {
|
|
883
|
+
if (this.zipEntries.length === 0) throw new Error('No ZIP entries loaded');
|
|
884
|
+
return this.zipEntries;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
/**
|
|
888
|
+
* Ensures ZIP data buffer is loaded
|
|
889
|
+
* @returns Buffer containing ZIP data
|
|
890
|
+
* @throws Error if no ZIP data is loaded
|
|
891
|
+
*/
|
|
892
|
+
private ensureBuffer(): Buffer {
|
|
893
|
+
if (!this.inBuffer) {
|
|
894
|
+
// Instead of throwing an error, return an empty buffer
|
|
895
|
+
this.inBuffer = Buffer.alloc(0);
|
|
896
|
+
}
|
|
897
|
+
return this.inBuffer;
|
|
898
|
+
}
|
|
899
|
+
}
|