|
1 |
| -import type { FileStat } from '@/types'; |
| 1 | +import { FileStat, HeaderOffset } from '@/types'; |
2 | 2 | import fc from 'fast-check';
|
3 | 3 | import { EntryType } from '@/types';
|
4 | 4 | import * as tarUtils from '@/utils';
|
@@ -34,7 +34,7 @@ function splitHeaderData(data: Uint8Array) {
|
34 | 34 | }
|
35 | 35 |
|
36 | 36 | const filenameArb = fc
|
37 |
| - .string({ minLength: 1, maxLength: 255 }) |
| 37 | + .string({ minLength: 1, maxLength: 32 }) |
38 | 38 | .filter((name) => !name.includes('/') && name !== '.' && name !== '..')
|
39 | 39 | .noShrink();
|
40 | 40 |
|
@@ -116,74 +116,84 @@ const virtualFsArb = fc
|
116 | 116 | .noShrink();
|
117 | 117 |
|
118 | 118 | const tarHeaderArb = fc
|
119 |
| - .tuple( |
120 |
| - fc.option(fc.string({ minLength: 1, maxLength: 50 }), { nil: undefined }), // Optional directory |
121 |
| - fc.string({ minLength: 1, maxLength: 50 }), // Filename |
122 |
| - fc.constant('0000777\0'), // Mode (octal) |
123 |
| - fc.constant('0000000\0'), // UID (octal) |
124 |
| - fc.constant('0000000\0'), // GID (octal) |
125 |
| - fc.nat(65536), // File size (up to 1MB for files, 0 for directories) |
126 |
| - fc.constant('0000000\0'), // Mtime (octal) |
127 |
| - fc.constant(' '), // Placeholder checksum (8 spaces) |
128 |
| - fc.constantFrom('0', '5'), // Typeflag ('0' for file, '5' for directory) |
129 |
| - fc.constant(''.padEnd(100, '\0')), // USTAR format padding |
130 |
| - ) |
131 |
| - .map(([dir, name, mode, uid, gid, size, , , typeflag]) => { |
| 119 | + .record({ |
| 120 | + path: filenameArb, |
| 121 | + uid: fc.nat(65535), |
| 122 | + gid: fc.nat(65535), |
| 123 | + size: fc.nat(65536), |
| 124 | + typeflag: fc.constantFrom('0', '5'), |
| 125 | + }) |
| 126 | + .map(({ path, uid, gid, size, typeflag }) => { |
132 | 127 | const header = new Uint8Array(tarConstants.BLOCK_SIZE);
|
133 | 128 | const type = typeflag as '0' | '5';
|
134 | 129 | const encoder = new TextEncoder();
|
135 | 130 |
|
136 | 131 | if (type === '5') size = 0;
|
137 | 132 |
|
138 |
| - // If nested, prepend directory name |
139 |
| - const fullPath = dir ? `${dir}/${name}` : name; |
140 |
| - |
141 | 133 | // Fill header fields
|
142 |
| - header.set(encoder.encode(fullPath), 0); |
143 |
| - header.set(encoder.encode(mode), 100); |
144 |
| - header.set(encoder.encode(uid), 108); |
145 |
| - header.set(encoder.encode(gid), 116); |
146 |
| - header.set(encoder.encode(size.toString(8).padStart(11, '0') + '\0'), 124); |
147 |
| - header.set(encoder.encode('0000000\0'), 136); // Mtime |
148 |
| - header.set(encoder.encode(' '), 148); // Checksum placeholder |
149 |
| - header.set(encoder.encode(type), 156); |
150 |
| - header.set(encoder.encode('ustar '), 257); |
| 134 | + header.set(encoder.encode(path), HeaderOffset.FILE_NAME); |
| 135 | + header.set(encoder.encode('0000777'), HeaderOffset.FILE_MODE); |
| 136 | + header.set( |
| 137 | + encoder.encode(uid.toString(8).padStart(7, '0')), |
| 138 | + HeaderOffset.OWNER_UID, |
| 139 | + ); |
| 140 | + header.set( |
| 141 | + encoder.encode(gid.toString(8).padStart(7, '0')), |
| 142 | + HeaderOffset.OWNER_GID, |
| 143 | + ); |
| 144 | + header.set( |
| 145 | + encoder.encode(size.toString(8).padStart(11, '0') + '\0'), |
| 146 | + HeaderOffset.FILE_SIZE, |
| 147 | + ); |
| 148 | + header.set(encoder.encode(' '), HeaderOffset.CHECKSUM); |
| 149 | + header.set(encoder.encode(type), HeaderOffset.TYPE_FLAG); |
| 150 | + header.set(encoder.encode('ustar '), HeaderOffset.USTAR_NAME); |
151 | 151 |
|
152 | 152 | // Compute and set checksum
|
153 | 153 | const checksum = header.reduce((sum, byte) => sum + byte, 0);
|
154 | 154 | header.set(
|
155 | 155 | encoder.encode(checksum.toString(8).padStart(6, '0') + '\0 '),
|
156 |
| - 148, |
| 156 | + HeaderOffset.CHECKSUM, |
157 | 157 | );
|
158 | 158 |
|
159 |
| - return { header, size, type, details: { fullPath, mode, uid, gid } }; |
| 159 | + return { header, stat: { type, size, path, uid, gid } }; |
160 | 160 | })
|
161 | 161 | .noShrink();
|
162 | 162 |
|
163 | 163 | const tarDataArb = tarHeaderArb
|
164 | 164 | .chain((header) =>
|
165 | 165 | fc
|
166 |
| - .tuple( |
167 |
| - fc.constant(header), |
168 |
| - fc.string({ minLength: header.size, maxLength: header.size }), |
169 |
| - ) |
170 |
| - .map(([header, data]) => { |
171 |
| - const { header: headerBlock, size, type } = header; |
| 166 | + .record({ |
| 167 | + header: fc.constant(header), |
| 168 | + data: fc.string({ |
| 169 | + minLength: header.stat.size, |
| 170 | + maxLength: header.stat.size, |
| 171 | + }), |
| 172 | + }) |
| 173 | + .map(({ header, data }) => { |
| 174 | + const { header: headerBlock, stat } = header; |
172 | 175 | const encoder = new TextEncoder();
|
173 | 176 | const encodedData = encoder.encode(data);
|
174 | 177 |
|
175 |
| - // Directories don't have data |
| 178 | + // Directories don't have any data, so set their size to zero. |
176 | 179 | let dataBlock: Uint8Array;
|
177 |
| - if (type === '0') { |
178 |
| - const paddedSize = |
179 |
| - Math.ceil(size / tarConstants.BLOCK_SIZE) * tarConstants.BLOCK_SIZE; |
180 |
| - dataBlock = new Uint8Array(paddedSize); |
181 |
| - dataBlock.set(encodedData.subarray(0, size)); // Subarray avoids unnecessary copy |
| 180 | + if (stat.type === '0') { |
| 181 | + // Make sure the data is aligned to 512-byte chunks |
| 182 | + dataBlock = new Uint8Array( |
| 183 | + Math.ceil(stat.size / tarConstants.BLOCK_SIZE) * |
| 184 | + tarConstants.BLOCK_SIZE, |
| 185 | + ); |
| 186 | + dataBlock.set(encodedData); |
182 | 187 | } else {
|
183 |
| - dataBlock = new Uint8Array(0); // Ensures consistent type |
| 188 | + dataBlock = new Uint8Array(0); |
184 | 189 | }
|
185 | 190 |
|
186 |
| - return { header: headerBlock, data, encodedData: dataBlock, type }; |
| 191 | + return { |
| 192 | + header: headerBlock, |
| 193 | + data: data, |
| 194 | + encodedData: dataBlock, |
| 195 | + type: stat.type, |
| 196 | + }; |
187 | 197 | }),
|
188 | 198 | )
|
189 | 199 | .noShrink();
|
|
0 commit comments