File size: 11,426 Bytes
8df6da4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
// Source: https://wiki.osdev.org/ISO_9660

// Limitations:
// - can only generate iso files
// - only supports a single directory, no file system hierarchy
// - root directory entry is limited to 2 KiB (~42 files)
// - filenames are normalised to 8.3 length and [A-Z0-9_.]

import { dbg_assert } from "./log.js";

const BLOCK_SIZE = 2 * 1024; // 0x800

const FILE_FLAGS_HIDDEN = 1 << 0;
const FILE_FLAGS_DIRECTORY = 1 << 1;
const FILE_FLAGS_ASSOCIATED_FILE = 1 << 2;
const FILE_FLAGS_HAS_EXTENDED_ATTRIBUTE_RECORD = 1 << 3;
const FILE_FLAGS_HAS_PERMISSIONS = 1 << 4;
const FILE_FLAGS_NOT_FINAL = 1 << 7;

/**
 * @param {Array.<{ name: string, contents: Uint8Array}>} files
 */
export function generate(files)
{
    const te = new TextEncoder();
    const date = new Date;

    const write8 = (b, v) => { b.buffer[b.offset++] = v; };
    const write_le16 = (b, v) => { b.buffer[b.offset++] = v; b.buffer[b.offset++] = v >> 8; };
    const write_le32 = (b, v) => { b.buffer[b.offset++] = v; b.buffer[b.offset++] = v >> 8; b.buffer[b.offset++] = v >> 16; b.buffer[b.offset++] = v >> 24; };
    const write_be16 = (b, v) => { b.buffer[b.offset++] = v >> 8; b.buffer[b.offset++] = v; };
    const write_be32 = (b, v) => { b.buffer[b.offset++] = v >> 24; b.buffer[b.offset++] = v >> 16; b.buffer[b.offset++] = v >> 8; b.buffer[b.offset++] = v; };
    const write_lebe16 = (b, v) => { write_le16(b, v); write_be16(b, v); };
    const write_lebe32 = (b, v) => { write_le32(b, v); write_be32(b, v); };
    const fill = (b, len, v) => { b.buffer.fill(v, b.offset, b.offset += len); };
    const write_ascii = (b, v) => { b.offset += te.encodeInto(v, b.buffer.subarray(b.offset)).written; };
    const write_padded_ascii = (b, len, v) => { b.offset += te.encodeInto(v.padEnd(len), b.buffer.subarray(b.offset)).written; };
    const write_dummy_date_ascii = b => { fill(b, 16, 0x20); write8(b, 0); };
    const write_date_compact = b => {
        write8(b, date.getUTCFullYear() - 1900);
        write8(b, 1 + date.getUTCMonth());
        write8(b, date.getUTCDate());
        write8(b, date.getUTCHours());
        write8(b, date.getUTCMinutes());
        write8(b, date.getUTCSeconds());
        write8(b, 0);
    };
    const skip = (b, len) => { b.offset += len; };

    const write_record = (b, name, flags, is_special, lba, len) => {
        if(!is_special) name = sanitise_filename(name) + ";1";
        // write name first and get its length
        const START = buffer.offset;
        const NAME_OFFSET = 33;
        const name_len = te.encodeInto(name, b.buffer.subarray(b.offset + NAME_OFFSET)).written;
        const pad = (name_len & 1) ? 0 : 1;
        const len_field = 33 + name_len + pad;
        dbg_assert(len_field < 256);
        write8(buffer, len_field);      // Length of directory record
        write8(buffer, 0);              // Extended Attribute Record length
        write_lebe32(buffer, lba);      // Location of extent (LBA)
        write_lebe32(buffer, len);      // Data length (size of extent)
        write_date_compact(buffer);
        write8(buffer, flags);
        write8(buffer, 0);              // File unit size for files recorded in interleaved mode, zero otherwise
        write8(buffer, 0);              // Interleave gap size for files recorded in interleaved mode, zero otherwise
        write_lebe16(buffer, 1);        // Volume sequence number - the volume that this extent is recorded on
        write8(buffer, name_len);       // length of file name
        dbg_assert(buffer.offset === START + NAME_OFFSET);
        skip(buffer, name_len + pad);   // File name: was already written
        dbg_assert(buffer.offset === START + len_field);
    };
    const write_special_directory_record = (b, name, lba, len) => write_record(b, name, FILE_FLAGS_DIRECTORY, true, lba, len);
    const write_file_record = (b, name, lba, len) => write_record(b, name, 0, false, lba, len);

    function round_byte_size_to_block_size(n)
    {
        return 1 + Math.floor((n - 1) / BLOCK_SIZE);
    }
    dbg_assert(round_byte_size_to_block_size(0) === 0);
    dbg_assert(round_byte_size_to_block_size(1) === 1);
    dbg_assert(round_byte_size_to_block_size(BLOCK_SIZE - 1) === 1);
    dbg_assert(round_byte_size_to_block_size(BLOCK_SIZE) === 1);
    dbg_assert(round_byte_size_to_block_size(BLOCK_SIZE + 1) === 2);
    dbg_assert(round_byte_size_to_block_size(2 * BLOCK_SIZE) === 2);
    dbg_assert(round_byte_size_to_block_size(2 * BLOCK_SIZE + 1) === 3);
    dbg_assert(round_byte_size_to_block_size(10 * BLOCK_SIZE + 1) === 11);

    function to_msdos_filename(name)
    {
        const dot = name.lastIndexOf(".");
        if(dot === -1) return name.substr(0, 8);
        return name.substr(0, Math.min(8, dot)) + "." + name.substr(dot + 1, 3);
    }

    dbg_assert(to_msdos_filename("abcdefghijkl.qwerty") === "abcdefgh.qwe");
    dbg_assert(to_msdos_filename("abcdefghijkl") === "abcdefgh");

    function sanitise_filename(name)
    {
        return to_msdos_filename(name.toUpperCase().replace(/[^A-Z0-9_.]/g, ""));
    }

    // layout:
    // (lba = one block of BLOCK_SIZE bytes)
    // LBA   | contents
    // ------+--------
    // 0..15 | System Area (could be used for mbr, but not used by us)
    //    16 | Primary Volume Descriptor
    //    17 | Volume Descriptor Set Terminator
    //    18 | empty
    //    19 | Little Endian Path Table
    //    20 | empty
    //    21 | Big Endian Path Table
    //    22 | empty
    //    23 | Root directory
    // 24..n | File contents
    const SYSTEM_AREA_SIZE = 16 * BLOCK_SIZE;
    const PRIMARY_VOLUME_LBA = 16;
    const VOLUME_SET_TERMINATOR_LBA = 17;
    const LE_PATH_TABLE_LBA = 19;
    const BE_PATH_TABLE_LBA = 21;
    const ROOT_DIRECTORY_LBA = 23;
    const LE_PATH_TABLE_SIZE = BLOCK_SIZE;
    const BE_PATH_TABLE_SIZE = BLOCK_SIZE;
    const ROOT_DIRECTORY_SIZE = BLOCK_SIZE;

    let next_file_lba = 24;
    files = files.map(({ name, contents }) => {
        const lba = next_file_lba;
        next_file_lba += round_byte_size_to_block_size(contents.length);
        name = to_msdos_filename(name);
        return { name, contents, lba };
    });

    const N_LBAS = next_file_lba;
    const total_size = N_LBAS * BLOCK_SIZE;

    const buffer = {
        buffer: new Uint8Array(total_size),
        offset: SYSTEM_AREA_SIZE,
    };

    // LBA 16: Primary Volume Descriptor
    dbg_assert(buffer.offset === PRIMARY_VOLUME_LBA * BLOCK_SIZE);
    write8(buffer, 0x01);                    // Volume Descriptor type: Primary Volume Descriptor
    write_ascii(buffer, "CD001");            // Always CD001
    write8(buffer, 0x01);                    // Version
    write8(buffer, 0x00);                    // unused
    write_padded_ascii(buffer, 32, "V86");   // System Identifier
    write_padded_ascii(buffer, 32, "CDROM"); // Identification of this volume
    skip(buffer, 8);                         // unused
    write_lebe32(buffer, N_LBAS);
    skip(buffer, 32);                        // unused
    dbg_assert(buffer.offset === 0x8000 + 120);

    write_lebe16(buffer, 1); // Volume Set Size
    write_lebe16(buffer, 1); // Volume Sequence Number
    dbg_assert(buffer.offset === 0x8080);

    write_lebe16(buffer, BLOCK_SIZE);

    write_lebe32(buffer, 10);               // Path Table Size
    write_le32(buffer, LE_PATH_TABLE_LBA);  // Location of Type-L Path Table
    write_le32(buffer, 0);                  // Location of the Optional Type-L Path Table
    write_be32(buffer, BE_PATH_TABLE_LBA);  // Location of Type-M Path Table
    write_be32(buffer, 0);                  // Location of the Optional Type-M Path Table
    dbg_assert(buffer.offset === 0x8000 + 156);

    // Directory entry for the root directory
    write_special_directory_record(buffer, "\x00", ROOT_DIRECTORY_LBA, 0x800);
    dbg_assert(buffer.offset === 0x8000 + 190);

    fill(buffer, 128, 0x20); // Volume Set Identifier
    fill(buffer, 128, 0x20); // Publisher Identifier
    fill(buffer, 128, 0x20); // Data Preparer Identifier
    fill(buffer, 128, 0x20); // Application Identifier
    fill(buffer,  37, 0x20); // Copyright File Identifier
    fill(buffer,  37, 0x20); // Abstract File Identifier
    fill(buffer,  37, 0x20); // Bibliographic File Identifier

    dbg_assert(buffer.offset === 0x8000 + 813);

    write_dummy_date_ascii(buffer); // Volume Creation Date and Time
    write_dummy_date_ascii(buffer); // Volume Modification Date and Time
    write_dummy_date_ascii(buffer); // Volume Expiration Date and Time
    write_dummy_date_ascii(buffer); // Volume Effective Date and Time

    write8(buffer, 0x01); // File Structure Version
    dbg_assert(buffer.offset === 0x8000 + 882);

    write8(buffer, 0x00); // Unused
    skip(buffer, 512);    // Application Used
    skip(buffer, 653);    // Reserved

    // LBA 17: Volume Descriptor Set Terminator
    dbg_assert(buffer.offset === VOLUME_SET_TERMINATOR_LBA * BLOCK_SIZE);
    write8(buffer, 0xFF);           // 0xFF: Volume Descriptor Set Terminator
    write_ascii(buffer, "CD001");   // Always CD001
    write8(buffer, 0x01);           // Version

    // LBA 19: Little Endian Path Table
    buffer.offset = LE_PATH_TABLE_LBA * BLOCK_SIZE;
    write8(buffer, 0x01);                   // Length of Directory Identifier
    write8(buffer, 0x00);                   // Extended Attribute Record Length
    write_le32(buffer, ROOT_DIRECTORY_LBA); // Location of Extent (LBA)
    write_le16(buffer, 1);                  // Directory number of parent directory
    write_ascii(buffer, "\x00");            // file name
    dbg_assert(buffer.offset < LE_PATH_TABLE_LBA * BLOCK_SIZE + LE_PATH_TABLE_SIZE);

    // LBA 21: Big Endian Path Table
    buffer.offset = BE_PATH_TABLE_LBA * BLOCK_SIZE;
    write8(buffer, 0x01);                   // Length of Directory Identifier
    write8(buffer, 0x00);                   // Extended Attribute Record Length
    write_be32(buffer, ROOT_DIRECTORY_LBA); // Location of Extent (LBA)
    write_be16(buffer, 1);                  // Directory number of parent directory
    write_ascii(buffer, "\x00");            // file name
    dbg_assert(buffer.offset < BE_PATH_TABLE_LBA * BLOCK_SIZE + BE_PATH_TABLE_SIZE);

    // LBA 23: root directory
    buffer.offset = ROOT_DIRECTORY_LBA * BLOCK_SIZE;
    write_special_directory_record(buffer, "\x00", ROOT_DIRECTORY_LBA, 0x800);  // "."
    write_special_directory_record(buffer, "\x01", ROOT_DIRECTORY_LBA, 0x800);  // ".."
    for(const { name, contents, lba } of files)
    {
        write_file_record(buffer, name, lba, contents.length);
    }
    // TODO: this assertion can fail if too many files are used as input
    // ROOT_DIRECTORY_SIZE should be choosen dynamically
    dbg_assert(buffer.offset < ROOT_DIRECTORY_LBA * BLOCK_SIZE + ROOT_DIRECTORY_SIZE);

    // file contents
    for(let { contents, lba } of files)
    {
        buffer.buffer.set(contents, lba * BLOCK_SIZE);
    }

    return buffer.buffer;
}

/**
 * @param {Uint8Array} buffer
 */
export function is_probably_iso9660_file(buffer)
{
    return (
        buffer.length >= 17 * BLOCK_SIZE &&
        buffer[BLOCK_SIZE + 0] ===  1 && // Primary Volume Descriptor
        buffer[BLOCK_SIZE + 1] === 67 && // "C"
        buffer[BLOCK_SIZE + 2] === 68 && // "D"
        buffer[BLOCK_SIZE + 3] === 48 && // "0"
        buffer[BLOCK_SIZE + 4] === 48 && // "0"
        buffer[BLOCK_SIZE + 5] === 49    // "1"
    );
}