libfreemkv 0.8.3

Open source raw disc access library for optical drives
Documentation
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
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
//! UDF ISO writer — creates Blu-ray disc images.
//!
//! Writes a minimal UDF 2.50 filesystem containing BDMV/STREAM/*.m2ts.
//! The ISO can be mounted or read back via IsoStream.
//!
//! Layout:
//!   Sector 0-15:    System area (zeros)
//!   Sector 16-18:   Volume Recognition Sequence (BEA01, NSR03, TEA01)
//!   Sector 32-37:   Volume Descriptor Sequence
//!   Sector 256:     Anchor Volume Descriptor Pointer
//!   Sector 260-271: Metadata partition (FSD, ICBs, directories)
//!   Sector 288+:    File data (m2ts content)
//!   Last-256:       Reserve AVDP

use std::io::{self, Seek, SeekFrom, Write};

const SECTOR_SIZE: u64 = 2048;

// Layout constants
const VRS_START: u32 = 16; // Volume Recognition Sequence
const VDS_START: u32 = 32; // Volume Descriptor Sequence
const AVDP_SECTOR: u32 = 256; // Anchor Volume Descriptor Pointer
const PARTITION_START: u32 = 257; // Physical partition start
const METADATA_START: u32 = 260; // Metadata partition content
const FSD_SECTOR: u32 = 260; // File Set Descriptor
const ROOT_ICB_SECTOR: u32 = 261; // Root directory ICB
const ROOT_DIR_SECTOR: u32 = 262; // Root directory data
const BDMV_ICB_SECTOR: u32 = 263; // BDMV/ ICB
const BDMV_DIR_SECTOR: u32 = 264; // BDMV/ directory data
const STREAM_ICB_SECTOR: u32 = 265; // BDMV/STREAM/ ICB
const STREAM_DIR_SECTOR: u32 = 266; // BDMV/STREAM/ directory data
const M2TS_ICB_SECTOR: u32 = 267; // m2ts file ICB
const DATA_START: u32 = 288; // Start of file data (aligned)

/// Write a complete BD ISO image.
///
/// Writes UDF structure, then streams m2ts content from the writer.
/// Call `start()` first, then write BD-TS bytes, then call `finish()`.
pub struct IsoWriter<W: Write + Seek> {
    writer: W,
    volume_id: String,
    m2ts_name: String,
    data_start_sector: u32,
    bytes_written: u64,
}

impl<W: Write + Seek> IsoWriter<W> {
    /// Create a new ISO writer. Call `start()` to write the UDF header.
    pub fn new(writer: W, volume_id: &str, m2ts_name: &str) -> Self {
        Self {
            writer,
            volume_id: volume_id.to_string(),
            m2ts_name: m2ts_name.to_string(),
            data_start_sector: DATA_START,
            bytes_written: 0,
        }
    }

    /// Update volume ID and m2ts filename. Must be called before `start()`.
    pub fn with_names(mut self, volume_id: &str, m2ts_name: &str) -> Self {
        self.volume_id = volume_id.to_string();
        self.m2ts_name = m2ts_name.to_string();
        self
    }

    /// Write UDF filesystem header. After this, write m2ts content bytes.
    pub fn start(&mut self) -> io::Result<()> {
        // System area: sectors 0-15 (zeros)
        let zero_sector = [0u8; SECTOR_SIZE as usize];
        for _ in 0..VRS_START {
            self.writer.write_all(&zero_sector)?;
        }

        // Volume Recognition Sequence
        self.write_vrs()?;

        // Pad sectors 19-31
        for _ in 19..VDS_START {
            self.writer.write_all(&zero_sector)?;
        }

        // Volume Descriptor Sequence (sectors 32-37)
        self.write_vds()?;

        // Pad sectors 38-255
        for _ in 38..AVDP_SECTOR {
            self.writer.write_all(&zero_sector)?;
        }

        // AVDP at sector 256
        self.write_avdp()?;

        // Partition area: metadata file ICB at partition_start
        self.write_metadata_file_icb()?;

        // Pad to metadata start
        for _ in (PARTITION_START + 1)..METADATA_START {
            self.writer.write_all(&zero_sector)?;
        }

        // Metadata partition
        self.write_fsd()?;
        self.write_root_icb()?;
        self.write_root_dir()?;
        self.write_bdmv_icb()?;
        self.write_bdmv_dir()?;
        self.write_stream_icb()?;
        self.write_stream_dir()?;
        self.write_m2ts_icb(0)?; // placeholder size, updated in finish()

        // Pad to data start
        for _ in (M2TS_ICB_SECTOR + 1)..self.data_start_sector {
            self.writer.write_all(&zero_sector)?;
        }

        Ok(())
    }

    /// Write m2ts content bytes. Call after `start()`.
    pub fn write_data(&mut self, buf: &[u8]) -> io::Result<usize> {
        let n = self.writer.write(buf)?;
        self.bytes_written += n as u64;
        Ok(n)
    }

    /// Finalize the ISO: pad to sector boundary, update file sizes, write reserve AVDP.
    pub fn finish(&mut self) -> io::Result<()> {
        // Pad to sector boundary
        let remainder = (self.bytes_written % SECTOR_SIZE) as usize;
        if remainder > 0 {
            let pad = SECTOR_SIZE as usize - remainder;
            let zeros = vec![0u8; pad];
            self.writer.write_all(&zeros)?;
            self.bytes_written += pad as u64;
        }

        let total_data_sectors = (self.bytes_written / SECTOR_SIZE) as u32;
        let total_sectors = self.data_start_sector + total_data_sectors;

        // Seek back and update m2ts file ICB with actual size
        self.writer
            .seek(SeekFrom::Start(M2TS_ICB_SECTOR as u64 * SECTOR_SIZE))?;
        self.write_m2ts_icb(self.bytes_written)?;

        // Seek to end and write reserve AVDP
        let reserve_sector = if total_sectors > 512 {
            total_sectors - 256
        } else {
            total_sectors.saturating_sub(1).max(AVDP_SECTOR + 1)
        };
        self.writer
            .seek(SeekFrom::Start(reserve_sector as u64 * SECTOR_SIZE))?;
        self.write_avdp()?;

        self.writer.flush()?;
        Ok(())
    }

    // ── UDF structure writers ──────────────────────────────────────────────

    fn write_vrs(&mut self) -> io::Result<()> {
        // BEA01 at sector 16
        let mut bea = [0u8; SECTOR_SIZE as usize];
        bea[0] = 0; // structure type
        bea[1..6].copy_from_slice(b"BEA01");
        bea[6] = 1; // structure version
        self.writer.write_all(&bea)?;

        // NSR03 at sector 17 (UDF 2.50)
        let mut nsr = [0u8; SECTOR_SIZE as usize];
        nsr[0] = 0;
        nsr[1..6].copy_from_slice(b"NSR03");
        nsr[6] = 1;
        self.writer.write_all(&nsr)?;

        // TEA01 at sector 18
        let mut tea = [0u8; SECTOR_SIZE as usize];
        tea[0] = 0;
        tea[1..6].copy_from_slice(b"TEA01");
        tea[6] = 1;
        self.writer.write_all(&tea)?;

        Ok(())
    }

    fn write_vds(&mut self) -> io::Result<()> {
        // Primary Volume Descriptor (tag 1) at sector 32
        let mut pvd = [0u8; SECTOR_SIZE as usize];
        write_descriptor_tag(&mut pvd, 1, VDS_START);
        // Volume Identifier at offset 24 (32-byte d-string)
        write_dstring(&mut pvd[24..56], &self.volume_id);
        self.writer.write_all(&pvd)?;

        // Partition Descriptor (tag 5) at sector 33
        let mut pd = [0u8; SECTOR_SIZE as usize];
        write_descriptor_tag(&mut pd, 5, VDS_START + 1);
        // Partition starting location at offset 188
        pd[188..192].copy_from_slice(&PARTITION_START.to_le_bytes());
        // Partition length (large enough for everything)
        let part_len: u32 = 0xFFFF_FFFF;
        pd[192..196].copy_from_slice(&part_len.to_le_bytes());
        self.writer.write_all(&pd)?;

        // Logical Volume Descriptor (tag 6) at sector 34
        let mut lvd = [0u8; SECTOR_SIZE as usize];
        write_descriptor_tag(&mut lvd, 6, VDS_START + 2);
        // Logical block size at offset 212
        lvd[212..216].copy_from_slice(&2048u32.to_le_bytes());
        // Number of partition maps at offset 268
        lvd[268..272].copy_from_slice(&2u32.to_le_bytes());
        // Partition map 1: Type 1 (physical), 6 bytes
        lvd[440] = 1; // type
        lvd[441] = 6; // length
                      // Partition map 2: Type 2 (metadata), 64 bytes
        lvd[446] = 2; // type
        lvd[447] = 64; // length
                       // Entity ID for metadata partition
        lvd[450..473].copy_from_slice(b"*UDF Metadata Partition");
        self.writer.write_all(&lvd)?;

        // Unallocated Space Descriptor (tag 7) at sector 35
        let mut usd = [0u8; SECTOR_SIZE as usize];
        write_descriptor_tag(&mut usd, 7, VDS_START + 3);
        self.writer.write_all(&usd)?;

        // Implementation Use Volume Descriptor (tag 4) at sector 36
        let mut iuvd = [0u8; SECTOR_SIZE as usize];
        write_descriptor_tag(&mut iuvd, 4, VDS_START + 4);
        self.writer.write_all(&iuvd)?;

        // Terminating Descriptor (tag 8) at sector 37
        let mut td = [0u8; SECTOR_SIZE as usize];
        write_descriptor_tag(&mut td, 8, VDS_START + 5);
        self.writer.write_all(&td)?;

        Ok(())
    }

    fn write_avdp(&mut self) -> io::Result<()> {
        let mut avdp = [0u8; SECTOR_SIZE as usize];
        write_descriptor_tag(&mut avdp, 2, AVDP_SECTOR);
        // Main VDS extent_ad: {length, location} per UDF spec
        avdp[16..20].copy_from_slice(&(6u32 * 2048).to_le_bytes()); // length
        avdp[20..24].copy_from_slice(&VDS_START.to_le_bytes()); // location
                                                                // Reserve VDS extent_ad (same as main for simplicity)
        avdp[24..28].copy_from_slice(&(6u32 * 2048).to_le_bytes()); // length
        avdp[28..32].copy_from_slice(&VDS_START.to_le_bytes()); // location
        self.writer.write_all(&avdp)?;
        Ok(())
    }

    fn write_metadata_file_icb(&mut self) -> io::Result<()> {
        // Extended File Entry (tag 266) at partition_start
        // Points to metadata content at METADATA_START
        let mut icb = [0u8; SECTOR_SIZE as usize];
        write_descriptor_tag(&mut icb, 266, PARTITION_START);
        // ICB tag at offset 16
        icb[16..20].copy_from_slice(&0u32.to_le_bytes()); // prior recorded
        icb[20..22].copy_from_slice(&0u16.to_le_bytes()); // strategy type
        icb[22..24].copy_from_slice(&0u16.to_le_bytes()); // strategy parameter
                                                          // File type at offset 27: 250 = metadata file
        icb[27] = 250;
        // Information length at offset 56
        let meta_len: u64 = 12 * SECTOR_SIZE; // 12 sectors of metadata
        icb[56..64].copy_from_slice(&meta_len.to_le_bytes());
        // Extended attribute length at offset 208
        icb[208..212].copy_from_slice(&0u32.to_le_bytes());
        // Allocation descriptor at offset 216: short_ad (length + position)
        let ad_len = meta_len as u32;
        let ad_pos = METADATA_START - PARTITION_START; // relative to partition
        icb[216..220].copy_from_slice(&ad_len.to_le_bytes());
        icb[220..224].copy_from_slice(&ad_pos.to_le_bytes());
        self.writer.write_all(&icb)?;
        Ok(())
    }

    fn write_fsd(&mut self) -> io::Result<()> {
        let mut fsd = [0u8; SECTOR_SIZE as usize];
        write_descriptor_tag(&mut fsd, 256, FSD_SECTOR);
        // Root Directory ICB (long_ad at offset 400)
        let root_lba = ROOT_ICB_SECTOR - METADATA_START; // metadata-relative
        fsd[400..404].copy_from_slice(&SECTOR_SIZE.to_le_bytes()[..4]); // extent length
        fsd[404..408].copy_from_slice(&root_lba.to_le_bytes());
        self.writer.write_all(&fsd)?;
        Ok(())
    }

    fn write_root_icb(&mut self) -> io::Result<()> {
        let mut icb = [0u8; SECTOR_SIZE as usize];
        write_descriptor_tag(&mut icb, 266, ROOT_ICB_SECTOR);
        icb[27] = 4; // file type: directory
        let dir_len: u64 = SECTOR_SIZE;
        icb[56..64].copy_from_slice(&dir_len.to_le_bytes());
        icb[208..212].copy_from_slice(&0u32.to_le_bytes());
        let ad_pos = ROOT_DIR_SECTOR - METADATA_START;
        icb[216..220].copy_from_slice(&(SECTOR_SIZE as u32).to_le_bytes());
        icb[220..224].copy_from_slice(&ad_pos.to_le_bytes());
        self.writer.write_all(&icb)?;
        Ok(())
    }

    fn write_root_dir(&mut self) -> io::Result<()> {
        let mut dir = [0u8; SECTOR_SIZE as usize];
        let mut offset = 0;
        // Parent entry (.. points to self)
        offset += write_fid(
            &mut dir[offset..],
            ROOT_ICB_SECTOR - METADATA_START,
            "",
            true,
        );
        // BDMV directory entry
        offset += write_fid(
            &mut dir[offset..],
            BDMV_ICB_SECTOR - METADATA_START,
            "BDMV",
            false,
        );
        let _ = offset;
        self.writer.write_all(&dir)?;
        Ok(())
    }

    fn write_bdmv_icb(&mut self) -> io::Result<()> {
        let mut icb = [0u8; SECTOR_SIZE as usize];
        write_descriptor_tag(&mut icb, 266, BDMV_ICB_SECTOR);
        icb[27] = 4; // directory
        let dir_len: u64 = SECTOR_SIZE;
        icb[56..64].copy_from_slice(&dir_len.to_le_bytes());
        icb[208..212].copy_from_slice(&0u32.to_le_bytes());
        let ad_pos = BDMV_DIR_SECTOR - METADATA_START;
        icb[216..220].copy_from_slice(&(SECTOR_SIZE as u32).to_le_bytes());
        icb[220..224].copy_from_slice(&ad_pos.to_le_bytes());
        self.writer.write_all(&icb)?;
        Ok(())
    }

    fn write_bdmv_dir(&mut self) -> io::Result<()> {
        let mut dir = [0u8; SECTOR_SIZE as usize];
        let mut offset = 0;
        offset += write_fid(
            &mut dir[offset..],
            ROOT_ICB_SECTOR - METADATA_START,
            "",
            true,
        );
        offset += write_fid(
            &mut dir[offset..],
            STREAM_ICB_SECTOR - METADATA_START,
            "STREAM",
            false,
        );
        let _ = offset;
        self.writer.write_all(&dir)?;
        Ok(())
    }

    fn write_stream_icb(&mut self) -> io::Result<()> {
        let mut icb = [0u8; SECTOR_SIZE as usize];
        write_descriptor_tag(&mut icb, 266, STREAM_ICB_SECTOR);
        icb[27] = 4; // directory
        let dir_len: u64 = SECTOR_SIZE;
        icb[56..64].copy_from_slice(&dir_len.to_le_bytes());
        icb[208..212].copy_from_slice(&0u32.to_le_bytes());
        let ad_pos = STREAM_DIR_SECTOR - METADATA_START;
        icb[216..220].copy_from_slice(&(SECTOR_SIZE as u32).to_le_bytes());
        icb[220..224].copy_from_slice(&ad_pos.to_le_bytes());
        self.writer.write_all(&icb)?;
        Ok(())
    }

    fn write_stream_dir(&mut self) -> io::Result<()> {
        let mut dir = [0u8; SECTOR_SIZE as usize];
        let mut offset = 0;
        offset += write_fid(
            &mut dir[offset..],
            BDMV_ICB_SECTOR - METADATA_START,
            "",
            true,
        );
        offset += write_fid(
            &mut dir[offset..],
            M2TS_ICB_SECTOR - METADATA_START,
            &self.m2ts_name,
            false,
        );
        let _ = offset;
        self.writer.write_all(&dir)?;
        Ok(())
    }

    fn write_m2ts_icb(&mut self, file_size: u64) -> io::Result<()> {
        let mut icb = [0u8; SECTOR_SIZE as usize];
        write_descriptor_tag(&mut icb, 266, M2TS_ICB_SECTOR);
        icb[27] = 5; // file type: regular file
        icb[56..64].copy_from_slice(&file_size.to_le_bytes());
        icb[208..212].copy_from_slice(&0u32.to_le_bytes());
        // Allocation: data starts at DATA_START in the physical partition.
        // UDF short_ad is 30 bits for extent length (max 1 GB = 0x3FFFFFFF).
        // For files > 1 GB, write multiple short_ad entries of 1 GB each plus remainder.
        let data_offset = self.data_start_sector - PARTITION_START;
        const MAX_EXTENT: u64 = 0x3FFF_FFFF; // 1 GB - 1 (30-bit max)
        let mut remaining = file_size;
        let mut ad_offset: usize = 216;
        let mut sector_pos = data_offset;
        while remaining > 0 && ad_offset + 8 <= SECTOR_SIZE as usize {
            let extent_len = if remaining > MAX_EXTENT {
                MAX_EXTENT
            } else {
                remaining
            };
            icb[ad_offset..ad_offset + 4].copy_from_slice(&(extent_len as u32).to_le_bytes());
            icb[ad_offset + 4..ad_offset + 8].copy_from_slice(&sector_pos.to_le_bytes());
            ad_offset += 8; // each short_ad is 8 bytes
            let extent_sectors = ((extent_len + SECTOR_SIZE - 1) / SECTOR_SIZE) as u32;
            sector_pos += extent_sectors;
            remaining -= extent_len;
        }
        self.writer.write_all(&icb)?;
        Ok(())
    }
}

// ── UDF primitives ─────────────────────────────────────────────────────────

/// Write a UDF Descriptor Tag at the start of a sector.
fn write_descriptor_tag(buf: &mut [u8], tag_id: u16, sector: u32) {
    buf[0..2].copy_from_slice(&tag_id.to_le_bytes());
    // Descriptor version: 3 (UDF 2.50)
    buf[2..4].copy_from_slice(&3u16.to_le_bytes());
    // Tag location
    buf[12..16].copy_from_slice(&sector.to_le_bytes());
    // Compute CRC-CCITT over descriptor body (bytes 16+)
    let body = &buf[16..];
    let body_len = body.len();
    let crc = udf_crc(body);
    buf[8..10].copy_from_slice(&crc.to_le_bytes());
    // Descriptor CRC length
    buf[10..12].copy_from_slice(&(body_len as u16).to_le_bytes());
    // Compute tag checksum: sum of bytes 0-3, 5-15 mod 256
    buf[4] = 0; // clear before computing
    let checksum: u8 = buf[0..4]
        .iter()
        .chain(buf[5..16].iter())
        .fold(0u8, |acc, &b| acc.wrapping_add(b));
    buf[4] = checksum;
}

/// UDF CRC-CCITT (CRC-16/ECMA-182 polynomial 0x11021).
fn udf_crc(data: &[u8]) -> u16 {
    // CRC lookup table for polynomial 0x11021
    static CRC_TABLE: [u16; 256] = {
        let mut table = [0u16; 256];
        let mut i = 0;
        while i < 256 {
            let mut crc = (i as u16) << 8;
            let mut j = 0;
            while j < 8 {
                if crc & 0x8000 != 0 {
                    crc = (crc << 1) ^ 0x1021;
                } else {
                    crc <<= 1;
                }
                j += 1;
            }
            table[i] = crc;
            i += 1;
        }
        table
    };
    let mut crc: u16 = 0;
    for &byte in data {
        crc = (crc << 8) ^ CRC_TABLE[((crc >> 8) as u8 ^ byte) as usize];
    }
    crc
}

/// Write a UDF d-string (compressed unicode string with length prefix).
fn write_dstring(buf: &mut [u8], s: &str) {
    let max = buf.len() - 1; // last byte is length
    let bytes = s.as_bytes();
    let len = bytes.len().min(max);
    if len > 0 {
        buf[0] = 8; // compression ID: 8 = Latin-1
        buf[1..1 + len].copy_from_slice(&bytes[..len]);
        buf[buf.len() - 1] = (len + 1) as u8; // d-string length including comp ID
    }
}

/// Write a File Identifier Descriptor. Returns bytes written (4-byte aligned).
fn write_fid(buf: &mut [u8], icb_lba: u32, name: &str, is_parent: bool) -> usize {
    // Tag 257 = File Identifier Descriptor
    let name_bytes = name.as_bytes();
    let name_len = if is_parent { 0 } else { name_bytes.len() + 1 }; // +1 for comp ID
    let fid_len = 38 + name_len; // fixed header + identifier
    let padded = (fid_len + 3) & !3; // 4-byte align

    if padded > buf.len() {
        return 0;
    }

    // Tag
    buf[0..2].copy_from_slice(&257u16.to_le_bytes());
    // File version number at offset 16
    buf[16..18].copy_from_slice(&1u16.to_le_bytes());
    // File characteristics at offset 18
    buf[18] = if is_parent { 0x0A } else { 0x02 }; // parent | directory
    if !is_parent && !name.contains('.') {
        buf[18] = 0x02; // directory
    } else if !is_parent {
        buf[18] = 0x00; // file
    }
    // ICB (long_ad at offset 20): extent length + location
    buf[20..24].copy_from_slice(&(SECTOR_SIZE as u32).to_le_bytes());
    buf[24..28].copy_from_slice(&icb_lba.to_le_bytes());
    // Identifier length at offset 36
    buf[36] = name_len as u8;
    // Implementation use length at offset 37
    buf[37] = 0;
    // File identifier at offset 38
    if !is_parent && !name_bytes.is_empty() {
        buf[38] = 8; // compression ID: Latin-1
        buf[39..39 + name_bytes.len()].copy_from_slice(name_bytes);
    }

    padded
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::io::Cursor;

    /// Read a little-endian u16 from a byte slice at the given offset.
    fn le_u16(data: &[u8], off: usize) -> u16 {
        u16::from_le_bytes([data[off], data[off + 1]])
    }

    /// Read a little-endian u32 from a byte slice at the given offset.
    #[allow(dead_code)]
    fn le_u32(data: &[u8], off: usize) -> u32 {
        u32::from_le_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]])
    }

    /// Read a little-endian u64 from a byte slice at the given offset.
    fn le_u64(data: &[u8], off: usize) -> u64 {
        u64::from_le_bytes([
            data[off],
            data[off + 1],
            data[off + 2],
            data[off + 3],
            data[off + 4],
            data[off + 5],
            data[off + 6],
            data[off + 7],
        ])
    }

    /// Get the sector at a given sector number from the output data.
    fn sector(data: &[u8], num: u32) -> &[u8] {
        let start = num as usize * SECTOR_SIZE as usize;
        &data[start..start + SECTOR_SIZE as usize]
    }

    #[test]
    fn isowriter_creates_valid_udf() {
        let buf = Cursor::new(Vec::new());
        let mut w = IsoWriter::new(buf, "TEST_VOL", "00001.m2ts");
        w.start().unwrap();
        w.write_data(&[0xAA; 4096]).unwrap();
        w.finish().unwrap();
        let data = w.writer.into_inner();

        // AVDP at sector 256 should have tag ID = 2
        let avdp = sector(&data, AVDP_SECTOR);
        assert_eq!(le_u16(avdp, 0), 2, "AVDP tag ID should be 2");

        // VRS at sector 16 should contain "BEA01"
        let vrs = sector(&data, VRS_START);
        assert_eq!(&vrs[1..6], b"BEA01", "VRS sector 16 should contain BEA01");

        // FSD at metadata sector should have tag ID = 256
        let fsd = sector(&data, FSD_SECTOR);
        assert_eq!(le_u16(fsd, 0), 256, "FSD tag ID should be 256");
    }

    #[test]
    fn isowriter_updates_file_size() {
        let buf = Cursor::new(Vec::new());
        let mut w = IsoWriter::new(buf, "SIZE_TEST", "00001.m2ts");
        w.start().unwrap();

        let test_data = vec![0x42u8; 8192]; // exactly 4 sectors
        let written = w.write_data(&test_data).unwrap();
        assert_eq!(written, 8192);

        w.finish().unwrap();
        let data = w.writer.into_inner();

        // Read m2ts ICB at M2TS_ICB_SECTOR and check information length at offset 56
        let icb = sector(&data, M2TS_ICB_SECTOR);
        let file_size = le_u64(icb, 56);
        assert_eq!(
            file_size, 8192,
            "m2ts ICB file size should match bytes written (8192), got {}",
            file_size
        );
    }

    #[test]
    fn isowriter_with_names() {
        let buf = Cursor::new(Vec::new());
        let mut w = IsoWriter::new(buf, "MY_DISC", "00042.m2ts");
        w.start().unwrap();
        w.write_data(&[0x00; 2048]).unwrap();
        w.finish().unwrap();
        let data = w.writer.into_inner();

        // Check PVD (sector 32) volume_id at offset 24 as d-string
        let pvd = sector(&data, VDS_START);
        // d-string: byte 0 = compression ID (8), then ASCII chars
        assert_eq!(pvd[24], 8, "PVD volume_id compression ID should be 8");
        assert_eq!(
            &pvd[25..32],
            b"MY_DISC",
            "PVD should contain volume_id 'MY_DISC'"
        );

        // Check STREAM directory (sector 266) for m2ts filename in FID
        let stream_dir = sector(&data, STREAM_DIR_SECTOR);
        // The FID for the m2ts file should contain the filename after the parent entry.
        // Search for "00042.m2ts" in the sector data
        let name = b"00042.m2ts";
        let found = stream_dir.windows(name.len()).any(|w| w == name);
        assert!(
            found,
            "STREAM directory should contain m2ts filename '00042.m2ts'"
        );
    }

    #[test]
    fn isowriter_empty_content() {
        let buf = Cursor::new(Vec::new());
        let mut w = IsoWriter::new(buf, "EMPTY", "00001.m2ts");
        w.start().unwrap();
        // No data written
        w.finish().unwrap();
        let data = w.writer.into_inner();

        // Should still have valid UDF structure
        // AVDP at sector 256
        let avdp = sector(&data, AVDP_SECTOR);
        assert_eq!(
            le_u16(avdp, 0),
            2,
            "AVDP tag should be present even with no data"
        );

        // VRS
        let vrs = sector(&data, VRS_START);
        assert_eq!(&vrs[1..6], b"BEA01");

        // FSD
        let fsd = sector(&data, FSD_SECTOR);
        assert_eq!(le_u16(fsd, 0), 256);

        // m2ts ICB should show 0 file size
        let icb = sector(&data, M2TS_ICB_SECTOR);
        let file_size = le_u64(icb, 56);
        assert_eq!(file_size, 0, "empty content should have 0 file size");

        // Output should be at least DATA_START sectors (the header structure)
        assert!(
            data.len() >= DATA_START as usize * SECTOR_SIZE as usize,
            "output too small for valid UDF structure"
        );
    }
}