Skip to main content

hadris_iso/
rrip.rs

1//! Rock Ridge Interchange Protocol (RRIP)
2//!
3//! Rock Ridge extends ISO 9660 with POSIX filesystem semantics including:
4//! - Long filenames (NM entries)
5//! - POSIX permissions, ownership, timestamps (PX, TF entries)
6//! - Symbolic links (SL entries)
7//! - Device files (PN entries)
8//! - Deep directory relocation (CL, PL, RE entries)
9
10#[cfg(feature = "std")]
11use super::io::{self, Read, Writable, Write};
12use super::susp::SystemUseHeader;
13use crate::types::U32LsbMsb;
14
15#[cfg(feature = "alloc")]
16bitflags::bitflags! {
17    /// POSIX file mode bits for PX entries
18    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
19    pub struct PosixFileMode: u32 {
20        // File type bits (high nibble of mode)
21        /// Socket
22        const S_IFSOCK = 0o140000;
23        /// Symbolic link
24        const S_IFLNK  = 0o120000;
25        /// Regular file
26        const S_IFREG  = 0o100000;
27        /// Block device
28        const S_IFBLK  = 0o060000;
29        /// Directory
30        const S_IFDIR  = 0o040000;
31        /// Character device
32        const S_IFCHR  = 0o020000;
33        /// FIFO (named pipe)
34        const S_IFIFO  = 0o010000;
35
36        // Permission bits
37        /// Set user ID on execution
38        const S_ISUID = 0o4000;
39        /// Set group ID on execution
40        const S_ISGID = 0o2000;
41        /// Sticky bit
42        const S_ISVTX = 0o1000;
43
44        /// Owner read
45        const S_IRUSR = 0o0400;
46        /// Owner write
47        const S_IWUSR = 0o0200;
48        /// Owner execute
49        const S_IXUSR = 0o0100;
50
51        /// Group read
52        const S_IRGRP = 0o0040;
53        /// Group write
54        const S_IWGRP = 0o0020;
55        /// Group execute
56        const S_IXGRP = 0o0010;
57
58        /// Others read
59        const S_IROTH = 0o0004;
60        /// Others write
61        const S_IWOTH = 0o0002;
62        /// Others execute
63        const S_IXOTH = 0o0001;
64    }
65}
66
67#[cfg(feature = "alloc")]
68impl PosixFileMode {
69    /// Create mode for a regular file with given permissions
70    pub fn regular(perms: u32) -> Self {
71        Self::S_IFREG | Self::from_bits_truncate(perms & 0o7777)
72    }
73
74    /// Create mode for a directory with given permissions
75    pub fn directory(perms: u32) -> Self {
76        Self::S_IFDIR | Self::from_bits_truncate(perms & 0o7777)
77    }
78
79    /// Create mode for a symbolic link
80    pub fn symlink() -> Self {
81        Self::S_IFLNK | Self::from_bits_truncate(0o777)
82    }
83
84    /// Default permissions for files (0644)
85    pub fn default_file() -> Self {
86        Self::regular(0o644)
87    }
88
89    /// Default permissions for directories (0755)
90    pub fn default_dir() -> Self {
91        Self::directory(0o755)
92    }
93}
94
95/// PX - POSIX file attributes entry
96///
97/// Contains mode, nlink, uid, gid, and optionally serial number
98#[repr(C)]
99#[derive(Debug, Clone, Copy, bytemuck::Zeroable, bytemuck::Pod)]
100pub struct PxEntry {
101    /// File mode (both-endian)
102    pub file_mode: U32LsbMsb,
103    /// Number of links (both-endian)
104    pub file_links: U32LsbMsb,
105    /// User ID (both-endian)
106    pub file_uid: U32LsbMsb,
107    /// Group ID (both-endian)
108    pub file_gid: U32LsbMsb,
109    /// Serial number / inode (both-endian) - optional, RRIP 1.12+
110    pub file_serial: U32LsbMsb,
111}
112
113impl PxEntry {
114    /// Create a new PX entry
115    pub fn new(mode: u32, nlink: u32, uid: u32, gid: u32, serial: u32) -> Self {
116        Self {
117            file_mode: U32LsbMsb::new(mode),
118            file_links: U32LsbMsb::new(nlink),
119            file_uid: U32LsbMsb::new(uid),
120            file_gid: U32LsbMsb::new(gid),
121            file_serial: U32LsbMsb::new(serial),
122        }
123    }
124}
125
126#[cfg(feature = "std")]
127impl PxEntry {
128    /// Performs the `header` operation.
129    pub fn header(&self) -> SystemUseHeader {
130        SystemUseHeader {
131            sig: *b"PX",
132            length: 44,
133            version: 1,
134        }
135    }
136}
137
138/// PN - POSIX device number entry
139///
140/// Contains major and minor device numbers for character/block devices
141#[repr(C)]
142#[derive(Debug, Clone, Copy, bytemuck::Zeroable, bytemuck::Pod)]
143pub struct PnEntry {
144    /// Device high (major) number
145    pub dev_high: U32LsbMsb,
146    /// Device low (minor) number
147    pub dev_low: U32LsbMsb,
148}
149
150impl PnEntry {
151    /// Create a new PN entry
152    pub fn new(major: u32, minor: u32) -> Self {
153        Self {
154            dev_high: U32LsbMsb::new(major),
155            dev_low: U32LsbMsb::new(minor),
156        }
157    }
158
159    /// Get the device number as a combined value
160    pub fn dev(&self) -> u64 {
161        ((self.dev_high.read() as u64) << 32) | (self.dev_low.read() as u64)
162    }
163}
164
165#[cfg(feature = "std")]
166impl PnEntry {
167    /// Performs the `header` operation.
168    pub fn header(&self) -> SystemUseHeader {
169        SystemUseHeader {
170            sig: *b"PN",
171            length: 20,
172            version: 1,
173        }
174    }
175}
176
177#[cfg(feature = "alloc")]
178bitflags::bitflags! {
179    /// Flags for NM (alternate name) entries
180    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
181    pub struct NmFlags: u8 {
182        /// Name continues in next NM entry
183        const CONTINUE = 0x01;
184        /// Refers to current directory (.)
185        const CURRENT = 0x02;
186        /// Refers to parent directory (..)
187        const PARENT = 0x04;
188        /// Reserved
189        const RESERVED3 = 0x08;
190        /// Reserved
191        const RESERVED4 = 0x10;
192        /// Network node name (host)
193        const HOST = 0x20;
194    }
195}
196
197/// NM - Alternate name entry
198///
199/// Contains the real (POSIX) filename
200#[derive(Debug, Clone)]
201#[cfg(feature = "alloc")]
202pub struct NmEntry {
203    /// Flags indicating special name handling
204    pub flags: NmFlags,
205    /// The name content (may be partial if CONTINUE is set)
206    pub name: alloc::vec::Vec<u8>,
207}
208
209#[cfg(feature = "alloc")]
210impl NmEntry {
211    /// Create a new NM entry with the given name
212    pub fn new(name: &[u8]) -> Self {
213        Self {
214            flags: NmFlags::empty(),
215            name: name.to_vec(),
216        }
217    }
218
219    /// Create an NM entry for current directory (.)
220    pub fn current() -> Self {
221        Self {
222            flags: NmFlags::CURRENT,
223            name: alloc::vec::Vec::new(),
224        }
225    }
226
227    /// Create an NM entry for parent directory (..)
228    pub fn parent() -> Self {
229        Self {
230            flags: NmFlags::PARENT,
231            name: alloc::vec::Vec::new(),
232        }
233    }
234
235    /// Calculate the size of this entry
236    pub fn size(&self) -> usize {
237        5 + self.name.len()
238    }
239}
240
241#[cfg(feature = "std")]
242impl NmEntry {
243    /// Performs the `header` operation.
244    pub fn header(&self) -> SystemUseHeader {
245        SystemUseHeader {
246            sig: *b"NM",
247            length: self.size() as u8,
248            version: 1,
249        }
250    }
251}
252
253#[cfg(feature = "alloc")]
254bitflags::bitflags! {
255    /// Flags for SL (symbolic link) component records
256    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
257    pub struct SlComponentFlags: u8 {
258        /// Component continues in next record
259        const CONTINUE = 0x01;
260        /// Current directory (.)
261        const CURRENT = 0x02;
262        /// Parent directory (..)
263        const PARENT = 0x04;
264        /// Root directory (/)
265        const ROOT = 0x08;
266        /// Reserved
267        const RESERVED4 = 0x10;
268        /// Network mount point
269        const VOLROOT = 0x20;
270    }
271}
272
273/// A component of a symbolic link path
274#[derive(Debug, Clone)]
275#[cfg(feature = "alloc")]
276pub struct SlComponent {
277    /// Component flags
278    pub flags: SlComponentFlags,
279    /// Component content (path segment)
280    pub content: alloc::vec::Vec<u8>,
281}
282
283#[cfg(feature = "alloc")]
284impl SlComponent {
285    /// Create a component for a path segment
286    pub fn new(segment: &[u8]) -> Self {
287        Self {
288            flags: SlComponentFlags::empty(),
289            content: segment.to_vec(),
290        }
291    }
292
293    /// Create a root component (/)
294    pub fn root() -> Self {
295        Self {
296            flags: SlComponentFlags::ROOT,
297            content: alloc::vec::Vec::new(),
298        }
299    }
300
301    /// Create a current directory component (.)
302    pub fn current() -> Self {
303        Self {
304            flags: SlComponentFlags::CURRENT,
305            content: alloc::vec::Vec::new(),
306        }
307    }
308
309    /// Create a parent directory component (..)
310    pub fn parent() -> Self {
311        Self {
312            flags: SlComponentFlags::PARENT,
313            content: alloc::vec::Vec::new(),
314        }
315    }
316
317    /// Size of this component record
318    pub fn size(&self) -> usize {
319        2 + self.content.len()
320    }
321}
322
323/// SL - Symbolic link entry
324#[derive(Debug, Clone)]
325#[cfg(feature = "alloc")]
326pub struct SlEntry {
327    /// Flags for the overall SL entry
328    pub flags: u8,
329    /// Components of the symbolic link path
330    pub components: alloc::vec::Vec<SlComponent>,
331}
332
333#[cfg(feature = "alloc")]
334impl SlEntry {
335    /// Create a symbolic link entry from a target path
336    pub fn from_path(target: &str) -> Self {
337        let mut components = alloc::vec::Vec::new();
338
339        for component in hadris_path::VPath::new(target).components() {
340            match component {
341                hadris_path::Component::Root => components.push(SlComponent::root()),
342                hadris_path::Component::Current => components.push(SlComponent::current()),
343                hadris_path::Component::Parent => components.push(SlComponent::parent()),
344                hadris_path::Component::Normal(segment) => {
345                    components.push(SlComponent::new(segment.as_bytes()));
346                }
347            }
348        }
349
350        Self {
351            flags: 0,
352            components,
353        }
354    }
355
356    /// Calculate the size of this entry
357    pub fn size(&self) -> usize {
358        5 + self.components.iter().map(|c| c.size()).sum::<usize>()
359    }
360}
361
362#[cfg(feature = "std")]
363impl SlEntry {
364    /// Performs the `header` operation.
365    pub fn header(&self) -> SystemUseHeader {
366        SystemUseHeader {
367            sig: *b"SL",
368            length: self.size() as u8,
369            version: 1,
370        }
371    }
372}
373
374#[cfg(feature = "alloc")]
375bitflags::bitflags! {
376    /// Flags for TF (timestamp) entries indicating which timestamps are present
377    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
378    pub struct TfFlags: u8 {
379        /// Creation time present
380        const CREATION = 0x01;
381        /// Modification time present
382        const MODIFY = 0x02;
383        /// Access time present
384        const ACCESS = 0x04;
385        /// Attribute change time present
386        const ATTRIBUTES = 0x08;
387        /// Backup time present
388        const BACKUP = 0x10;
389        /// Expiration time present
390        const EXPIRATION = 0x20;
391        /// Effective time present
392        const EFFECTIVE = 0x40;
393        /// Long form timestamps (17 bytes vs 7 bytes)
394        const LONG_FORM = 0x80;
395    }
396}
397
398/// TF - Timestamps entry
399#[derive(Debug, Clone)]
400#[cfg(feature = "alloc")]
401pub struct TfEntry {
402    /// Flags indicating which timestamps are present
403    pub flags: TfFlags,
404    /// Timestamps in order (creation, modify, access, attributes, backup, expiration, effective)
405    /// Each is 7 bytes (short form) or 17 bytes (long form)
406    pub timestamps: alloc::vec::Vec<u8>,
407}
408
409#[cfg(feature = "alloc")]
410impl TfEntry {
411    /// Create a new TF entry with modification and access times (short form)
412    ///
413    /// Times are in ISO 9660 7-byte format: YY MM DD HH MM SS TZ
414    pub fn new_short(mtime: &[u8; 7], atime: &[u8; 7]) -> Self {
415        let flags = TfFlags::MODIFY | TfFlags::ACCESS;
416        let mut timestamps = alloc::vec::Vec::with_capacity(14);
417        timestamps.extend_from_slice(mtime);
418        timestamps.extend_from_slice(atime);
419        Self { flags, timestamps }
420    }
421
422    /// Calculate the size of this entry
423    pub fn size(&self) -> usize {
424        5 + self.timestamps.len()
425    }
426}
427
428#[cfg(feature = "std")]
429impl TfEntry {
430    /// Performs the `header` operation.
431    pub fn header(&self) -> SystemUseHeader {
432        SystemUseHeader {
433            sig: *b"TF",
434            length: self.size() as u8,
435            version: 1,
436        }
437    }
438}
439
440/// CL - Child link entry (for relocated directories)
441#[repr(C)]
442#[derive(Debug, Clone, Copy, bytemuck::Zeroable, bytemuck::Pod)]
443pub struct ClEntry {
444    /// Location of the actual directory
445    pub child_directory_location: U32LsbMsb,
446}
447
448impl ClEntry {
449    /// Create a new CL entry
450    pub fn new(location: u32) -> Self {
451        Self {
452            child_directory_location: U32LsbMsb::new(location),
453        }
454    }
455}
456
457#[cfg(feature = "std")]
458impl ClEntry {
459    /// Performs the `header` operation.
460    pub fn header(&self) -> SystemUseHeader {
461        SystemUseHeader {
462            sig: *b"CL",
463            length: 12,
464            version: 1,
465        }
466    }
467}
468
469/// PL - Parent link entry (for relocated directories)
470#[repr(C)]
471#[derive(Debug, Clone, Copy, bytemuck::Zeroable, bytemuck::Pod)]
472pub struct PlEntry {
473    /// Location of the original parent directory
474    pub parent_directory_location: U32LsbMsb,
475}
476
477impl PlEntry {
478    /// Create a new PL entry
479    pub fn new(location: u32) -> Self {
480        Self {
481            parent_directory_location: U32LsbMsb::new(location),
482        }
483    }
484}
485
486#[cfg(feature = "std")]
487impl PlEntry {
488    /// Performs the `header` operation.
489    pub fn header(&self) -> SystemUseHeader {
490        SystemUseHeader {
491            sig: *b"PL",
492            length: 12,
493            version: 1,
494        }
495    }
496}
497
498/// RE - Relocated directory marker
499///
500/// This entry marks a directory as being relocated from its original position
501#[derive(Debug, Clone, Copy)]
502pub struct ReEntry;
503
504#[cfg(feature = "std")]
505impl ReEntry {
506    /// Performs the `header` operation.
507    pub fn header(&self) -> SystemUseHeader {
508        SystemUseHeader {
509            sig: *b"RE",
510            length: 4,
511            version: 1,
512        }
513    }
514}
515
516io_transform! {
517#[cfg(feature = "std")]
518impl Writable for PxEntry {
519    async fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
520        self.header().write(writer).await?;
521        writer.write_all(bytemuck::bytes_of(self)).await?;
522        Ok(())
523    }
524}
525
526#[cfg(feature = "std")]
527impl PxEntry {
528    /// Performs the `parse_data` operation.
529    pub async fn parse_data<R: Read>(header: SystemUseHeader, data: &mut R) -> io::Result<Self> {
530        let mut buf = [0u8; 40];
531        let len = (header.length as usize).saturating_sub(4).min(40);
532        data.read_exact(&mut buf[..len]).await?;
533
534        Ok(Self {
535            file_mode: *bytemuck::from_bytes(&buf[0..8]),
536            file_links: *bytemuck::from_bytes(&buf[8..16]),
537            file_uid: *bytemuck::from_bytes(&buf[16..24]),
538            file_gid: *bytemuck::from_bytes(&buf[24..32]),
539            file_serial: if len >= 40 {
540                *bytemuck::from_bytes(&buf[32..40])
541            } else {
542                U32LsbMsb::new(0)
543            },
544        })
545    }
546}
547
548#[cfg(feature = "std")]
549impl Writable for PnEntry {
550    async fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
551        self.header().write(writer).await?;
552        writer.write_all(bytemuck::bytes_of(self)).await?;
553        Ok(())
554    }
555}
556
557#[cfg(feature = "std")]
558impl PnEntry {
559    /// Performs the `parse_data` operation.
560    pub async fn parse_data<R: Read>(_header: SystemUseHeader, data: &mut R) -> io::Result<Self> {
561        let mut buf = [0u8; 16];
562        data.read_exact(&mut buf).await?;
563        Ok(Self {
564            dev_high: *bytemuck::from_bytes(&buf[0..8]),
565            dev_low: *bytemuck::from_bytes(&buf[8..16]),
566        })
567    }
568}
569
570#[cfg(feature = "std")]
571impl Writable for NmEntry {
572    async fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
573        self.header().write(writer).await?;
574        writer.write_all(&[self.flags.bits()]).await?;
575        writer.write_all(&self.name).await?;
576        Ok(())
577    }
578}
579
580#[cfg(feature = "std")]
581impl NmEntry {
582    /// Performs the `parse_data` operation.
583    pub async fn parse_data<R: Read>(header: SystemUseHeader, data: &mut R) -> io::Result<Self> {
584        let mut flags_byte = [0u8; 1];
585        data.read_exact(&mut flags_byte).await?;
586        let name_len = (header.length as usize).saturating_sub(5);
587        let mut name = alloc::vec![0u8; name_len];
588        data.read_exact(&mut name).await?;
589        Ok(Self {
590            flags: NmFlags::from_bits_truncate(flags_byte[0]),
591            name,
592        })
593    }
594}
595
596#[cfg(feature = "std")]
597impl Writable for SlEntry {
598    async fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
599        self.header().write(writer).await?;
600        writer.write_all(&[self.flags]).await?;
601        for component in &self.components {
602            writer.write_all(&[component.flags.bits(), component.content.len() as u8]).await?;
603            writer.write_all(&component.content).await?;
604        }
605        Ok(())
606    }
607}
608
609#[cfg(feature = "std")]
610impl SlEntry {
611    /// Performs the `parse_data` operation.
612    pub async fn parse_data<R: Read>(header: SystemUseHeader, data: &mut R) -> io::Result<Self> {
613        let mut flags = [0u8; 1];
614        data.read_exact(&mut flags).await?;
615
616        let mut remaining = (header.length as usize).saturating_sub(5);
617        let mut components = alloc::vec::Vec::new();
618
619        while remaining >= 2 {
620            let mut comp_header = [0u8; 2];
621            data.read_exact(&mut comp_header).await?;
622            remaining -= 2;
623
624            let comp_flags = SlComponentFlags::from_bits_truncate(comp_header[0]);
625            let comp_len = comp_header[1] as usize;
626
627            let mut content = alloc::vec![0u8; comp_len.min(remaining)];
628            data.read_exact(&mut content).await?;
629            remaining -= content.len();
630
631            components.push(SlComponent {
632                flags: comp_flags,
633                content,
634            });
635        }
636
637        Ok(Self {
638            flags: flags[0],
639            components,
640        })
641    }
642}
643
644#[cfg(feature = "std")]
645impl Writable for TfEntry {
646    async fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
647        self.header().write(writer).await?;
648        writer.write_all(&[self.flags.bits()]).await?;
649        writer.write_all(&self.timestamps).await?;
650        Ok(())
651    }
652}
653
654#[cfg(feature = "std")]
655impl TfEntry {
656    /// Performs the `parse_data` operation.
657    pub async fn parse_data<R: Read>(header: SystemUseHeader, data: &mut R) -> io::Result<Self> {
658        let mut flags_byte = [0u8; 1];
659        data.read_exact(&mut flags_byte).await?;
660        let flags = TfFlags::from_bits_truncate(flags_byte[0]);
661
662        let ts_len = (header.length as usize).saturating_sub(5);
663        let mut timestamps = alloc::vec![0u8; ts_len];
664        data.read_exact(&mut timestamps).await?;
665
666        Ok(Self { flags, timestamps })
667    }
668}
669
670#[cfg(feature = "std")]
671impl Writable for ClEntry {
672    async fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
673        self.header().write(writer).await?;
674        writer.write_all(bytemuck::bytes_of(&self.child_directory_location)).await?;
675        Ok(())
676    }
677}
678
679#[cfg(feature = "std")]
680impl ClEntry {
681    /// Performs the `parse_data` operation.
682    pub async fn parse_data<R: Read>(_header: SystemUseHeader, data: &mut R) -> io::Result<Self> {
683        let mut buf = [0u8; 8];
684        data.read_exact(&mut buf).await?;
685        Ok(Self {
686            child_directory_location: *bytemuck::from_bytes(&buf),
687        })
688    }
689}
690
691#[cfg(feature = "std")]
692impl Writable for PlEntry {
693    async fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
694        self.header().write(writer).await?;
695        writer.write_all(bytemuck::bytes_of(&self.parent_directory_location)).await?;
696        Ok(())
697    }
698}
699
700#[cfg(feature = "std")]
701impl PlEntry {
702    /// Performs the `parse_data` operation.
703    pub async fn parse_data<R: Read>(_header: SystemUseHeader, data: &mut R) -> io::Result<Self> {
704        let mut buf = [0u8; 8];
705        data.read_exact(&mut buf).await?;
706        Ok(Self {
707            parent_directory_location: *bytemuck::from_bytes(&buf),
708        })
709    }
710}
711
712#[cfg(feature = "std")]
713impl Writable for ReEntry {
714    async fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
715        self.header().write(writer).await
716    }
717}
718} // io_transform!
719
720/// Options for Rock Ridge extension support
721#[derive(Debug, Clone, Copy)]
722#[cfg(feature = "alloc")]
723pub struct RripOptions {
724    /// Enable Rock Ridge extensions
725    pub enabled: bool,
726    /// Automatically relocate directories deeper than 8 levels
727    pub relocate_deep_dirs: bool,
728    /// Preserve POSIX permissions
729    pub preserve_permissions: bool,
730    /// Preserve owner/group information
731    pub preserve_ownership: bool,
732    /// Preserve timestamps
733    pub preserve_timestamps: bool,
734    /// Preserve symbolic links
735    pub preserve_symlinks: bool,
736    /// Preserve device files (char/block)
737    pub preserve_devices: bool,
738}
739
740#[cfg(feature = "alloc")]
741impl Default for RripOptions {
742    fn default() -> Self {
743        Self {
744            enabled: true,
745            relocate_deep_dirs: true,
746            preserve_permissions: true,
747            preserve_ownership: true,
748            preserve_timestamps: true,
749            preserve_symlinks: true,
750            preserve_devices: true,
751        }
752    }
753}
754
755#[cfg(feature = "alloc")]
756impl RripOptions {
757    /// Create options with all features disabled
758    pub fn disabled() -> Self {
759        Self {
760            enabled: false,
761            relocate_deep_dirs: false,
762            preserve_permissions: false,
763            preserve_ownership: false,
764            preserve_timestamps: false,
765            preserve_symlinks: false,
766            preserve_devices: false,
767        }
768    }
769}
770
771/// Builder for Rock Ridge system use entries
772#[cfg(feature = "alloc")]
773pub struct RripBuilder {
774    builder: super::susp::SystemUseBuilder,
775}
776
777#[cfg(feature = "alloc")]
778impl RripBuilder {
779    /// Create a new RRIP builder
780    pub fn new() -> Self {
781        Self {
782            builder: super::susp::SystemUseBuilder::new(),
783        }
784    }
785
786    /// Add the SP entry (for root directory only)
787    pub fn add_sp(&mut self, bytes_skipped: u8) -> &mut Self {
788        self.builder.add_sp(bytes_skipped);
789        self
790    }
791
792    /// Add the ER entry declaring RRIP support (for root directory only)
793    pub fn add_rrip_er(&mut self) -> &mut Self {
794        // RRIP 1.12 extension reference
795        self.builder.add_er(
796            "RRIP_1991A",
797            "THE ROCK RIDGE INTERCHANGE PROTOCOL PROVIDES SUPPORT FOR POSIX FILE SYSTEM SEMANTICS",
798            "PLEASE CONTACT DISC PUBLISHER FOR SPECIFICATION SOURCE. SEE PUBLISHER IDENTIFIER IN PRIMARY VOLUME DESCRIPTOR FOR CONTACT INFORMATION.",
799            1,
800        );
801        self
802    }
803
804    /// Add a PX entry for POSIX attributes
805    pub fn add_px(&mut self, mode: u32, nlink: u32, uid: u32, gid: u32, serial: u32) -> &mut Self {
806        let px = PxEntry::new(mode, nlink, uid, gid, serial);
807        let mut buf = alloc::vec![0u8; 44];
808        buf[0..2].copy_from_slice(b"PX");
809        buf[2] = 44;
810        buf[3] = 1;
811        buf[4..44].copy_from_slice(bytemuck::bytes_of(&px));
812        self.builder.add_raw(buf);
813        self
814    }
815
816    /// Add a PN entry for device files
817    pub fn add_pn(&mut self, major: u32, minor: u32) -> &mut Self {
818        let pn = PnEntry::new(major, minor);
819        let mut buf = alloc::vec![0u8; 20];
820        buf[0..2].copy_from_slice(b"PN");
821        buf[2] = 20;
822        buf[3] = 1;
823        buf[4..20].copy_from_slice(bytemuck::bytes_of(&pn));
824        self.builder.add_raw(buf);
825        self
826    }
827
828    /// Add an NM entry for the alternate name
829    pub fn add_nm(&mut self, name: &[u8]) -> &mut Self {
830        // Handle names that are too long by splitting into multiple NM entries
831        let mut remaining = name;
832        let max_per_entry = 250; // Leave room for header + flags
833
834        while !remaining.is_empty() {
835            let chunk_len = remaining.len().min(max_per_entry);
836            let (chunk, rest) = remaining.split_at(chunk_len);
837            remaining = rest;
838
839            let flags = if remaining.is_empty() { 0 } else { 0x01 }; // CONTINUE flag
840            let total_len = 5 + chunk.len();
841            let mut buf = alloc::vec![0u8; total_len];
842            buf[0..2].copy_from_slice(b"NM");
843            buf[2] = total_len as u8;
844            buf[3] = 1;
845            buf[4] = flags;
846            buf[5..].copy_from_slice(chunk);
847            self.builder.add_raw(buf);
848        }
849        self
850    }
851
852    /// Add an NM entry for current directory (.)
853    pub fn add_nm_current(&mut self) -> &mut Self {
854        let buf = alloc::vec![b'N', b'M', 5, 1, 0x02]; // CURRENT flag
855        self.builder.add_raw(buf);
856        self
857    }
858
859    /// Add an NM entry for parent directory (..)
860    pub fn add_nm_parent(&mut self) -> &mut Self {
861        let buf = alloc::vec![b'N', b'M', 5, 1, 0x04]; // PARENT flag
862        self.builder.add_raw(buf);
863        self
864    }
865
866    /// Add an SL entry for a symbolic link
867    pub fn add_sl(&mut self, target: &str) -> &mut Self {
868        let sl = SlEntry::from_path(target);
869        let size = sl.size();
870        let mut buf = alloc::vec![0u8; size];
871        buf[0..2].copy_from_slice(b"SL");
872        buf[2] = size as u8;
873        buf[3] = 1;
874        buf[4] = sl.flags;
875
876        let mut offset = 5;
877        for component in &sl.components {
878            buf[offset] = component.flags.bits();
879            buf[offset + 1] = component.content.len() as u8;
880            buf[offset + 2..offset + 2 + component.content.len()]
881                .copy_from_slice(&component.content);
882            offset += 2 + component.content.len();
883        }
884
885        self.builder.add_raw(buf);
886        self
887    }
888
889    /// Add a short-form TF entry carrying modify and access timestamps.
890    pub fn add_tf_short(&mut self, mtime: &[u8; 7], atime: &[u8; 7]) -> &mut Self {
891        self.add_tf(None, mtime, atime)
892    }
893
894    /// Add a short-form (7-byte) TF timestamp entry.
895    ///
896    /// Emits the creation timestamp when provided (RRIP `CREATION` flag, 0x01),
897    /// followed by modify (0x02) and access (0x04). Timestamps are written in
898    /// ascending flag-bit order, as required by RRIP.
899    pub fn add_tf(
900        &mut self,
901        creation: Option<&[u8; 7]>,
902        mtime: &[u8; 7],
903        atime: &[u8; 7],
904    ) -> &mut Self {
905        // CREATION | MODIFY | ACCESS, short form (no LONG_FORM bit).
906        let mut flags = 0x06u8; // MODIFY | ACCESS
907        let mut len = 5 + 7 + 7;
908        if creation.is_some() {
909            flags |= 0x01; // CREATION
910            len += 7;
911        }
912        let mut buf = alloc::vec![0u8; len];
913        buf[0..2].copy_from_slice(b"TF");
914        buf[2] = len as u8;
915        buf[3] = 1;
916        buf[4] = flags;
917        let mut off = 5;
918        if let Some(ctime) = creation {
919            buf[off..off + 7].copy_from_slice(ctime);
920            off += 7;
921        }
922        buf[off..off + 7].copy_from_slice(mtime);
923        buf[off + 7..off + 14].copy_from_slice(atime);
924        self.builder.add_raw(buf);
925        self
926    }
927
928    /// Add a CL entry for child link (relocated directory)
929    pub fn add_cl(&mut self, child_location: u32) -> &mut Self {
930        let cl = ClEntry::new(child_location);
931        let mut buf = alloc::vec![0u8; 12];
932        buf[0..2].copy_from_slice(b"CL");
933        buf[2] = 12;
934        buf[3] = 1;
935        buf[4..12].copy_from_slice(bytemuck::bytes_of(&cl.child_directory_location));
936        self.builder.add_raw(buf);
937        self
938    }
939
940    /// Add a PL entry for parent link (relocated directory)
941    pub fn add_pl(&mut self, parent_location: u32) -> &mut Self {
942        let pl = PlEntry::new(parent_location);
943        let mut buf = alloc::vec![0u8; 12];
944        buf[0..2].copy_from_slice(b"PL");
945        buf[2] = 12;
946        buf[3] = 1;
947        buf[4..12].copy_from_slice(bytemuck::bytes_of(&pl.parent_directory_location));
948        self.builder.add_raw(buf);
949        self
950    }
951
952    /// Add a RE entry (marks a relocated directory)
953    pub fn add_re(&mut self) -> &mut Self {
954        let buf = alloc::vec![b'R', b'E', 4, 1];
955        self.builder.add_raw(buf);
956        self
957    }
958
959    /// Add a CE entry pointing to a continuation area
960    pub fn add_ce(&mut self, ce: super::susp::ContinuationArea) -> &mut Self {
961        self.builder.add_ce(ce);
962        self
963    }
964
965    /// Add the ST terminator
966    pub fn add_st(&mut self) -> &mut Self {
967        self.builder.add_st();
968        self
969    }
970
971    /// Get the total size of the system use area
972    pub fn size(&self) -> usize {
973        self.builder.size()
974    }
975
976    /// Build the system use area
977    pub fn build(&self) -> alloc::vec::Vec<u8> {
978        self.builder.build()
979    }
980
981    /// Split entries across inline and overflow areas.
982    ///
983    /// Delegates to [`super::susp::SystemUseBuilder::build_split`].
984    pub fn build_split(&self, max_inline: usize) -> super::susp::SplitSu {
985        self.builder.build_split(max_inline)
986    }
987
988    /// Check if the builder is empty
989    pub fn is_empty(&self) -> bool {
990        self.builder.is_empty()
991    }
992}
993
994#[cfg(feature = "alloc")]
995impl Default for RripBuilder {
996    fn default() -> Self {
997        Self::new()
998    }
999}
1000
1001/// Parsed Rock Ridge entry
1002#[derive(Debug, Clone)]
1003#[cfg(feature = "alloc")]
1004pub enum RockRidgeEntry {
1005    /// PX - POSIX attributes
1006    PosixAttributes(PxEntry),
1007    /// PN - Device numbers
1008    DeviceNumber(PnEntry),
1009    /// NM - Alternate name
1010    AlternateName(NmEntry),
1011    /// SL - Symbolic link
1012    SymbolicLink(SlEntry),
1013    /// TF - Timestamps
1014    Timestamps(TfEntry),
1015    /// CL - Child link
1016    ChildLink(ClEntry),
1017    /// PL - Parent link
1018    ParentLink(PlEntry),
1019    /// RE - Relocated marker
1020    Relocated,
1021    /// Unknown entry
1022    Unknown(SystemUseHeader),
1023}
1024
1025sync_only! {
1026#[cfg(all(feature = "std", test))]
1027mod tests {
1028    use super::*;
1029    use alloc::vec;
1030
1031    static_assertions::const_assert_eq!(size_of::<PxEntry>(), 40);
1032    static_assertions::const_assert_eq!(size_of::<PnEntry>(), 16);
1033    static_assertions::const_assert_eq!(size_of::<ClEntry>(), 8);
1034    static_assertions::const_assert_eq!(size_of::<PlEntry>(), 8);
1035
1036    #[test]
1037    fn test_px_entry_new() {
1038        let px = PxEntry::new(0o100644, 1, 1000, 1000, 12345);
1039        assert_eq!(px.file_mode.read(), 0o100644);
1040        assert_eq!(px.file_links.read(), 1);
1041        assert_eq!(px.file_uid.read(), 1000);
1042        assert_eq!(px.file_gid.read(), 1000);
1043        assert_eq!(px.file_serial.read(), 12345);
1044    }
1045
1046    #[test]
1047    fn test_px_entry_header() {
1048        let px = PxEntry::new(0o100644, 1, 0, 0, 0);
1049        let header = px.header();
1050        assert_eq!(&header.sig, b"PX");
1051        assert_eq!(header.length, 44);
1052        assert_eq!(header.version, 1);
1053    }
1054
1055    #[test]
1056    fn test_pn_entry_new() {
1057        let pn = PnEntry::new(8, 1);
1058        assert_eq!(pn.dev_high.read(), 8);
1059        assert_eq!(pn.dev_low.read(), 1);
1060    }
1061
1062    #[test]
1063    fn test_pn_entry_dev() {
1064        let pn = PnEntry::new(0x12345678, 0xABCDEF01);
1065        let dev = pn.dev();
1066        assert_eq!(dev, (0x12345678u64 << 32) | 0xABCDEF01u64);
1067    }
1068
1069    #[test]
1070    fn test_cl_entry_new() {
1071        let cl = ClEntry::new(100);
1072        assert_eq!(cl.child_directory_location.read(), 100);
1073    }
1074
1075    #[test]
1076    fn test_pl_entry_new() {
1077        let pl = PlEntry::new(50);
1078        assert_eq!(pl.parent_directory_location.read(), 50);
1079    }
1080
1081    #[test]
1082    fn test_posix_file_mode_regular() {
1083        let mode = PosixFileMode::regular(0o644);
1084        assert!(mode.contains(PosixFileMode::S_IFREG));
1085        assert!(mode.contains(PosixFileMode::S_IRUSR));
1086        assert!(mode.contains(PosixFileMode::S_IWUSR));
1087        assert!(mode.contains(PosixFileMode::S_IRGRP));
1088        assert!(mode.contains(PosixFileMode::S_IROTH));
1089        assert!(!mode.contains(PosixFileMode::S_IXUSR));
1090    }
1091
1092    #[test]
1093    fn test_posix_file_mode_directory() {
1094        let mode = PosixFileMode::directory(0o755);
1095        assert!(mode.contains(PosixFileMode::S_IFDIR));
1096        assert!(mode.contains(PosixFileMode::S_IRUSR));
1097        assert!(mode.contains(PosixFileMode::S_IWUSR));
1098        assert!(mode.contains(PosixFileMode::S_IXUSR));
1099        assert!(mode.contains(PosixFileMode::S_IRGRP));
1100        assert!(mode.contains(PosixFileMode::S_IXGRP));
1101    }
1102
1103    #[test]
1104    fn test_posix_file_mode_symlink() {
1105        let mode = PosixFileMode::symlink();
1106        assert!(mode.contains(PosixFileMode::S_IFLNK));
1107    }
1108
1109    #[test]
1110    fn test_nm_entry_new() {
1111        let nm = NmEntry::new(b"test_file.txt");
1112        assert!(nm.flags.is_empty());
1113        assert_eq!(nm.name, b"test_file.txt");
1114        assert_eq!(nm.size(), 5 + 13);
1115    }
1116
1117    #[test]
1118    fn test_nm_entry_special() {
1119        let current = NmEntry::current();
1120        assert!(current.flags.contains(NmFlags::CURRENT));
1121        assert!(current.name.is_empty());
1122
1123        let parent = NmEntry::parent();
1124        assert!(parent.flags.contains(NmFlags::PARENT));
1125        assert!(parent.name.is_empty());
1126    }
1127
1128    #[test]
1129    fn test_sl_component_new() {
1130        let comp = SlComponent::new(b"subdir");
1131        assert!(comp.flags.is_empty());
1132        assert_eq!(comp.content, b"subdir");
1133        assert_eq!(comp.size(), 2 + 6);
1134    }
1135
1136    #[test]
1137    fn test_sl_component_special() {
1138        let root = SlComponent::root();
1139        assert!(root.flags.contains(SlComponentFlags::ROOT));
1140        assert!(root.content.is_empty());
1141
1142        let current = SlComponent::current();
1143        assert!(current.flags.contains(SlComponentFlags::CURRENT));
1144
1145        let parent = SlComponent::parent();
1146        assert!(parent.flags.contains(SlComponentFlags::PARENT));
1147    }
1148
1149    #[test]
1150    fn test_sl_entry_from_path_absolute() {
1151        let sl = SlEntry::from_path("/usr/bin/test");
1152        assert_eq!(sl.components.len(), 4); // root + usr + bin + test
1153        assert!(sl.components[0].flags.contains(SlComponentFlags::ROOT));
1154        assert_eq!(sl.components[1].content, b"usr");
1155        assert_eq!(sl.components[2].content, b"bin");
1156        assert_eq!(sl.components[3].content, b"test");
1157    }
1158
1159    #[test]
1160    fn test_sl_entry_from_path_relative() {
1161        let sl = SlEntry::from_path("../lib/libtest.so");
1162        assert_eq!(sl.components.len(), 3); // .. + lib + libtest.so
1163        assert!(sl.components[0].flags.contains(SlComponentFlags::PARENT));
1164        assert_eq!(sl.components[1].content, b"lib");
1165        assert_eq!(sl.components[2].content, b"libtest.so");
1166    }
1167
1168    #[test]
1169    fn test_sl_entry_from_path_current() {
1170        let sl = SlEntry::from_path("./local");
1171        assert_eq!(sl.components.len(), 2); // . + local
1172        assert!(sl.components[0].flags.contains(SlComponentFlags::CURRENT));
1173        assert_eq!(sl.components[1].content, b"local");
1174    }
1175
1176    #[test]
1177    fn test_tf_entry_new_short() {
1178        let mtime = [126, 1, 15, 10, 30, 0, 0]; // 2026-01-15 10:30:00 UTC
1179        let atime = [126, 1, 15, 12, 0, 0, 0]; // 2026-01-15 12:00:00 UTC
1180        let tf = TfEntry::new_short(&mtime, &atime);
1181        assert!(tf.flags.contains(TfFlags::MODIFY));
1182        assert!(tf.flags.contains(TfFlags::ACCESS));
1183        assert!(!tf.flags.contains(TfFlags::LONG_FORM));
1184        assert_eq!(tf.timestamps.len(), 14);
1185    }
1186
1187    #[test]
1188    fn test_rrip_options_default() {
1189        let opts = RripOptions::default();
1190        assert!(opts.enabled);
1191        assert!(opts.relocate_deep_dirs);
1192        assert!(opts.preserve_permissions);
1193        assert!(opts.preserve_ownership);
1194        assert!(opts.preserve_timestamps);
1195        assert!(opts.preserve_symlinks);
1196        assert!(opts.preserve_devices);
1197    }
1198
1199    #[test]
1200    fn test_rrip_options_disabled() {
1201        let opts = RripOptions::disabled();
1202        assert!(!opts.enabled);
1203        assert!(!opts.relocate_deep_dirs);
1204        assert!(!opts.preserve_permissions);
1205    }
1206
1207    #[test]
1208    fn test_rrip_builder_empty() {
1209        let builder = RripBuilder::new();
1210        assert!(builder.is_empty());
1211        assert_eq!(builder.size(), 0);
1212    }
1213
1214    #[test]
1215    fn test_rrip_builder_px() {
1216        let mut builder = RripBuilder::new();
1217        builder.add_px(0o100644, 1, 1000, 1000, 12345);
1218        assert_eq!(builder.size(), 44);
1219
1220        let data = builder.build();
1221        assert_eq!(&data[0..2], b"PX");
1222        assert_eq!(data[2], 44); // length
1223        assert_eq!(data[3], 1); // version
1224    }
1225
1226    #[test]
1227    fn test_rrip_builder_pn() {
1228        let mut builder = RripBuilder::new();
1229        builder.add_pn(8, 1);
1230        assert_eq!(builder.size(), 20);
1231
1232        let data = builder.build();
1233        assert_eq!(&data[0..2], b"PN");
1234        assert_eq!(data[2], 20);
1235    }
1236
1237    #[test]
1238    fn test_rrip_builder_nm() {
1239        let mut builder = RripBuilder::new();
1240        builder.add_nm(b"test.txt");
1241        assert_eq!(builder.size(), 5 + 8); // header(5) + name(8)
1242
1243        let data = builder.build();
1244        assert_eq!(&data[0..2], b"NM");
1245        assert_eq!(data[4], 0); // no flags
1246        assert_eq!(&data[5..], b"test.txt");
1247    }
1248
1249    #[test]
1250    fn test_rrip_builder_nm_special() {
1251        let mut builder = RripBuilder::new();
1252        builder.add_nm_current();
1253        let data = builder.build();
1254        assert_eq!(data[4], 0x02); // CURRENT flag
1255
1256        let mut builder = RripBuilder::new();
1257        builder.add_nm_parent();
1258        let data = builder.build();
1259        assert_eq!(data[4], 0x04); // PARENT flag
1260    }
1261
1262    #[test]
1263    fn test_rrip_builder_sl() {
1264        let mut builder = RripBuilder::new();
1265        builder.add_sl("../lib");
1266
1267        let data = builder.build();
1268        assert_eq!(&data[0..2], b"SL");
1269    }
1270
1271    #[test]
1272    fn test_rrip_builder_cl_pl_re() {
1273        let mut builder = RripBuilder::new();
1274        builder.add_cl(100).add_pl(50).add_re();
1275
1276        let data = builder.build();
1277        // CL
1278        assert_eq!(&data[0..2], b"CL");
1279        assert_eq!(data[2], 12);
1280        // PL starts at offset 12
1281        assert_eq!(&data[12..14], b"PL");
1282        assert_eq!(data[14], 12);
1283        // RE starts at offset 24
1284        assert_eq!(&data[24..26], b"RE");
1285        assert_eq!(data[26], 4);
1286    }
1287
1288    #[test]
1289    fn test_rrip_builder_complete() {
1290        let mut builder = RripBuilder::new();
1291        builder
1292            .add_sp(0)
1293            .add_rrip_er()
1294            .add_px(0o100644, 1, 1000, 1000, 1)
1295            .add_nm(b"testfile.txt")
1296            .add_st();
1297
1298        let data = builder.build();
1299        assert!(!data.is_empty());
1300        // Verify SP is first
1301        assert_eq!(&data[0..2], b"SP");
1302    }
1303
1304    #[test]
1305    fn test_rrip_builder_long_name_split() {
1306        let mut builder = RripBuilder::new();
1307        // Create a name longer than 250 bytes
1308        let long_name = vec![b'a'; 300];
1309        builder.add_nm(&long_name);
1310
1311        let data = builder.build();
1312        // Should have at least 2 NM entries
1313        assert_eq!(&data[0..2], b"NM");
1314        // First entry should have CONTINUE flag set
1315        assert_eq!(data[4] & 0x01, 0x01);
1316    }
1317}
1318}