Spaces:
Running
Running
coyotte508
commited on
Commit
โข
0914da1
1
Parent(s):
8d0e117
๐
Browse files- README.md +5 -0
- src/lib/check-dduf.ts +129 -28
- 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(
|
|
|
|
|
|
|
5 |
const blob = await WebBlob.create(new URL(url));
|
6 |
|
7 |
-
|
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 |
-
|
32 |
|
33 |
const diskNumber = view.getUint16(index + 4, true);
|
34 |
|
35 |
-
|
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 |
-
|
44 |
-
|
45 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
46 |
|
47 |
-
|
48 |
-
|
49 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
90 |
-
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
105 |
|
106 |
-
|
|
|
|
|
107 |
|
108 |
offset += 46 + filenameLength + extraFieldLength + commentLength;
|
|
|
|
|
109 |
}
|
110 |
|
111 |
-
|
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
|
16 |
-
|
17 |
-
|
18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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>
|