diff --git a/cli/src/command/core.rs b/cli/src/command/core.rs index b8140c58d..02cae960f 100644 --- a/cli/src/command/core.rs +++ b/cli/src/command/core.rs @@ -296,6 +296,21 @@ pub(crate) enum StoreAs { Dir, Symlink, Hardlink(PathBuf), + /// Block device with major and minor numbers. + #[cfg(unix)] + BlockDevice { + major: u32, + minor: u32, + }, + /// Character device with major and minor numbers. + #[cfg(unix)] + CharDevice { + major: u32, + minor: u32, + }, + /// FIFO (named pipe). + #[cfg(unix)] + Fifo, } /// Collects items from multiple paths, preserving CLI argument order. @@ -451,10 +466,35 @@ pub(crate) fn collect_items_with_state( None } } else { - return Err(io::Error::new( - io::ErrorKind::Unsupported, - format!("Unsupported file type: {}", path.display()), - )); + #[cfg(unix)] + { + use std::os::unix::fs::{FileTypeExt, MetadataExt}; + let metadata = fs::symlink_metadata(path)?; + let ft = metadata.file_type(); + if ft.is_block_device() { + let rdev = metadata.rdev(); + let (major, minor) = utils::fs::decode_rdev(rdev); + Some(StoreAs::BlockDevice { major, minor }) + } else if ft.is_char_device() { + let rdev = metadata.rdev(); + let (major, minor) = utils::fs::decode_rdev(rdev); + Some(StoreAs::CharDevice { major, minor }) + } else if ft.is_fifo() { + Some(StoreAs::Fifo) + } else { + return Err(io::Error::new( + io::ErrorKind::Unsupported, + format!("Unsupported file type: {}", path.display()), + )); + } + } + #[cfg(not(unix))] + { + return Err(io::Error::new( + io::ErrorKind::Unsupported, + format!("Unsupported file type: {}", path.display()), + )); + } }; if let Some(store) = store_as { @@ -600,6 +640,45 @@ pub(crate) fn create_entry( )? .build() } + #[cfg(unix)] + StoreAs::BlockDevice { major, minor } => { + let entry = EntryBuilder::new_block_device(entry_name, *major, *minor)?; + apply_metadata( + entry, + path, + keep_options, + owner_options, + time_options, + fs::symlink_metadata, + )? + .build() + } + #[cfg(unix)] + StoreAs::CharDevice { major, minor } => { + let entry = EntryBuilder::new_char_device(entry_name, *major, *minor)?; + apply_metadata( + entry, + path, + keep_options, + owner_options, + time_options, + fs::symlink_metadata, + )? + .build() + } + #[cfg(unix)] + StoreAs::Fifo => { + let entry = EntryBuilder::new_fifo(entry_name); + apply_metadata( + entry, + path, + keep_options, + owner_options, + time_options, + fs::symlink_metadata, + )? + .build() + } } .map(Some) } diff --git a/cli/src/command/diff.rs b/cli/src/command/diff.rs index 885dc5969..de5d8df25 100644 --- a/cli/src/command/diff.rs +++ b/cli/src/command/diff.rs @@ -59,6 +59,9 @@ fn compare_entry>(entry: NormalEntry, password: Option<&[u8]>) DataKind::Directory => println!("Missing directory: {path}"), DataKind::SymbolicLink => println!("Missing symbolic link: {path}"), DataKind::HardLink => println!("Missing hard link: {path}"), + DataKind::BlockDevice => println!("Missing block device: {path}"), + DataKind::CharDevice => println!("Missing character device: {path}"), + DataKind::Fifo => println!("Missing FIFO: {path}"), } return Ok(()); } @@ -88,7 +91,12 @@ fn compare_entry>(entry: NormalEntry, password: Option<&[u8]>) } Err(e) => return Err(e), }, - DataKind::File | DataKind::Directory | DataKind::SymbolicLink => { + DataKind::File + | DataKind::Directory + | DataKind::SymbolicLink + | DataKind::BlockDevice + | DataKind::CharDevice + | DataKind::Fifo => { println!("Mismatch file type: {path}") } DataKind::HardLink => (), diff --git a/cli/src/command/extract.rs b/cli/src/command/extract.rs index 5e5d0f946..afc7fc2be 100644 --- a/cli/src/command/extract.rs +++ b/cli/src/command/extract.rs @@ -690,6 +690,109 @@ where } fs::hard_link(original, &path)?; } + DataKind::BlockDevice => { + #[cfg(unix)] + { + let Some(dev) = item.read_device_numbers(ReadOptions::with_password(password))? + else { + log::warn!( + "Skipping block device without device numbers: {}", + path.display() + ); + return Ok(()); + }; + let mode = item + .metadata() + .permission() + .map(|p| p.permissions() as u32) + .unwrap_or(0o666); + if remove_existing { + utils::io::ignore_not_found(utils::fs::remove_path(&path))?; + } + if let Err(e) = utils::fs::mknod_block(&path, dev.major(), dev.minor(), mode) { + log::warn!( + "Failed to create block device {} (major={}, minor={}): {} (may require root)", + path.display(), + dev.major(), + dev.minor(), + e + ); + return Ok(()); + } + } + #[cfg(not(unix))] + { + log::warn!( + "Skipping block device (not supported on this platform): {}", + path.display() + ); + return Ok(()); + } + } + DataKind::CharDevice => { + #[cfg(unix)] + { + let Some(dev) = item.read_device_numbers(ReadOptions::with_password(password))? + else { + log::warn!( + "Skipping character device without device numbers: {}", + path.display() + ); + return Ok(()); + }; + let mode = item + .metadata() + .permission() + .map(|p| p.permissions() as u32) + .unwrap_or(0o666); + if remove_existing { + utils::io::ignore_not_found(utils::fs::remove_path(&path))?; + } + if let Err(e) = utils::fs::mknod_char(&path, dev.major(), dev.minor(), mode) { + log::warn!( + "Failed to create character device {} (major={}, minor={}): {} (may require root)", + path.display(), + dev.major(), + dev.minor(), + e + ); + return Ok(()); + } + } + #[cfg(not(unix))] + { + log::warn!( + "Skipping character device (not supported on this platform): {}", + path.display() + ); + return Ok(()); + } + } + DataKind::Fifo => { + #[cfg(unix)] + { + let mode = item + .metadata() + .permission() + .map(|p| p.permissions() as u32) + .unwrap_or(0o666); + if remove_existing { + utils::io::ignore_not_found(utils::fs::remove_path(&path))?; + } + if let Err(e) = utils::fs::mkfifo(&path, mode) { + log::warn!("Failed to create FIFO {}: {}", path.display(), e); + return Ok(()); + } + } + #[cfg(not(unix))] + { + log::warn!( + "Skipping FIFO (not supported on this platform): {}", + path.display() + ); + return Ok(()); + } + } } restore_metadata(&item, &path, keep_options, owner_options, same_owner)?; drop(path_guard); diff --git a/cli/src/command/list.rs b/cli/src/command/list.rs index 65aeed204..2ef1222e0 100644 --- a/cli/src/command/list.rs +++ b/cli/src/command/list.rs @@ -178,6 +178,9 @@ enum EntryType { Directory(String), SymbolicLink(String, String), HardLink(String, String), + BlockDevice(String), + CharDevice(String), + Fifo(String), } impl EntryType { @@ -187,7 +190,10 @@ impl EntryType { EntryType::File(name) | EntryType::Directory(name) | EntryType::SymbolicLink(name, _) - | EntryType::HardLink(name, _) => name, + | EntryType::HardLink(name, _) + | EntryType::BlockDevice(name) + | EntryType::CharDevice(name) + | EntryType::Fifo(name) => name, } } @@ -211,6 +217,9 @@ impl Display for EntryTypeBsdLongStyleDisplay<'_> { EntryType::HardLink(name, link_to) => { write!(f, "{name} link to {link_to}") } + EntryType::BlockDevice(name) => write!(f, "{name} (block device)"), + EntryType::CharDevice(name) => write!(f, "{name} (char device)"), + EntryType::Fifo(name) => write!(f, "{name} (fifo)"), } } } @@ -292,6 +301,9 @@ where ), DataKind::Directory => EntryType::Directory(entry.name().to_string()), DataKind::File => EntryType::File(entry.name().to_string()), + DataKind::BlockDevice => EntryType::BlockDevice(entry.name().to_string()), + DataKind::CharDevice => EntryType::CharDevice(entry.name().to_string()), + DataKind::Fifo => EntryType::Fifo(entry.name().to_string()), }, xattrs: entry.xattrs().to_vec(), acl, @@ -663,7 +675,11 @@ fn detail_list_entries_to( EntryType::SymbolicLink(name, link_to) if options.classify => { format!("{name}@ -> {link_to}") } - EntryType::File(path) | EntryType::Directory(path) => path, + EntryType::File(path) + | EntryType::Directory(path) + | EntryType::BlockDevice(path) + | EntryType::CharDevice(path) + | EntryType::Fifo(path) => path, EntryType::SymbolicLink(path, link_to) | EntryType::HardLink(path, link_to) => { format!("{path} -> {link_to}") } @@ -815,6 +831,9 @@ fn kind_paint(kind: &EntryType) -> impl Display + 'static { EntryType::File(_) | EntryType::HardLink(_, _) => STYLE_HYPHEN.paint('.'), EntryType::Directory(_) => STYLE_DIR.paint('d'), EntryType::SymbolicLink(_, _) => STYLE_LINK.paint('l'), + EntryType::BlockDevice(_) => STYLE_HYPHEN.paint('b'), + EntryType::CharDevice(_) => STYLE_HYPHEN.paint('c'), + EntryType::Fifo(_) => STYLE_HYPHEN.paint('p'), } } @@ -854,6 +873,9 @@ const fn kind_char(kind: &EntryType) -> char { EntryType::File(_) | EntryType::HardLink(_, _) => '-', EntryType::Directory(_) => 'd', EntryType::SymbolicLink(_, _) => 'l', + EntryType::BlockDevice(_) => 'b', + EntryType::CharDevice(_) => 'c', + EntryType::Fifo(_) => 'p', } } @@ -1081,6 +1103,9 @@ fn tree_entries_to( EntryType::Directory(name) => (name.as_str(), DataKind::Directory), EntryType::SymbolicLink(name, _) => (name.as_str(), DataKind::SymbolicLink), EntryType::HardLink(name, _) => (name.as_str(), DataKind::HardLink), + EntryType::BlockDevice(name) => (name.as_str(), DataKind::BlockDevice), + EntryType::CharDevice(name) => (name.as_str(), DataKind::CharDevice), + EntryType::Fifo(name) => (name.as_str(), DataKind::Fifo), }); let map = build_tree_map(entries); let tree = build_term_tree(&map, Cow::Borrowed(""), None, DataKind::Directory, options); diff --git a/cli/src/utils/fs.rs b/cli/src/utils/fs.rs index 766b56c1c..4ef0fd56c 100644 --- a/cli/src/utils/fs.rs +++ b/cli/src/utils/fs.rs @@ -74,3 +74,64 @@ pub(crate) fn file_create(path: impl AsRef, overwrite: bool) -> io::Result fs::File::create_new(path) } } + +/// Decodes a raw device number (rdev) into major and minor device numbers. +/// +/// On Linux, the rdev field uses a more complex encoding for devices with +/// large numbers, but for most common devices the simple encoding suffices. +#[cfg(unix)] +#[inline] +pub(crate) fn decode_rdev(rdev: u64) -> (u32, u32) { + // Use libc's major/minor macros for platform-correct decoding + let major = libc::major(rdev) as u32; + let minor = libc::minor(rdev) as u32; + (major, minor) +} + +/// Encodes major and minor device numbers into a raw device number (rdev). +#[cfg(unix)] +#[inline] +pub(crate) fn encode_rdev(major: u32, minor: u32) -> u64 { + libc::makedev(major, minor) +} + +/// Creates a block device node at the specified path. +/// +/// Requires root privileges on most systems. +#[cfg(unix)] +pub(crate) fn mknod_block( + path: impl AsRef, + major: u32, + minor: u32, + mode: u32, +) -> io::Result<()> { + use nix::sys::stat::{self, SFlag}; + let dev = encode_rdev(major, minor); + let mode = stat::Mode::from_bits_truncate(mode); + stat::mknod(path.as_ref(), SFlag::S_IFBLK, mode, dev).map_err(io::Error::other) +} + +/// Creates a character device node at the specified path. +/// +/// Requires root privileges on most systems. +#[cfg(unix)] +pub(crate) fn mknod_char( + path: impl AsRef, + major: u32, + minor: u32, + mode: u32, +) -> io::Result<()> { + use nix::sys::stat::{self, SFlag}; + let dev = encode_rdev(major, minor); + let mode = stat::Mode::from_bits_truncate(mode); + stat::mknod(path.as_ref(), SFlag::S_IFCHR, mode, dev).map_err(io::Error::other) +} + +/// Creates a FIFO (named pipe) at the specified path. +#[cfg(unix)] +pub(crate) fn mkfifo(path: impl AsRef, mode: u32) -> io::Result<()> { + use nix::sys::stat::Mode; + use nix::unistd; + let mode = Mode::from_bits_truncate(mode); + unistd::mkfifo(path.as_ref(), mode).map_err(io::Error::other) +} diff --git a/cli/tests/cli/device_files.rs b/cli/tests/cli/device_files.rs new file mode 100644 index 000000000..806c6c952 --- /dev/null +++ b/cli/tests/cli/device_files.rs @@ -0,0 +1,237 @@ +//! Integration tests for device file support (block devices, character devices, FIFOs). +//! +//! These tests verify that device files can be archived and extracted correctly. +//! Note: Creating block/character devices requires root privileges and may be +//! restricted in containerized environments. + +use crate::utils::setup; +use clap::Parser; +use portable_network_archive::cli; +use std::{fs, path::Path}; + +/// Creates a FIFO in the specified directory. +/// Returns true if successful, false otherwise. +#[cfg(unix)] +fn create_fifo(path: &Path) -> bool { + use nix::sys::stat::Mode; + use nix::unistd; + + if path.exists() { + let _ = fs::remove_file(path); + } + unistd::mkfifo(path, Mode::from_bits_truncate(0o644)).is_ok() +} + +/// Creates a character device in the specified directory. +/// Returns true if successful, false otherwise (e.g., not root or restricted environment). +#[cfg(unix)] +fn create_char_device(path: &Path, major: u32, minor: u32) -> bool { + use nix::sys::stat::{self, Mode, SFlag}; + + if path.exists() { + let _ = fs::remove_file(path); + } + let dev = libc::makedev(major, minor); + stat::mknod(path, SFlag::S_IFCHR, Mode::from_bits_truncate(0o666), dev).is_ok() +} + +/// Creates a block device in the specified directory. +/// Returns true if successful, false otherwise (e.g., not root or restricted environment). +#[cfg(unix)] +fn create_block_device(path: &Path, major: u32, minor: u32) -> bool { + use nix::sys::stat::{self, Mode, SFlag}; + + if path.exists() { + let _ = fs::remove_file(path); + } + let dev = libc::makedev(major, minor); + stat::mknod(path, SFlag::S_IFBLK, Mode::from_bits_truncate(0o660), dev).is_ok() +} + +/// Precondition: A FIFO is created in the input directory. +/// Action: Archive the FIFO and extract it. +/// Expectation: The extracted file is a FIFO. +#[test] +#[cfg(unix)] +fn fifo_roundtrip() { + use std::os::unix::fs::FileTypeExt; + + setup(); + + let base_path = Path::new("device_files_fifo"); + let in_path = base_path.join("in"); + let archive_path = base_path.join("archive.pna"); + let out_path = base_path.join("out"); + + // Clean up from previous runs + if base_path.exists() { + fs::remove_dir_all(base_path).unwrap(); + } + fs::create_dir_all(&in_path).unwrap(); + + // Create test FIFO + let fifo_path = in_path.join("test_fifo"); + if !create_fifo(&fifo_path) { + eprintln!("Skipping fifo_roundtrip: cannot create FIFO"); + return; + } + + // Create archive + cli::Cli::try_parse_from([ + "pna", + "--quiet", + "create", + "--file", + &archive_path.to_string_lossy(), + "--overwrite", + "--keep-permission", + &in_path.to_string_lossy(), + ]) + .unwrap() + .execute() + .unwrap(); + + assert!(archive_path.exists()); + + // Extract archive + cli::Cli::try_parse_from([ + "pna", + "--quiet", + "extract", + "--file", + &archive_path.to_string_lossy(), + "--overwrite", + "--out-dir", + &out_path.to_string_lossy(), + "--keep-permission", + "--strip-components", + "1", + ]) + .unwrap() + .execute() + .unwrap(); + + // Verify FIFO was extracted correctly + let extracted_fifo = out_path.join("in/test_fifo"); + assert!( + extracted_fifo.exists(), + "FIFO should exist after extraction" + ); + + let metadata = fs::symlink_metadata(&extracted_fifo).unwrap(); + assert!( + metadata.file_type().is_fifo(), + "Extracted file should be a FIFO" + ); +} + +/// Precondition: Block and character devices are created (requires root and mknod capability). +/// Action: Archive the devices and extract them. +/// Expectation: The extracted files are devices with correct major/minor numbers. +#[test] +#[cfg(unix)] +fn device_nodes_roundtrip() { + use std::os::unix::fs::{FileTypeExt, MetadataExt}; + + setup(); + + let base_path = Path::new("device_files_nodes"); + let in_path = base_path.join("in"); + let archive_path = base_path.join("archive.pna"); + let out_path = base_path.join("out"); + + // Clean up from previous runs + if base_path.exists() { + fs::remove_dir_all(base_path).unwrap(); + } + fs::create_dir_all(&in_path).unwrap(); + + // Try to create test devices (may fail in containers) + let char_path = in_path.join("test_char"); + let block_path = in_path.join("test_block"); + + let char_created = create_char_device(&char_path, 1, 3); + let block_created = create_block_device(&block_path, 7, 0); + + if !char_created && !block_created { + eprintln!( + "Skipping device_nodes_roundtrip: cannot create device nodes (requires root and mknod capability)" + ); + return; + } + + // Create archive + cli::Cli::try_parse_from([ + "pna", + "--quiet", + "create", + "--file", + &archive_path.to_string_lossy(), + "--overwrite", + "--keep-permission", + &in_path.to_string_lossy(), + ]) + .unwrap() + .execute() + .unwrap(); + + assert!(archive_path.exists()); + + // Extract archive + cli::Cli::try_parse_from([ + "pna", + "--quiet", + "extract", + "--file", + &archive_path.to_string_lossy(), + "--overwrite", + "--out-dir", + &out_path.to_string_lossy(), + "--keep-permission", + "--strip-components", + "1", + ]) + .unwrap() + .execute() + .unwrap(); + + // Verify character device (if it was created) + if char_created { + let extracted_char = out_path.join("in/test_char"); + assert!( + extracted_char.exists(), + "Character device should exist after extraction" + ); + let metadata = fs::symlink_metadata(&extracted_char).unwrap(); + assert!( + metadata.file_type().is_char_device(), + "Extracted file should be a character device" + ); + // Verify major/minor (1, 3 for /dev/null-like device) + let rdev = metadata.rdev(); + let major = libc::major(rdev); + let minor = libc::minor(rdev); + assert_eq!(major, 1, "Character device major number should be 1"); + assert_eq!(minor, 3, "Character device minor number should be 3"); + } + + // Verify block device (if it was created) + if block_created { + let extracted_block = out_path.join("in/test_block"); + assert!( + extracted_block.exists(), + "Block device should exist after extraction" + ); + let metadata = fs::symlink_metadata(&extracted_block).unwrap(); + assert!( + metadata.file_type().is_block_device(), + "Extracted file should be a block device" + ); + // Verify major/minor (7, 0 for loop device) + let rdev = metadata.rdev(); + let major = libc::major(rdev); + let minor = libc::minor(rdev); + assert_eq!(major, 7, "Block device major number should be 7"); + assert_eq!(minor, 0, "Block device minor number should be 0"); + } +} diff --git a/cli/tests/cli/main.rs b/cli/tests/cli/main.rs index 78174caa8..27ca46c19 100644 --- a/cli/tests/cli/main.rs +++ b/cli/tests/cli/main.rs @@ -10,6 +10,8 @@ mod combination; mod concat; mod create; mod delete; +#[cfg(unix)] +mod device_files; #[cfg(not(target_family = "wasm"))] mod diff; mod encrypt; diff --git a/lib/src/entry.rs b/lib/src/entry.rs index 5e5c8e844..8561c9595 100644 --- a/lib/src/entry.rs +++ b/lib/src/entry.rs @@ -1004,6 +1004,58 @@ impl> NormalEntry { let reader = decompress_reader(decrypt_reader, self.header.compression)?; Ok(EntryDataReader(EntryReader(reader))) } + + /// Reads device numbers for block/character device entries. + /// + /// Returns `Some` for [`DataKind::BlockDevice`] and [`DataKind::CharDevice`] entries, + /// `None` for other entry types. + /// + /// Device numbers are stored in the entry's data (FDAT) as 8 bytes: + /// - 4 bytes: major device number (big-endian) + /// - 4 bytes: minor device number (big-endian) + /// + /// # Examples + /// + /// ```no_run + /// use libpna::{Archive, DataKind, ReadOptions}; + /// use std::{fs, io}; + /// + /// # fn main() -> io::Result<()> { + /// let file = fs::File::open("foo.pna")?; + /// let mut archive = Archive::read_header(file)?; + /// for entry in archive.entries().skip_solid() { + /// let entry = entry?; + /// match entry.data_kind() { + /// DataKind::BlockDevice | DataKind::CharDevice => { + /// if let Some(dev) = entry.read_device_numbers(ReadOptions::builder().build())? { + /// println!("Device: {}:{}", dev.major(), dev.minor()); + /// } + /// } + /// _ => {} + /// } + /// } + /// # Ok(()) + /// # } + /// ``` + /// + /// # Errors + /// + /// Returns an error if an I/O error occurs while reading the device numbers. + #[inline] + pub fn read_device_numbers( + &self, + option: impl ReadOption, + ) -> io::Result> { + match self.header.data_kind { + DataKind::BlockDevice | DataKind::CharDevice => { + let mut reader = self.reader(option)?; + let mut buf = [0u8; 8]; + reader.read_exact(&mut buf)?; + Ok(Some(DeviceNumbers::try_from_bytes(&buf)?)) + } + _ => Ok(None), + } + } } impl<'a> From>> for NormalEntry> { diff --git a/lib/src/entry/builder.rs b/lib/src/entry/builder.rs index c5ef30500..8fbcd9e5b 100644 --- a/lib/src/entry/builder.rs +++ b/lib/src/entry/builder.rs @@ -5,9 +5,9 @@ use crate::{ cipher::CipherWriter, compress::CompressionWriter, entry::{ - DataKind, Entry, EntryHeader, EntryName, EntryReference, ExtendedAttribute, Metadata, - NormalEntry, Permission, SolidEntry, SolidHeader, WriteCipher, WriteOption, WriteOptions, - get_writer, get_writer_context, private::SealedEntryExt, + DataKind, DeviceNumbers, Entry, EntryHeader, EntryName, EntryReference, ExtendedAttribute, + Metadata, NormalEntry, Permission, SolidEntry, SolidHeader, WriteCipher, WriteOption, + WriteOptions, get_writer, get_writer_context, private::SealedEntryExt, }, io::{FlattenWriter, TryIntoInner}, }; @@ -322,6 +322,120 @@ impl EntryBuilder { }) } + /// Creates a new block device entry with the given name and device numbers. + /// + /// # Arguments + /// + /// * `name` - The name of the entry to create. + /// * `major` - The major device number. + /// * `minor` - The minor device number. + /// + /// # Returns + /// + /// A new [EntryBuilder]. + /// + /// # Errors + /// + /// Returns an error if initialization fails. + /// + /// # Examples + /// ``` + /// use libpna::{EntryBuilder, EntryName}; + /// + /// // Create a block device entry (e.g., /dev/sda with major=8, minor=0) + /// let builder = EntryBuilder::new_block_device( + /// EntryName::try_from("dev/sda").unwrap(), + /// 8, + /// 0, + /// ).unwrap(); + /// let entry = builder.build().unwrap(); + /// ``` + #[inline] + pub fn new_block_device(name: EntryName, major: u32, minor: u32) -> io::Result { + let option = WriteOptions::store(); + let context = get_writer_context(option)?; + let mut writer = get_writer(FlattenWriter::new(), &context)?; + writer.write_all(&DeviceNumbers::new(major, minor).to_bytes())?; + let (iv, phsf) = match context.cipher { + None => (None, None), + Some(WriteCipher { context: c, .. }) => (Some(c.iv), Some(c.phsf)), + }; + Ok(Self { + data: Some(writer), + iv, + phsf, + ..Self::new(EntryHeader::for_block_device(name)) + }) + } + + /// Creates a new character device entry with the given name and device numbers. + /// + /// # Arguments + /// + /// * `name` - The name of the entry to create. + /// * `major` - The major device number. + /// * `minor` - The minor device number. + /// + /// # Returns + /// + /// A new [EntryBuilder]. + /// + /// # Errors + /// + /// Returns an error if initialization fails. + /// + /// # Examples + /// ``` + /// use libpna::{EntryBuilder, EntryName}; + /// + /// // Create a character device entry (e.g., /dev/null with major=1, minor=3) + /// let builder = EntryBuilder::new_char_device( + /// EntryName::try_from("dev/null").unwrap(), + /// 1, + /// 3, + /// ).unwrap(); + /// let entry = builder.build().unwrap(); + /// ``` + #[inline] + pub fn new_char_device(name: EntryName, major: u32, minor: u32) -> io::Result { + let option = WriteOptions::store(); + let context = get_writer_context(option)?; + let mut writer = get_writer(FlattenWriter::new(), &context)?; + writer.write_all(&DeviceNumbers::new(major, minor).to_bytes())?; + let (iv, phsf) = match context.cipher { + None => (None, None), + Some(WriteCipher { context: c, .. }) => (Some(c.iv), Some(c.phsf)), + }; + Ok(Self { + data: Some(writer), + iv, + phsf, + ..Self::new(EntryHeader::for_char_device(name)) + }) + } + + /// Creates a new FIFO (named pipe) entry with the given name. + /// + /// # Arguments + /// + /// * `name` - The name of the entry to create. + /// + /// # Returns + /// + /// A new [EntryBuilder]. + /// + /// # Examples + /// ``` + /// use libpna::{EntryBuilder, EntryName}; + /// + /// let builder = EntryBuilder::new_fifo(EntryName::try_from("path/to/pipe").unwrap()); + /// let entry = builder.build().unwrap(); + /// ``` + #[inline] + pub const fn new_fifo(name: EntryName) -> Self { + Self::new(EntryHeader::for_fifo(name)) + } + /// Sets the creation timestamp of the entry. /// /// # Arguments diff --git a/lib/src/entry/header.rs b/lib/src/entry/header.rs index 883cf686d..4176e83d4 100644 --- a/lib/src/entry/header.rs +++ b/lib/src/entry/header.rs @@ -75,6 +75,24 @@ impl EntryHeader { Self::new(DataKind::HardLink, path) } + /// Creates a header for a block device. + #[inline] + pub(crate) const fn for_block_device(path: EntryName) -> Self { + Self::new(DataKind::BlockDevice, path) + } + + /// Creates a header for a character device. + #[inline] + pub(crate) const fn for_char_device(path: EntryName) -> Self { + Self::new(DataKind::CharDevice, path) + } + + /// Creates a header for a FIFO (named pipe). + #[inline] + pub(crate) const fn for_fifo(path: EntryName) -> Self { + Self::new(DataKind::Fifo, path) + } + /// Path of the entry that sanitized to remove path traversal characters by [`EntryName::sanitize`]. #[inline] pub fn path(&self) -> &EntryName { diff --git a/lib/src/entry/meta.rs b/lib/src/entry/meta.rs index c19898ba7..bab21675f 100644 --- a/lib/src/entry/meta.rs +++ b/lib/src/entry/meta.rs @@ -316,6 +316,112 @@ impl Permission { } } +/// Device numbers for block and character device entries. +/// +/// This stores the major and minor device numbers that identify the device type +/// and specific device instance on Unix-like systems. +/// +/// # Examples +/// +/// ```rust +/// use libpna::DeviceNumbers; +/// +/// // Create device numbers for /dev/null (typically major=1, minor=3 on Linux) +/// let dev = DeviceNumbers::new(1, 3); +/// assert_eq!(dev.major(), 1); +/// assert_eq!(dev.minor(), 3); +/// ``` +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] +pub struct DeviceNumbers { + major: u32, + minor: u32, +} + +impl DeviceNumbers { + /// Creates new device numbers with the given major and minor values. + /// + /// # Arguments + /// + /// - `major`: The major device number (identifies the device driver) + /// - `minor`: The minor device number (identifies the specific device) + /// + /// # Examples + /// + /// ```rust + /// use libpna::DeviceNumbers; + /// + /// let dev = DeviceNumbers::new(8, 0); // Typical SCSI disk + /// ``` + #[inline] + pub const fn new(major: u32, minor: u32) -> Self { + Self { major, minor } + } + + /// Returns the major device number. + /// + /// The major number identifies the device driver responsible for the device. + /// + /// # Examples + /// + /// ```rust + /// use libpna::DeviceNumbers; + /// + /// let dev = DeviceNumbers::new(8, 0); + /// assert_eq!(dev.major(), 8); + /// ``` + #[inline] + pub const fn major(&self) -> u32 { + self.major + } + + /// Returns the minor device number. + /// + /// The minor number identifies the specific device managed by the driver. + /// + /// # Examples + /// + /// ```rust + /// use libpna::DeviceNumbers; + /// + /// let dev = DeviceNumbers::new(8, 1); + /// assert_eq!(dev.minor(), 1); + /// ``` + #[inline] + pub const fn minor(&self) -> u32 { + self.minor + } + + /// Serializes the device numbers to bytes. + /// + /// Format: `[major: 4 bytes BE][minor: 4 bytes BE]` + #[inline] + pub(crate) fn to_bytes(self) -> [u8; 8] { + let mut bytes = [0u8; 8]; + bytes[0..4].copy_from_slice(&self.major.to_be_bytes()); + bytes[4..8].copy_from_slice(&self.minor.to_be_bytes()); + bytes + } + + /// Deserializes device numbers from bytes. + /// + /// # Errors + /// + /// Returns an error if the byte slice is too short. + pub(crate) fn try_from_bytes(mut bytes: &[u8]) -> io::Result { + let major = u32::from_be_bytes({ + let mut buf = [0; 4]; + bytes.read_exact(&mut buf)?; + buf + }); + let minor = u32::from_be_bytes({ + let mut buf = [0; 4]; + bytes.read_exact(&mut buf)?; + buf + }); + Ok(Self { major, minor }) + } +} + #[cfg(test)] mod tests { use super::*; @@ -327,4 +433,10 @@ mod tests { let perm = Permission::new(1000, "user1".into(), 100, "group1".into(), 0o644); assert_eq!(perm, Permission::try_from_bytes(&perm.to_bytes()).unwrap()); } + + #[test] + fn device_numbers() { + let dev = DeviceNumbers::new(8, 1); + assert_eq!(dev, DeviceNumbers::try_from_bytes(&dev.to_bytes()).unwrap()); + } } diff --git a/lib/src/entry/options.rs b/lib/src/entry/options.rs index fe38c1817..67d606a4d 100644 --- a/lib/src/entry/options.rs +++ b/lib/src/entry/options.rs @@ -481,6 +481,14 @@ pub enum DataKind { /// Hard link. Entry data contains the UTF-8 encoded path of the target entry /// within the same archive. HardLink = 3, + /// Block device. Entry has no data content but stores device major/minor numbers + /// in a `dNUM` chunk. + BlockDevice = 4, + /// Character device. Entry has no data content but stores device major/minor numbers + /// in a `dNUM` chunk. + CharDevice = 5, + /// FIFO (named pipe). Entry has no data content. + Fifo = 6, } impl TryFrom for DataKind { @@ -493,6 +501,9 @@ impl TryFrom for DataKind { 1 => Ok(Self::Directory), 2 => Ok(Self::SymbolicLink), 3 => Ok(Self::HardLink), + 4 => Ok(Self::BlockDevice), + 5 => Ok(Self::CharDevice), + 6 => Ok(Self::Fifo), value => Err(UnknownValueError(value)), } }