tar.ts
⎇
Raw
1function pad(value: string, length: number, padWith = "\0"): string {
2 if (value.length > length) return value.slice(0, length);
3 return value + padWith.repeat(length - value.length);
4}
5
6function truncateFilename(filename: string, maxBytes = 100): string {
7 const encoder = new TextEncoder();
8
9 // Identify extension (last '.' not at position 0)
10 const dotIdx = filename.lastIndexOf(".");
11 let base: string;
12 let ext: string;
13 if (dotIdx > 0 && dotIdx < filename.length - 1) {
14 base = filename.slice(0, dotIdx);
15 ext = filename.slice(dotIdx); // includes dot
16 } else {
17 base = filename;
18 ext = "";
19 }
20
21 // Whole filename fits? Done.
22 if (encoder.encode(filename).length <= maxBytes) {
23 return filename;
24 }
25
26 const ellipsis = "...";
27 const tail = ellipsis + ext;
28 const tailBytes = encoder.encode(tail).length;
29
30 // Figure out how many bytes are left for the base
31 const baseMaxBytes = maxBytes - tailBytes;
32 if (baseMaxBytes <= 0) {
33 // Only enough space for truncated tail
34 let res = "";
35 let count = 0;
36 for (const ch of tail) {
37 const chBytes = encoder.encode(ch).length;
38 if (count + chBytes > maxBytes) break;
39 res += ch;
40 count += chBytes;
41 }
42 return res;
43 }
44
45 // Take base from left, as much as fits in baseMaxBytes
46 let outBase = "";
47 let totalBytes = 0;
48 for (const ch of Array.from(base)) {
49 const chBytes = encoder.encode(ch).length;
50 if (totalBytes + chBytes > baseMaxBytes) {
51 break;
52 }
53 outBase += ch;
54 totalBytes += chBytes;
55 }
56
57 return outBase + ellipsis + ext;
58}
59
60function octalString(n: number, length: number): string {
61 // Reserve 1 for NUL (tar standard)
62 const fieldLen = length - 1;
63 let s = n.toString(8);
64 if (s.length > fieldLen) s = "0".repeat(fieldLen);
65 s = s.padStart(fieldLen, "0");
66 return `${s}\0`;
67}
68
69export function createTar(files: Map<string, Uint8Array>): Uint8Array {
70 const blocks: Uint8Array[] = [];
71
72 files.forEach((content, filename) => {
73 const truncatedName = truncateFilename(filename);
74
75 // --- HEADER ---
76 const header = new Uint8Array(512);
77 let idx = 0;
78 // name (100)
79 const nameBytes = new TextEncoder().encode(pad(truncatedName, 100));
80 header.set(nameBytes, idx);
81 idx += 100;
82 // mode (8)
83 header.set(new TextEncoder().encode(octalString(0o644, 8)), idx);
84 idx += 8;
85 // uid (8)
86 header.set(new TextEncoder().encode(octalString(0, 8)), idx);
87 idx += 8;
88 // gid (8)
89 header.set(new TextEncoder().encode(octalString(0, 8)), idx);
90 idx += 8;
91 // size (12)
92 header.set(new TextEncoder().encode(octalString(content.length, 12)), idx);
93 idx += 12;
94 // mtime (12)
95 header.set(new TextEncoder().encode(octalString(Math.floor(Date.now() / 1000), 12)), idx);
96 idx += 12;
97 // chksum (8) - space-filled for now
98 header.set(new TextEncoder().encode(" "), idx);
99 idx += 8;
100 // typeflag (1) - '0' for regular file
101 header[idx++] = "0".charCodeAt(0);
102 // linkname (100)
103 idx += 100;
104 // magic (6)
105 header.set(new TextEncoder().encode("ustar\0"), idx);
106 idx += 6;
107 // version (2)
108 header.set(new TextEncoder().encode("00"), idx);
109 idx += 2;
110 // uname (32)
111 idx += 32;
112 // gname (32)
113 idx += 32;
114 // devmajor (8)
115 idx += 8;
116 // devminor (8)
117 idx += 8;
118 // prefix (155)
119 idx += 155;
120 // padding (12)
121 idx += 12;
122
123 // Compute checksum: set chksum field to spaces, then sum all bytes of header
124 let chksum = 0;
125 for (let i = 0; i < 512; i++) {
126 // chksum is at 148..155, treated as spaces (0x20)
127 if (i >= 148 && i < 156) {
128 chksum += 0x20;
129 } else {
130 chksum += header[i];
131 }
132 }
133 const chksumStr = pad(octalString(chksum, 7), 8, " "); // 6 digits, null, space
134 header.set(new TextEncoder().encode(chksumStr), 148);
135
136 blocks.push(header);
137
138 // --- CONTENT ---
139 blocks.push(content);
140
141 // pad to 512 bytes
142 if (content.length % 512 !== 0) {
143 blocks.push(new Uint8Array(512 - (content.length % 512)));
144 }
145 });
146
147 // Tar ends with two 512-byte zero blocks
148 blocks.push(new Uint8Array(512));
149 blocks.push(new Uint8Array(512));
150
151 // Flatten
152 const total = blocks.reduce((a, b) => a + b.length, 0);
153 const out = new Uint8Array(total);
154 let offset = 0;
155 for (const b of blocks) {
156 out.set(b, offset);
157 offset += b.length;
158 }
159 return out;
160}
161