coyotte508 commited on
Commit
0914da1
โ€ข
1 Parent(s): 8d0e117
Files changed (3) hide show
  1. README.md +5 -0
  2. src/lib/check-dduf.ts +129 -28
  3. src/routes/+page.svelte +27 -7
README.md CHANGED
@@ -23,3 +23,8 @@ You can try zip 64 format by running the following command:
23
  ```bash
24
  cd dduf-content && zip -r -0 -fz ../file-64.dduf .
25
  ```
 
 
 
 
 
 
23
  ```bash
24
  cd dduf-content && zip -r -0 -fz ../file-64.dduf .
25
  ```
26
+
27
+ ## Credits
28
+
29
+ - https://en.wikipedia.org/wiki/ZIP_(file_format)
30
+ - https://blog.yaakov.online/zip64-go-big-or-go-home/
src/lib/check-dduf.ts CHANGED
@@ -1,10 +1,13 @@
1
  import { checkFilename } from './check-filename';
2
  import { WebBlob } from './WebBlob';
3
 
4
- export async function* checkDduf(url: string): AsyncGenerator<string> {
 
 
 
5
  const blob = await WebBlob.create(new URL(url));
6
 
7
- yield 'File size: ' + blob.size;
8
 
9
  // DDUF is a zip file, uncompressed.
10
 
@@ -28,25 +31,80 @@ export async function* checkDduf(url: string): AsyncGenerator<string> {
28
  throw new Error('DDUF footer not found in last 100kB of file');
29
  }
30
 
31
- yield 'DDUF footer found at offset ' + (blob.size - last100kB.byteLength + index);
32
 
33
  const diskNumber = view.getUint16(index + 4, true);
34
 
35
- if (diskNumber === 0xffff) {
36
- throw new Error('Spanned archives (ZIP64) not yet supported');
37
- }
38
 
39
- if (diskNumber !== 0) {
40
  throw new Error('Multi-disk archives not supported');
41
  }
42
 
43
- const fileCount = view.getUint16(index + 10, true);
44
- const centralDirSize = view.getUint32(index + 12, true);
45
- const centralDirOffset = view.getUint32(index + 16, true);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
 
47
- yield 'File count: ' + fileCount;
48
- yield 'Central directory size: ' + centralDirSize;
49
- yield 'Central directory offset: ' + centralDirOffset;
 
 
 
 
 
 
50
 
51
  const centralDir =
52
  centralDirOffset > blob.size - last100kB.byteLength
@@ -74,39 +132,82 @@ export async function* checkDduf(url: string): AsyncGenerator<string> {
74
  throw new Error('Unsupported compression method: ' + compressionMethod);
75
  }
76
 
77
- const size = centralDirView.getUint32(offset + 24, true);
78
- const compressedSize = centralDirView.getUint32(offset + 20, true);
79
-
80
- if (size !== compressedSize) {
81
- throw new Error('Compressed size and size differ');
82
- }
83
-
84
  const filenameLength = centralDirView.getUint16(offset + 28, true);
85
  const fileName = new TextDecoder().decode(
86
  new Uint8Array(centralDir, offset + 46, filenameLength)
87
  );
88
 
89
- yield 'File ' + i;
90
- yield 'File name: ' + fileName;
91
- yield 'File size: ' + size;
92
 
93
  checkFilename(fileName);
94
 
95
  const fileDiskNumber = centralDirView.getUint16(34, true);
96
 
97
- if (fileDiskNumber !== 0) {
98
  throw new Error('Multi-disk archives not supported');
99
  }
100
 
 
 
 
 
101
  const extraFieldLength = centralDirView.getUint16(offset + 30, true);
102
- const commentLength = centralDirView.getUint16(offset + 32, true);
103
 
104
- const filePosition = centralDirView.getUint32(offset + 42, true);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
 
106
- yield 'File position in archive: ' + filePosition;
 
 
107
 
108
  offset += 46 + filenameLength + extraFieldLength + commentLength;
 
 
109
  }
110
 
111
- yield 'All files checked';
112
  }
 
1
  import { checkFilename } from './check-filename';
2
  import { WebBlob } from './WebBlob';
3
 
4
+ export async function* checkDduf(
5
+ url: string,
6
+ opts?: { log?: (x: string) => void }
7
+ ): AsyncGenerator<{ type: 'file'; name: string; size: number; fileHeaderOffset: number }> {
8
  const blob = await WebBlob.create(new URL(url));
9
 
10
+ opts?.log?.('File size: ' + blob.size);
11
 
12
  // DDUF is a zip file, uncompressed.
13
 
 
31
  throw new Error('DDUF footer not found in last 100kB of file');
32
  }
33
 
34
+ opts?.log?.('DDUF footer found at offset ' + (blob.size - last100kB.byteLength + index));
35
 
36
  const diskNumber = view.getUint16(index + 4, true);
37
 
38
+ opts?.log?.('Disk number: ' + diskNumber);
 
 
39
 
40
+ if (diskNumber !== 0 && diskNumber !== 0xffff) {
41
  throw new Error('Multi-disk archives not supported');
42
  }
43
 
44
+ let fileCount = view.getUint16(index + 10, true);
45
+ let centralDirSize = view.getUint32(index + 12, true);
46
+ let centralDirOffset = view.getUint32(index + 16, true);
47
+ const isZip64 = centralDirOffset === 0xffffffff;
48
+
49
+ opts?.log?.('File count: ' + fileCount);
50
+
51
+ if (isZip64) {
52
+ opts?.log?.('Zip64 format detected');
53
+
54
+ index -= 20;
55
+ while (index >= 0) {
56
+ if (view.getUint32(index, true) === 0x07064b50) {
57
+ found = true;
58
+ break;
59
+ }
60
+
61
+ index--;
62
+ }
63
+
64
+ if (!found) {
65
+ throw new Error('Zip64 footer not found in last 100kB of file');
66
+ }
67
+
68
+ opts?.log?.('Zip64 footer found at offset ' + (blob.size - last100kB.byteLength + index));
69
+
70
+ const diskWithCentralDir = view.getUint32(index + 4, true);
71
+
72
+ if (diskWithCentralDir !== 0) {
73
+ throw new Error('Multi-disk archives not supported');
74
+ }
75
+
76
+ const endCentralDirOffset = Number(view.getBigUint64(index + 8, true));
77
+
78
+ index = endCentralDirOffset - (blob.size - last100kB.byteLength);
79
+
80
+ if (index < 0) {
81
+ throw new Error('Central directory offset is outside the last 100kB of the file');
82
+ }
83
+
84
+ if (view.getUint32(index, true) !== 0x06064b50) {
85
+ throw new Error('Invalid central directory header');
86
+ }
87
+
88
+ const thisDisk = view.getUint16(index + 16, true);
89
+ const centralDirDisk = view.getUint16(index + 20, true);
90
+
91
+ if (thisDisk !== 0) {
92
+ throw new Error('Multi-disk archives not supported');
93
+ }
94
+
95
+ if (centralDirDisk !== 0) {
96
+ throw new Error('Multi-disk archives not supported');
97
+ }
98
 
99
+ centralDirSize = Number(view.getBigUint64(index + 40, true));
100
+ centralDirOffset = Number(view.getBigUint64(index + 48, true));
101
+ fileCount = Number(view.getBigUint64(index + 32, true));
102
+
103
+ opts?.log?.('File count zip 64: ' + fileCount);
104
+ }
105
+
106
+ opts?.log?.('Central directory size: ' + centralDirSize);
107
+ opts?.log?.('Central directory offset: ' + centralDirOffset);
108
 
109
  const centralDir =
110
  centralDirOffset > blob.size - last100kB.byteLength
 
132
  throw new Error('Unsupported compression method: ' + compressionMethod);
133
  }
134
 
 
 
 
 
 
 
 
135
  const filenameLength = centralDirView.getUint16(offset + 28, true);
136
  const fileName = new TextDecoder().decode(
137
  new Uint8Array(centralDir, offset + 46, filenameLength)
138
  );
139
 
140
+ opts?.log?.('File ' + i);
141
+ opts?.log?.('File name: ' + fileName);
 
142
 
143
  checkFilename(fileName);
144
 
145
  const fileDiskNumber = centralDirView.getUint16(34, true);
146
 
147
+ if (fileDiskNumber !== 0 && fileDiskNumber !== 0xffff) {
148
  throw new Error('Multi-disk archives not supported');
149
  }
150
 
151
+ let size = centralDirView.getUint32(offset + 24, true);
152
+ let compressedSize = centralDirView.getUint32(offset + 20, true);
153
+ let filePosition = centralDirView.getUint32(offset + 42, true);
154
+
155
  const extraFieldLength = centralDirView.getUint16(offset + 30, true);
 
156
 
157
+ if (size === 0xffffffff || compressedSize === 0xffffffff || filePosition === 0xffffffff) {
158
+ opts?.log?.('File size is in zip64 format');
159
+
160
+ const extraFields = new DataView(centralDir, offset + 46 + filenameLength, extraFieldLength);
161
+
162
+ let extraFieldOffset = 0;
163
+
164
+ while (extraFieldOffset < extraFieldLength) {
165
+ const headerId = extraFields.getUint16(extraFieldOffset, true);
166
+ const extraFieldSize = extraFields.getUint16(extraFieldOffset + 2, true);
167
+ if (headerId !== 0x0001) {
168
+ extraFieldOffset += 4 + extraFieldSize;
169
+ continue;
170
+ }
171
+
172
+ const zip64ExtraField = new DataView(
173
+ centralDir,
174
+ offset + 46 + filenameLength + extraFieldOffset + 4,
175
+ extraFieldSize
176
+ );
177
+ let zip64ExtraFieldOffset = 0;
178
+
179
+ if (size === 0xffffffff) {
180
+ size = Number(zip64ExtraField.getBigUint64(zip64ExtraFieldOffset, true));
181
+ zip64ExtraFieldOffset += 8;
182
+ }
183
+
184
+ if (compressedSize === 0xffffffff) {
185
+ compressedSize = Number(zip64ExtraField.getBigUint64(zip64ExtraFieldOffset, true));
186
+ zip64ExtraFieldOffset += 8;
187
+ }
188
+
189
+ if (filePosition === 0xffffffff) {
190
+ filePosition = Number(zip64ExtraField.getBigUint64(zip64ExtraFieldOffset, true));
191
+ zip64ExtraFieldOffset += 8;
192
+ }
193
+
194
+ break;
195
+ }
196
+ }
197
+
198
+ if (size !== compressedSize) {
199
+ throw new Error('Compressed size and size differ: ' + compressedSize + ' vs ' + size);
200
+ }
201
+ opts?.log?.('File size: ' + size);
202
 
203
+ const commentLength = centralDirView.getUint16(offset + 32, true);
204
+
205
+ opts?.log?.('File header position in archive: ' + filePosition);
206
 
207
  offset += 46 + filenameLength + extraFieldLength + commentLength;
208
+
209
+ yield { type: 'file', name: fileName, size, fileHeaderOffset: filePosition };
210
  }
211
 
212
+ opts?.log?.('All files checked');
213
  }
src/routes/+page.svelte CHANGED
@@ -2,9 +2,10 @@
2
  import { checkDduf } from '$lib/check-dduf';
3
  import { tick } from 'svelte';
4
 
5
- let url = 'https://huggingface.co/spaces/coyotte508/dduf-check/resolve/main/file.dduf';
6
- let output = '';
7
- let error = '';
 
8
 
9
  async function handleSubmit(event: Event) {
10
  event.preventDefault();
@@ -12,10 +13,18 @@
12
  error = '';
13
 
14
  try {
15
- for await (const str of checkDduf(url)) {
16
- output += '\n' + str;
17
- tick().then(() => {
18
- textarea.scrollTop = textarea.scrollHeight;
 
 
 
 
 
 
 
 
19
  });
20
  }
21
  } catch (e) {
@@ -54,5 +63,16 @@
54
  {#if error}
55
  <p class="text-red-500">{error}</p>
56
  {/if}
 
 
 
 
 
 
 
 
 
 
 
57
  </form>
58
  </div>
 
2
  import { checkDduf } from '$lib/check-dduf';
3
  import { tick } from 'svelte';
4
 
5
+ let url = $state('https://huggingface.co/spaces/coyotte508/dduf-check/resolve/main/file.dduf');
6
+ let output = $state('');
7
+ let error = $state('');
8
+ let files: Array<{ position: number; size: number; name: string }> = $state([]);
9
 
10
  async function handleSubmit(event: Event) {
11
  event.preventDefault();
 
13
  error = '';
14
 
15
  try {
16
+ for await (const file of checkDduf(url, {
17
+ log: (s) => {
18
+ output += '\n' + s;
19
+ tick().then(() => {
20
+ textarea.scrollTop = textarea.scrollHeight;
21
+ });
22
+ }
23
+ })) {
24
+ files.push({
25
+ position: file.fileHeaderOffset,
26
+ size: file.size,
27
+ name: file.name
28
  });
29
  }
30
  } catch (e) {
 
63
  {#if error}
64
  <p class="text-red-500">{error}</p>
65
  {/if}
66
+
67
+ {#if files.length > 0}
68
+ <h2 class="text-lg font-bold">Files</h2>
69
+ <ul class="ml-4 list-disc">
70
+ {#each files as file}
71
+ <li style="margin-left: {file.name.slice(0, -1).split('/').length - 1}rem">
72
+ {file.name} - {file.size} B
73
+ </li>
74
+ {/each}
75
+ </ul>
76
+ {/if}
77
  </form>
78
  </div>