libfreemkv 0.7.0

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
//! UDF filesystem reader — read files from Blu-ray discs.
//!
//! Blu-ray discs use UDF 2.50 with metadata partitions.
//! The read sequence follows pointers through the disc structure:
//!
//!   Sector 256 (AVDP)
//!     → VDS (Partition Descriptor + Logical Volume Descriptor)
//!       → Metadata Partition (virtual partition stored as a file)
//!         → File Set Descriptor (FSD)
//!           → Root Directory ICB
//!             → Directory data (File Identifier Descriptors)
//!               → BDMV/PLAYLIST/*.mpls, BDMV/CLIPINF/*.clpi
//!
//! Each step reads one or two sectors. No bulk reads needed.
//!
//! References:
//!   ECMA-167 (UDF base)
//!   UDF 2.50 (OSTA) — metadata partition extension
//!   BD-ROM Part 3 — Blu-ray filesystem profile

use crate::error::{Error, Result};
use crate::drive::DriveSession;

/// A UDF filesystem parsed from disc.
#[derive(Debug)]
pub struct UdfFs {
    /// Root directory with full tree
    pub root: DirEntry,
    /// UDF Volume Identifier from Primary Volume Descriptor
    pub volume_id: String,
    /// Physical partition start (absolute sector)
    partition_start: u32,
    /// Metadata partition start (absolute sector)
    /// For UDF 2.50 discs, all file/directory references use metadata-relative LBAs
    metadata_start: u32,
    /// Metadata partition size in sectors
    metadata_sectors: u32,
}

/// A directory or file entry.
#[derive(Debug, Clone)]
pub struct DirEntry {
    pub name: String,
    pub is_dir: bool,
    /// LBA within the metadata partition (add metadata_start for absolute)
    pub meta_lba: u32,
    /// File size in bytes (from ICB info_length)
    pub size: u64,
    /// Child entries (if directory)
    pub entries: Vec<DirEntry>,
}

impl UdfFs {
    /// Physical partition start sector.
    pub fn partition_start(&self) -> u32 { self.partition_start }

    /// Metadata partition start sector.
    pub fn metadata_start(&self) -> u32 { self.metadata_start }

    /// Find a directory by path (e.g. "/BDMV/PLAYLIST").
    /// Path matching is case-insensitive.
    pub fn find_dir(&self, path: &str) -> Option<&DirEntry> {
        let parts: Vec<&str> = path.trim_matches('/').split('/').collect();
        let mut current = &self.root;
        for part in &parts {
            current = current.entries.iter().find(|e| {
                e.is_dir && e.name.eq_ignore_ascii_case(part)
            })?;
        }
        Some(current)
    }

    /// Read a file by path, returning its raw bytes.
    /// Reads sector by sector from disc — no buffering.
    /// Get the absolute starting LBA of a file on disc.
    /// Used by the rip pipeline to locate m2ts content sectors.
    pub fn file_start_lba(&self, session: &mut DriveSession, path: &str) -> Result<u32> {
        let parts: Vec<&str> = path.trim_matches('/').split('/').collect();
        let mut current = &self.root;
        for part in &parts[..parts.len() - 1] {
            current = current.entries.iter().find(|e| {
                e.is_dir && e.name.eq_ignore_ascii_case(part)
            }).ok_or_else(|| Error::UdfNotFound { path: part.to_string() }
)?;
        }
        let filename = match parts.last() {
            Some(f) => f,
            None => return Err(Error::UdfNotFound { path: path.to_string() }),
        };
        let entry = current.entries.iter().find(|e| {
            !e.is_dir && e.name.eq_ignore_ascii_case(filename)
        }).ok_or_else(|| Error::UdfNotFound { path: path.to_string() }
)?;
        let (data_lba, _) = self.read_icb_extent(session, entry.meta_lba)?;
        Ok(self.partition_start + data_lba)
    }

    pub fn read_file(&self, session: &mut DriveSession, path: &str) -> Result<Vec<u8>> {
        let parts: Vec<&str> = path.trim_matches('/').split('/').collect();
        let mut current = &self.root;

        // Navigate to parent directory
        for part in &parts[..parts.len() - 1] {
            current = current.entries.iter().find(|e| {
                e.is_dir && e.name.eq_ignore_ascii_case(part)
            }).ok_or_else(|| Error::UdfNotFound { path: part.to_string() }
)?;
        }

        // Find the file
        let filename = match parts.last() {
            Some(f) => f,
            None => return Err(Error::UdfNotFound { path: path.to_string() }),
        };
        let entry = current.entries.iter().find(|e| {
            !e.is_dir && e.name.eq_ignore_ascii_case(filename)
        }).ok_or_else(|| Error::UdfNotFound { path: path.to_string() }
)?;

        // Read the file's ICB to get its data extent
        let (data_lba, data_len) = self.read_icb_extent(session, entry.meta_lba)?;

        // Read file data sector by sector
        // File DATA is in the physical partition (partition_start + lba),
        // NOT the metadata partition. ICBs are in metadata, data is in physical.
        let sector_count = ((data_len as u64 + 2047) / 2048) as u32;
        let mut data = vec![0u8; (sector_count as usize) * 2048];
        let abs_start = self.partition_start + data_lba;

        for i in 0..sector_count {
            let offset = (i as usize) * 2048;
            read_sector(session, abs_start + i, &mut data[offset..offset + 2048])?;
        }

        data.truncate(entry.size as usize);
        Ok(data)
    }

    /// Collect all sector ranges needed for disc-info and AACS.
    ///
    /// Returns a list of (start_lba, sector_count) ranges covering:
    ///   - UDF structure (AVDP, VDS, metadata partition, directories)
    ///   - BDMV/PLAYLIST/*.mpls, CLIPINF/*.clpi, JAR/*, META/*, *.bdmv
    ///   - AACS/* (Content*.cer, Unit_Key_RO.inf, CPSUnit*.cci)
    ///
    /// Skips: STREAM/ (video), BACKUP/, DUPLICATE/,
    ///   MKB_RO.inf, ContentHash*, ContentRevocation*
    pub fn metadata_sector_ranges(&self, session: &mut DriveSession) -> Result<Vec<(u32, u32)>> {
        let mut ranges = Vec::new();

        // UDF structure: sector 0 through end of metadata partition
        // Covers AVDP, VDS, partition descriptor, metadata ICB, FSD, all directories
        let meta_end = self.metadata_start + self.metadata_sectors;
        ranges.push((0, meta_end));

        // Walk tree, collect ranges for each metadata file
        self.collect_file_ranges(session, &self.root, &mut ranges)?;

        // Merge overlapping/adjacent ranges and sort
        ranges.sort_by_key(|r| r.0);
        let merged = merge_ranges(&ranges);
        Ok(merged)
    }

    fn collect_file_ranges(&self, session: &mut DriveSession, entry: &DirEntry, ranges: &mut Vec<(u32, u32)>) -> Result<()> {
        for child in &entry.entries {
            if child.is_dir {
                // Only skip STREAM — those are the multi-GB video files
                if child.name.eq_ignore_ascii_case("STREAM") {
                    continue;
                }
                self.collect_file_ranges(session, child, ranges)?;
            } else {
                // Include the ICB sector itself (in metadata partition)
                ranges.push((self.meta_to_abs(child.meta_lba), 1));

                // Include file data — skip only truly huge files (MKB_RO.inf = 134MB)
                if child.size > 50_000_000 {
                    continue;
                }

                if let Ok((data_lba, data_len)) = self.read_icb_extent(session, child.meta_lba) {
                    let abs_start = self.partition_start + data_lba;
                    let sector_count = (data_len + 2047) / 2048;
                    ranges.push((abs_start, sector_count));
                }
            }
        }
        Ok(())
    }

    /// Convert a metadata-partition-relative LBA to an absolute sector number.
    fn meta_to_abs(&self, meta_lba: u32) -> u32 {
        self.metadata_start + meta_lba
    }

    /// Read an Extended File Entry (tag 266) or File Entry (tag 261)
    /// and return its first allocation extent: (data_lba, data_length).
    /// The data_lba is partition-relative.
    fn read_icb_extent(&self, session: &mut DriveSession, meta_lba: u32) -> Result<(u32, u32)> {
        let extents = self.read_icb_extents(session, meta_lba)?;
        extents.first().copied().ok_or_else(|| Error::DiscRead { sector: 0 })
    }

    /// Read ALL allocation extents for a file from its ICB.
    /// Returns Vec of (partition_relative_lba, byte_length) pairs.
    /// Handles files with many extents (e.g. 88 GB m2ts files have ~90 extents).
    fn read_icb_extents(&self, session: &mut DriveSession, meta_lba: u32) -> Result<Vec<(u32, u32)>> {
        let mut icb = [0u8; 2048];
        read_sector(session, self.meta_to_abs(meta_lba), &mut icb)?;

        let tag = u16::from_le_bytes([icb[0], icb[1]]);

        // Get allocation descriptor offset and total length based on ICB type
        let (ad_offset, l_ad) = match tag {
            // Extended File Entry (UDF 2.50, used by BD-ROM)
            266 => {
                let l_ea = u32::from_le_bytes([icb[208], icb[209], icb[210], icb[211]]) as usize;
                let l_ad = u32::from_le_bytes([icb[212], icb[213], icb[214], icb[215]]) as usize;
                (216 + l_ea, l_ad)
            }
            // Standard File Entry
            261 => {
                let l_ea = u32::from_le_bytes([icb[168], icb[169], icb[170], icb[171]]) as usize;
                let l_ad = u32::from_le_bytes([icb[172], icb[173], icb[174], icb[175]]) as usize;
                (176 + l_ea, l_ad)
            }
            _ => return Err(Error::DiscRead { sector: 0 }),
        };

        let mut extents = Vec::new();
        let num_descriptors = l_ad / 8; // Short Allocation Descriptor = 8 bytes

        for i in 0..num_descriptors {
            let off = ad_offset + i * 8;
            if off + 8 > icb.len() {
                break;
            }

            let raw_len = u32::from_le_bytes([icb[off], icb[off + 1], icb[off + 2], icb[off + 3]]);
            let extent_type = raw_len >> 30;
            let data_len = raw_len & 0x3FFFFFFF;
            let data_lba = u32::from_le_bytes([icb[off + 4], icb[off + 5], icb[off + 6], icb[off + 7]]);

            match extent_type {
                0 => extents.push((data_lba, data_len)), // recorded and allocated
                1 => {} // allocated but not recorded (sparse) — skip
                3 => break, // next extent of allocation descriptors — TODO
                _ => break,
            }
        }

        Ok(extents)
    }

    /// Get all absolute disc sector extents for a file.
    /// Returns Vec of (absolute_lba, sector_count) covering the entire file.
    pub fn file_extents(&self, session: &mut DriveSession, path: &str) -> Result<Vec<(u32, u32)>> {
        let parts: Vec<&str> = path.trim_matches('/').split('/').collect();
        let mut current = &self.root;
        for part in &parts[..parts.len() - 1] {
            current = current.entries.iter().find(|e| {
                e.is_dir && e.name.eq_ignore_ascii_case(part)
            }).ok_or_else(|| Error::UdfNotFound { path: part.to_string() }
)?;
        }
        let filename = match parts.last() {
            Some(f) => f,
            None => return Err(Error::UdfNotFound { path: path.to_string() }),
        };
        let entry = current.entries.iter().find(|e| {
            !e.is_dir && e.name.eq_ignore_ascii_case(filename)
        }).ok_or_else(|| Error::UdfNotFound { path: path.to_string() }
)?;

        let alloc_extents = self.read_icb_extents(session, entry.meta_lba)?;
        let mut disc_extents = Vec::new();
        for (lba, byte_len) in alloc_extents {
            let abs_lba = self.partition_start + lba;
            let sectors = ((byte_len as u64 + 2047) / 2048) as u32;
            disc_extents.push((abs_lba, sectors));
        }
        Ok(disc_extents)
    }
}

/// Read the UDF filesystem from a Blu-ray disc.
///
/// Follows the UDF pointer chain:
/// 1. AVDP (sector 256) → VDS location
/// 2. VDS → Partition Descriptor (physical partition start)
///        → Logical Volume Descriptor (FSD location + partition maps)
/// 3. Metadata partition file → metadata content location
/// 4. FSD → root directory ICB
/// 5. Root directory → file tree
pub fn read_filesystem(session: &mut DriveSession) -> Result<UdfFs> {
    // Step 1: Anchor Volume Descriptor Pointer at sector 256
    // ECMA-167 §10.2 — always at sector 256
    let mut avdp = [0u8; 2048];
    read_sector(session, 256, &mut avdp)?;

    let tag_id = u16::from_le_bytes([avdp[0], avdp[1]]);
    if tag_id != 2 {
        return Err(Error::DiscRead { sector: 0 });
    }

    // Main VDS extent location: bytes [16:20] = LBA, [20:24] = length
    // (We use the VDS at sectors 32+, not the reserve copy at sector 32768+)

    // Step 2: Read Volume Descriptor Sequence (sectors 32-37 typically)
    // Find Partition Descriptor (tag 5) and Logical Volume Descriptor (tag 6)
    let mut partition_start: u32 = 0;
    let mut num_partition_maps: u32 = 0;
    let mut lvd_sector: Option<u32> = None;
    let mut volume_id = String::new();
    let mut metadata_size_bytes: u32 = 0;

    for i in 32..64 {
        let mut desc = [0u8; 2048];
        read_sector(session, i, &mut desc)?;

        let desc_tag = u16::from_le_bytes([desc[0], desc[1]]);
        match desc_tag {
            // Primary Volume Descriptor — volume identifier at offset 24, 32-byte d-string
            1 => {
                volume_id = parse_dstring(&desc[24..56]);
            }
            // Partition Descriptor — tells us where the physical partition starts
            5 => {
                partition_start = u32::from_le_bytes([desc[188], desc[189], desc[190], desc[191]]);
            }
            // Logical Volume Descriptor — contains FSD location and partition maps
            6 => {
                num_partition_maps = u32::from_le_bytes([desc[268], desc[269], desc[270], desc[271]]);
                lvd_sector = Some(i);
            }
            // Terminating Descriptor — end of VDS
            8 => break,
            _ => continue,
        }
    }

    if partition_start == 0 {
        return Err(Error::DiscRead { sector: 0 });
    }

    // Step 3: Parse partition maps from LVD to find metadata partition
    // BD-ROM discs (UDF 2.50) use a metadata partition (Type 2 map with "*UDF Metadata Partition")
    // The metadata file is stored at lba=0 of the physical partition
    let metadata_start = if num_partition_maps >= 2 {
        let lvd_sec = lvd_sector.ok_or_else(|| Error::DiscRead { sector: 0 })?;

        // Read LVD to check partition map type
        let mut lvd = [0u8; 2048];
        read_sector(session, lvd_sec, &mut lvd)?;

        // Parse partition maps starting at offset 440
        // Map 0 = Type 1 (physical), Map 1 = Type 2 (metadata)
        let _pm1_type = lvd[440]; // First map type
        let pm1_len = lvd[441] as usize;

        if pm1_len > 0 && 440 + pm1_len < 2048 {
            let pm2_type = lvd[440 + pm1_len]; // Second map type

            if pm2_type == 2 {
                // Type 2 = metadata partition
                // The metadata file ICB is at physical partition lba 0
                // Read it to find where the metadata content starts
                let meta_file_lba = partition_start; // lba 0 of partition
                let mut meta_icb = [0u8; 2048];
                read_sector(session, meta_file_lba, &mut meta_icb)?;

                let meta_tag = u16::from_le_bytes([meta_icb[0], meta_icb[1]]);
                if meta_tag == 266 {
                    // Extended File Entry — get allocation extent
                    let l_ea = u32::from_le_bytes([meta_icb[208], meta_icb[209],
                                                   meta_icb[210], meta_icb[211]]) as usize;
                    let ad_off = 216 + l_ea;
                    let ad_len = u32::from_le_bytes([meta_icb[ad_off], meta_icb[ad_off + 1],
                                                      meta_icb[ad_off + 2], meta_icb[ad_off + 3]]) & 0x3FFFFFFF;
                    metadata_size_bytes = ad_len;
                    let ad_pos = u32::from_le_bytes([meta_icb[ad_off + 4], meta_icb[ad_off + 5],
                                                     meta_icb[ad_off + 6], meta_icb[ad_off + 7]]);
                    // Metadata content starts at partition_start + ad_pos
                    partition_start + ad_pos
                } else {
                    // Fallback: no metadata partition, use physical partition directly
                    partition_start
                }
            } else {
                partition_start
            }
        } else {
            partition_start
        }
    } else {
        // Single partition map — no metadata partition (older UDF)
        partition_start
    };

    // Step 4: Read File Set Descriptor from metadata partition
    // FSD is at metadata-relative lba 0 (first sector of metadata content)
    let mut fsd = [0u8; 2048];
    read_sector(session, metadata_start, &mut fsd)?;

    let fsd_tag = u16::from_le_bytes([fsd[0], fsd[1]]);
    if fsd_tag != 256 {
        return Err(Error::DiscRead { sector: 0 });
    }

    // Root Directory ICB: long_ad at FSD offset 400
    // long_ad = extent_length(4) + extent_location: lba(4) + part_ref(2) + impl_use(6)
    let root_lba = u32::from_le_bytes([fsd[404], fsd[405], fsd[406], fsd[407]]);

    // Step 5: Read root directory and build file tree
    let root = read_directory(session, partition_start, metadata_start, root_lba, "", 0)?;

    let metadata_sectors = (metadata_size_bytes + 2047) / 2048;

    Ok(UdfFs {
        root,
        volume_id,
        partition_start,
        metadata_start,
        metadata_sectors,
    })
}

/// Read a UDF directory and its children (up to max_depth levels).
///
/// Each directory is an ICB (Extended File Entry) pointing to directory data
/// containing File Identifier Descriptors (FIDs). Each FID names a file/subdir
/// and points to its ICB.
fn read_directory(
    session: &mut DriveSession,
    part_start: u32,
    meta_start: u32,
    meta_lba: u32,
    name: &str,
    depth: u32,
) -> Result<DirEntry> {
    // Read ICB for this directory
    let mut icb = [0u8; 2048];
    read_sector(session, meta_start + meta_lba, &mut icb)?;

    let tag = u16::from_le_bytes([icb[0], icb[1]]);

    // Get allocation extent: where the directory data lives
    let (ad_len, ad_pos) = match tag {
        266 => {
            let l_ea = u32::from_le_bytes([icb[208], icb[209], icb[210], icb[211]]) as usize;
            let ad_off = 216 + l_ea;
            let len = u32::from_le_bytes([icb[ad_off], icb[ad_off + 1],
                                          icb[ad_off + 2], icb[ad_off + 3]]) & 0x3FFFFFFF;
            let pos = u32::from_le_bytes([icb[ad_off + 4], icb[ad_off + 5],
                                          icb[ad_off + 6], icb[ad_off + 7]]);
            (len, pos)
        }
        261 => {
            let l_ea = u32::from_le_bytes([icb[168], icb[169], icb[170], icb[171]]) as usize;
            let ad_off = 176 + l_ea;
            let len = u32::from_le_bytes([icb[ad_off], icb[ad_off + 1],
                                          icb[ad_off + 2], icb[ad_off + 3]]) & 0x3FFFFFFF;
            let pos = u32::from_le_bytes([icb[ad_off + 4], icb[ad_off + 5],
                                          icb[ad_off + 6], icb[ad_off + 7]]);
            (len, pos)
        }
        _ => {
            return Ok(DirEntry {
                name: name.to_string(), is_dir: true, meta_lba, size: 0, entries: Vec::new(),
            });
        }
    };

    // Read directory data
    let dir_abs = meta_start + ad_pos;
    let sector_count = ((ad_len + 2047) / 2048).min(64);
    let mut dir_data = vec![0u8; sector_count as usize * 2048];
    for i in 0..sector_count {
        read_sector(session, dir_abs + i,
                   &mut dir_data[(i as usize) * 2048..(i as usize + 1) * 2048])?;
    }

    // Parse File Identifier Descriptors
    let mut entries = Vec::new();
    let mut pos = 0;

    while pos + 38 < dir_data.len().min(ad_len as usize) {
        let fid_tag = u16::from_le_bytes([dir_data[pos], dir_data[pos + 1]]);
        if fid_tag != 257 {
            break;
        }

        let file_chars = dir_data[pos + 18];
        let l_fi = dir_data[pos + 19] as usize;

        // FID ICB is a long_ad starting at offset 20:
        //   [20:24] = extent_length
        //   [24:28] = extent_location (LBA within metadata partition)
        //   [28:30] = partition_reference_number
        //   [30:36] = implementation_use
        let icb_lba = u32::from_le_bytes([dir_data[pos + 24], dir_data[pos + 25],
                                          dir_data[pos + 26], dir_data[pos + 27]]);
        let l_iu = u16::from_le_bytes([dir_data[pos + 36], dir_data[pos + 37]]) as usize;

        let is_dir = (file_chars & 0x02) != 0;
        let is_parent = (file_chars & 0x08) != 0;

        if !is_parent && l_fi > 0 {
            let name_start = pos + 38 + l_iu;
            let entry_name = parse_udf_name(&dir_data[name_start..name_start + l_fi]);

            if !entry_name.is_empty() {
                // Read the ICB to get file size
                let file_size = read_file_size(session, meta_start, icb_lba).unwrap_or(0);

                if is_dir && depth < 3 {
                    // Recurse into subdirectory (max 3 levels: BDMV/PLAYLIST/*.mpls)
                    let subdir = read_directory(session, part_start, meta_start, icb_lba, &entry_name, depth + 1)?;
                    entries.push(subdir);
                } else {
                    entries.push(DirEntry {
                        name: entry_name,
                        is_dir,
                        meta_lba: icb_lba,
                        size: file_size,
                        entries: Vec::new(),
                    });
                }
            }
        }

        // Advance to next FID (4-byte aligned)
        let fid_len = ((38 + l_iu + l_fi + 3) & !3) as usize;
        pos += fid_len;
    }

    Ok(DirEntry {
        name: name.to_string(),
        is_dir: true,
        meta_lba,
        size: ad_len as u64,
        entries,
    })
}

/// Read file size (info_length) from an Extended File Entry ICB.
fn read_file_size(session: &mut DriveSession, meta_start: u32, meta_lba: u32) -> Result<u64> {
    let mut icb = [0u8; 2048];
    read_sector(session, meta_start + meta_lba, &mut icb)?;

    let tag = u16::from_le_bytes([icb[0], icb[1]]);
    match tag {
        // Both File Entry (261) and Extended File Entry (266) have
        // info_length as a u64 at offset 56
        261 | 266 => {
            Ok(u64::from_le_bytes([icb[56], icb[57], icb[58], icb[59],
                                   icb[60], icb[61], icb[62], icb[63]]))
        }
        _ => Ok(0),
    }
}

/// Parse a UDF filename from raw bytes.
///
/// UDF uses a compression ID as the first byte:
///   8  = 8-bit characters (ASCII)
///   16 = 16-bit big-endian Unicode (UTF-16BE)
fn parse_udf_name(data: &[u8]) -> String {
    if data.is_empty() {
        return String::new();
    }

    match data[0] {
        8 => {
            // 8-bit ASCII
            String::from_utf8_lossy(&data[1..]).trim().to_string()
        }
        16 => {
            // 16-bit big-endian Unicode
            let mut s = String::new();
            let chars = &data[1..];
            for i in (0..chars.len()).step_by(2) {
                if i + 1 < chars.len() {
                    let c = ((chars[i] as u16) << 8) | chars[i + 1] as u16;
                    if let Some(ch) = char::from_u32(c as u32) {
                        s.push(ch);
                    }
                }
            }
            s.trim().to_string()
        }
        _ => String::from_utf8_lossy(&data[1..]).trim().to_string(),
    }
}

/// Merge overlapping or adjacent (start, count) ranges.
fn merge_ranges(ranges: &[(u32, u32)]) -> Vec<(u32, u32)> {
    if ranges.is_empty() { return Vec::new(); }
    let mut result = vec![ranges[0]];
    for &(start, count) in &ranges[1..] {
        let last = result.last_mut().unwrap();
        let last_end = last.0 + last.1;
        if start <= last_end + 1 {
            // Overlapping or adjacent — extend
            let new_end = (start + count).max(last_end);
            last.1 = new_end - last.0;
        } else {
            result.push((start, count));
        }
    }
    result
}

/// Parse a UDF d-string (fixed-length field with length byte at the end).
/// Used for Volume Identifier and other UDF descriptor strings.
/// The first byte of content is a compression ID: 8 = ASCII, 16 = UTF-16BE.
fn parse_dstring(data: &[u8]) -> String {
    if data.is_empty() { return String::new(); }
    let len = *data.last().unwrap() as usize;
    if len == 0 || len > data.len() { return String::new(); }
    let content = &data[..len];
    if content.is_empty() { return String::new(); }
    match content[0] {
        8 => String::from_utf8_lossy(&content[1..]).trim_end_matches('\0').trim().to_string(),
        16 => {
            let mut s = String::new();
            let chars = &content[1..];
            for i in (0..chars.len()).step_by(2) {
                if i + 1 < chars.len() {
                    let c = ((chars[i] as u16) << 8) | chars[i + 1] as u16;
                    if c != 0 {
                        if let Some(ch) = char::from_u32(c as u32) { s.push(ch); }
                    }
                }
            }
            s.trim().to_string()
        }
        _ => String::from_utf8_lossy(&content[1..]).trim_end_matches('\0').trim().to_string(),
    }
}

/// Read a single 2048-byte sector from the drive.
/// Uses standard READ(10) — no unlock required.
fn read_sector(session: &mut DriveSession, lba: u32, buf: &mut [u8]) -> Result<()> {
    session.read_disc(lba, 1, buf)?;
    Ok(())
}