Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 83 additions & 4 deletions cli/src/command/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
Expand Down
10 changes: 9 additions & 1 deletion cli/src/command/diff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ fn compare_entry<T: AsRef<[u8]>>(entry: NormalEntry<T>, 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(());
}
Expand Down Expand Up @@ -88,7 +91,12 @@ fn compare_entry<T: AsRef<[u8]>>(entry: NormalEntry<T>, 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 => (),
Expand Down
103 changes: 103 additions & 0 deletions cli/src/command/extract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(());
}
}
Comment on lines +693 to +770
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The logic for handling DataKind::BlockDevice and DataKind::CharDevice is nearly identical, leading to significant code duplication. This can be refactored into a helper function to improve maintainability. The helper function could take a boolean flag to distinguish between block and character devices and encapsulate the common logic for reading device numbers, checking permissions, and creating the device node.

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);
Expand Down
29 changes: 27 additions & 2 deletions cli/src/command/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,9 @@ enum EntryType {
Directory(String),
SymbolicLink(String, String),
HardLink(String, String),
BlockDevice(String),
CharDevice(String),
Fifo(String),
}

impl EntryType {
Expand All @@ -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,
}
}

Expand All @@ -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)"),
}
}
}
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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}")
}
Expand Down Expand Up @@ -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'),
}
}

Expand Down Expand Up @@ -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',
}
}

Expand Down Expand Up @@ -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);
Expand Down
61 changes: 61 additions & 0 deletions cli/src/utils/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,64 @@ pub(crate) fn file_create(path: impl AsRef<Path>, 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<Path>,
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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using io::Error::other to wrap nix::Error can obscure the original error kind. Since nix::Error implements From<nix::Error> for io::Error, you can use Into::into to perform an idiomatic conversion, which preserves more error information. This also applies to mknod_char and mkfifo.

Suggested change
stat::mknod(path.as_ref(), SFlag::S_IFBLK, mode, dev).map_err(io::Error::other)
stat::mknod(path.as_ref(), SFlag::S_IFBLK, mode, dev).map_err(Into::into)

}

/// 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<Path>,
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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using io::Error::other to wrap nix::Error can obscure the original error kind. Since nix::Error implements From<nix::Error> for io::Error, you can use Into::into to perform an idiomatic conversion, which preserves more error information.

Suggested change
stat::mknod(path.as_ref(), SFlag::S_IFCHR, mode, dev).map_err(io::Error::other)
stat::mknod(path.as_ref(), SFlag::S_IFCHR, mode, dev).map_err(Into::into)

}

/// Creates a FIFO (named pipe) at the specified path.
#[cfg(unix)]
pub(crate) fn mkfifo(path: impl AsRef<Path>, 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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using io::Error::other to wrap nix::Error can obscure the original error kind. Since nix::Error implements From<nix::Error> for io::Error, you can use Into::into to perform an idiomatic conversion, which preserves more error information.

Suggested change
unistd::mkfifo(path.as_ref(), mode).map_err(io::Error::other)
unistd::mkfifo(path.as_ref(), mode).map_err(Into::into)

}
Loading
Loading