diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index d721d57a..cbe4a8d5 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -14,6 +14,9 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 + - name: install dependencies + run: | + sudo apt install -y libacl1-dev - uses: ./.github/actions/setup-rust with: channel: nightly @@ -30,6 +33,9 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - uses: ./.github/actions/setup-rust + - name: install dependencies + run: | + sudo apt install -y libacl1-dev - name: Install cli run: | cargo install --locked --all-features --path cli diff --git a/.github/workflows/cli_test.yml b/.github/workflows/cli_test.yml index 78629eff..469757a9 100644 --- a/.github/workflows/cli_test.yml +++ b/.github/workflows/cli_test.yml @@ -15,6 +15,9 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - uses: ./.github/actions/setup-rust + - name: install dependencies + run: | + sudo apt install -y libacl1-dev - name: Install command run: | cargo install --path cli --all-features diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b6a0947f..aa64246b 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -18,6 +18,9 @@ jobs: with: submodules: true - uses: ./.github/actions/setup-rust + - name: install dependencies + run: | + sudo apt install -y libacl1-dev - if: startsWith(github.ref, 'refs/tags/libpna') name: Publish libpna crate run: cargo publish -p libpna diff --git a/.github/workflows/rust-clippy.yml b/.github/workflows/rust-clippy.yml index f73631c5..fb4d9c3b 100644 --- a/.github/workflows/rust-clippy.yml +++ b/.github/workflows/rust-clippy.yml @@ -26,6 +26,9 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - uses: ./.github/actions/setup-rust + - name: install dependencies + run: | + sudo apt install -y libacl1-dev - name: Install required cargo run: cargo install clippy-sarif sarif-fmt - name: Run rust-clippy diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dbbbbe9b..02c4686e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -23,7 +23,10 @@ jobs: run: | git config --global core.autocrlf false git config --global core.eol lf - + - name: install dependencies + if: startsWith(matrix.os, 'ubuntu') + run: | + sudo apt install -y libacl1-dev - uses: actions/checkout@v4 - id: install_rust uses: ./.github/actions/setup-rust @@ -33,7 +36,7 @@ jobs: uses: actions/cache@v4 with: path: target - key: "${{ matrix.os }}-rust-${{ steps.install_rust.outputs.version }}-${{ hashFiles('**/Cargo.lock') }}" + key: "${{ matrix.os }}-rust-${{ steps.install_rust.outputs.version }}-${{ hashFiles(format('{0}/Cargo.lock', env.WORKING_DIRECTORY)) }}" - name: run test run: cargo test --locked --release --all-features env: diff --git a/Cargo.lock b/Cargo.lock index 25f830c0..576f5d9c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -330,9 +330,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" [[package]] name = "blake2" @@ -702,6 +702,18 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "exacl" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22be12de19decddab85d09f251ec8363f060ccb22ec9c81bc157c0c8433946d8" +dependencies = [ + "bitflags 2.5.0", + "log", + "scopeguard", + "uuid", +] + [[package]] name = "fastrand" version = "1.9.0" @@ -717,6 +729,16 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + [[package]] name = "flate2" version = "1.0.30" @@ -1099,6 +1121,15 @@ version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -1120,7 +1151,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.5.0", "cfg-if", "cfg_aliases", "libc", @@ -1309,10 +1340,13 @@ name = "portable-network-archive" version = "0.11.1" dependencies = [ "ansi_term", + "bitflags 2.5.0", "bytesize", "chrono", "clap", "clap_complete", + "exacl", + "field-offset", "glob", "indicatif", "nix", @@ -1347,7 +1381,7 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.5.0", "memchr", "unicase", ] @@ -1467,6 +1501,15 @@ version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "0.37.27" @@ -1487,13 +1530,19 @@ version = "0.38.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.5.0", "errno", "libc", "linux-raw-sys 0.4.13", "windows-sys 0.52.0", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "semver" version = "1.0.23" @@ -1747,6 +1796,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "uuid" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" + [[package]] name = "value-bag" version = "1.7.0" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 0094007c..41432848 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -11,6 +11,7 @@ keywords = ["pna", "archive", "cli", "crypto", "data"] [dependencies] ansi_term = "0.12.1" +bitflags = "2.5.0" bytesize = "1.3.0" chrono = "0.4.38" clap = { version = "4.5.4", features = ["derive"] } @@ -28,12 +29,23 @@ normalize-path = "0.2.1" nix = { version = "0.29.0", features = ["user", "fs"] } xattr = "1.3.1" +[target.'cfg(any(target_os = "linux", target_os = "freebsd", target_os = "macos"))'.dependencies] +exacl = { version = "0.12.0", optional = true } + [target.'cfg(windows)'.dependencies] windows = { version = "0.56.0", features = ["Win32_Storage_FileSystem"] } +field-offset = { version = "0.3.6", optional = true } [features] experimental = [] # deprecated unstable-split = ["experimental"] # deprecated +acl = [ + "exacl", + "field-offset", + "windows/Win32_Security_Authorization", + "windows/Win32_System_SystemServices", + "windows/Win32_System_WindowsProgramming", +] [[bin]] name = "pna" diff --git a/cli/src/chunk.rs b/cli/src/chunk.rs new file mode 100644 index 00000000..4c90f92d --- /dev/null +++ b/cli/src/chunk.rs @@ -0,0 +1,3 @@ +mod acl; + +pub use acl::*; diff --git a/cli/src/chunk/acl.rs b/cli/src/chunk/acl.rs new file mode 100644 index 00000000..6b0c743d --- /dev/null +++ b/cli/src/chunk/acl.rs @@ -0,0 +1,669 @@ +use bitflags::bitflags; +use pna::ChunkType; +use std::{ + error::Error, + fmt::{self, Display, Formatter}, + str::FromStr, +}; + +/// [ChunkType] File Access Control Entry +#[allow(non_upper_case_globals)] +pub const faCe: ChunkType = unsafe { ChunkType::from_unchecked(*b"faCe") }; + +#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] +pub enum AcePlatform { + General, + Windows, + MacOs, + Linux, + FreeBSD, + Unknown(String), +} + +impl AcePlatform { + #[cfg(windows)] + pub const CURRENT: Self = Self::Windows; + #[cfg(target_os = "macos")] + pub const CURRENT: Self = Self::MacOs; + #[cfg(target_os = "linux")] + pub const CURRENT: Self = Self::Linux; + #[cfg(target_os = "freebsd")] + pub const CURRENT: Self = Self::FreeBSD; + #[cfg(not(any( + target_os = "macos", + target_os = "linux", + target_os = "freebsd", + windows + )))] + pub const CURRENT: Self = Self::General; +} + +impl Display for AcePlatform { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Self::General => f.write_str(""), + Self::Windows => f.write_str("windows"), + Self::MacOs => f.write_str("macos"), + Self::Linux => f.write_str("linux"), + Self::FreeBSD => f.write_str("freebsd"), + Self::Unknown(s) => f.write_str(s), + } + } +} + +impl FromStr for AcePlatform { + type Err = core::convert::Infallible; + + fn from_str(s: &str) -> Result { + match s { + "" => Ok(Self::General), + "windows" => Ok(Self::Windows), + "macos" => Ok(Self::MacOs), + "linux" => Ok(Self::Linux), + "freebsd" => Ok(Self::FreeBSD), + s => Ok(Self::Unknown(s.to_string())), + } + } +} + +#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] +pub struct Identifier(pub(crate) String); + +impl Display for Identifier { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.write_str(&self.0) + } +} + +#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] +pub enum OwnerType { + Owner, + User(Identifier), + OwnerGroup, + Group(Identifier), + Mask, + Other, +} + +impl Display for OwnerType { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match &self { + OwnerType::Owner => f.write_str("u:"), + OwnerType::User(i) => write!(f, "u:{}", i), + OwnerType::OwnerGroup => f.write_str("g:"), + OwnerType::Group(i) => write!(f, "g:{}", i), + OwnerType::Mask => f.write_str("m:"), + OwnerType::Other => f.write_str("o:"), + } + } +} + +/// An error which can be returned when parsing an integer. +#[derive(Clone, Eq, PartialEq, Debug)] +pub enum ParseAceError { + NotEnoughElement, + TooManyElement, + UnexpectedAccessControl(String), + UnexpectedOwnerType(String), +} + +impl Display for ParseAceError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +impl Error for ParseAceError {} + +/// Access Control Entry +#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] +pub struct Ace { + pub(crate) platform: AcePlatform, + pub(crate) flags: Flag, + pub(crate) owner_type: OwnerType, + pub(crate) allow: bool, + pub(crate) permission: Permission, +} + +impl Ace { + #[cfg(feature = "acl")] + pub(crate) fn to_bytes(&self) -> Vec { + self.to_string().into_bytes() + } +} + +impl Display for Ace { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let mut flags = Vec::new(); + if self.flags.contains(Flag::DEFAULT) { + flags.push("d"); + } + if self.flags.contains(Flag::FILE_INHERIT) { + flags.push("file_inherit"); + } + if self.flags.contains(Flag::DIRECTORY_INHERIT) { + flags.push("directory_inherit"); + } + if self.flags.contains(Flag::LIMIT_INHERIT) { + flags.push("limit_inherit"); + } + if self.flags.contains(Flag::ONLY_INHERIT) { + flags.push("only_inherit"); + } + if self.flags.contains(Flag::INHERITED) { + flags.push("inherited"); + } + + let mut permission_list = Vec::new(); + if self.permission.contains(Permission::READ) { + permission_list.push("r"); + } + if self.permission.contains(Permission::WRITE) { + permission_list.push("w"); + } + if self.permission.contains(Permission::EXECUTE) { + permission_list.push("x"); + } + if self.permission.contains(Permission::DELETE) { + permission_list.push("delete"); + } + if self.permission.contains(Permission::APPEND) { + permission_list.push("append"); + } + if self.permission.contains(Permission::DELETE_CHILD) { + permission_list.push("delete_child"); + } + if self.permission.contains(Permission::READATTR) { + permission_list.push("readattr"); + } + if self.permission.contains(Permission::WRITEATTR) { + permission_list.push("writeattr"); + } + if self.permission.contains(Permission::READEXTATTR) { + permission_list.push("readextattr"); + } + if self.permission.contains(Permission::WRITEEXTATTR) { + permission_list.push("writeextattr"); + } + if self.permission.contains(Permission::READSECURITY) { + permission_list.push("readsecurity"); + } + if self.permission.contains(Permission::WRITESECURITY) { + permission_list.push("writesecurity"); + } + if self.permission.contains(Permission::CHOWN) { + permission_list.push("chown"); + } + if self.permission.contains(Permission::SYNC) { + permission_list.push("sync"); + } + if self.permission.contains(Permission::READ_DATA) { + permission_list.push("read_data"); + } + if self.permission.contains(Permission::WRITE_DATA) { + permission_list.push("write_data"); + } + + write!( + f, + "{}:{}:{}:{}:{}", + self.platform, + flags.join(","), + self.owner_type, + if self.allow { "allow" } else { "deny" }, + permission_list.join(","), + ) + } +} + +impl FromStr for Ace { + type Err = ParseAceError; + + fn from_str(s: &str) -> Result { + let mut it = s.split(':'); + let platform = AcePlatform::from_str(it.next().ok_or(ParseAceError::NotEnoughElement)?) + .expect("Infallible error occurred"); + let flag_list = it + .next() + .ok_or(ParseAceError::NotEnoughElement)? + .split(',') + .collect::>(); + let mut flags = Flag::empty(); + if flag_list.contains(&"d") || flag_list.contains(&"default") { + flags.insert(Flag::DEFAULT); + } + if flag_list.contains(&"file_inherit") { + flags.insert(Flag::FILE_INHERIT); + } + if flag_list.contains(&"directory_inherit") { + flags.insert(Flag::DIRECTORY_INHERIT); + } + if flag_list.contains(&"limit_inherit") { + flags.insert(Flag::LIMIT_INHERIT); + } + if flag_list.contains(&"only_inherit") { + flags.insert(Flag::ONLY_INHERIT); + } + if flag_list.contains(&"inherited") { + flags.insert(Flag::INHERITED); + } + let owner_type = it.next().ok_or(ParseAceError::NotEnoughElement)?; + let owner_name = it.next().ok_or(ParseAceError::NotEnoughElement)?; + let owner = match owner_type { + "u" | "user" => match owner_name { + "" => OwnerType::Owner, + name => OwnerType::User(Identifier(name.to_string())), + }, + "g" | "group" => match owner_name { + "" => OwnerType::OwnerGroup, + name => OwnerType::Group(Identifier(name.to_string())), + }, + "m" | "mask" => OwnerType::Mask, + "o" | "other" => OwnerType::Other, + o => return Err(Self::Err::UnexpectedOwnerType(o.to_string())), + }; + let allow = match it.next().ok_or(Self::Err::NotEnoughElement)? { + "allow" => true, + "deny" => false, + a => return Err(Self::Err::UnexpectedAccessControl(a.to_string())), + }; + let permissions = it + .next() + .ok_or(ParseAceError::NotEnoughElement)? + .split(',') + .collect::>(); + let mut permission = Permission::empty(); + if permissions.contains(&"r") || permissions.contains(&"read") { + permission.insert(Permission::READ); + } + if permissions.contains(&"w") || permissions.contains(&"write") { + permission.insert(Permission::WRITE); + } + if permissions.contains(&"x") || permissions.contains(&"execute") { + permission.insert(Permission::EXECUTE); + } + if permissions.contains(&"delete") { + permission.insert(Permission::DELETE); + } + if permissions.contains(&"append") { + permission.insert(Permission::APPEND); + } + if permissions.contains(&"delete_child") { + permission.insert(Permission::DELETE_CHILD); + } + if permissions.contains(&"readattr") { + permission.insert(Permission::READATTR); + } + if permissions.contains(&"writeattr") { + permission.insert(Permission::WRITEATTR); + } + if permissions.contains(&"readextattr") { + permission.insert(Permission::READEXTATTR); + } + if permissions.contains(&"writeextattr") { + permission.insert(Permission::WRITEEXTATTR); + } + if permissions.contains(&"readsecurity") { + permission.insert(Permission::READSECURITY); + } + if permissions.contains(&"writesecurity") { + permission.insert(Permission::WRITESECURITY); + } + if permissions.contains(&"chown") { + permission.insert(Permission::CHOWN); + } + if permissions.contains(&"sync") { + permission.insert(Permission::SYNC); + } + if permissions.contains(&"read_data") { + permission.insert(Permission::READ_DATA); + } + if permissions.contains(&"write_data") { + permission.insert(Permission::WRITE_DATA); + } + + if it.next().is_some() { + return Err(Self::Err::TooManyElement); + } + Ok(Self { + platform, + flags, + owner_type: owner, + allow, + permission, + }) + } +} + +#[allow(dead_code)] +pub fn ace_convert_current_platform(src: Ace) -> Ace { + ace_convert_platform(src, AcePlatform::CURRENT) +} + +pub fn ace_convert_platform(src: Ace, to: AcePlatform) -> Ace { + match &to { + AcePlatform::General | AcePlatform::Unknown(_) => ace_to_generic(src), + AcePlatform::Windows => ace_to_windows(src), + AcePlatform::MacOs => ace_to_macos(src), + AcePlatform::Linux => ace_to_linux(src), + AcePlatform::FreeBSD => ace_to_freebsd(src), + } +} + +const TO_GENERAL_PERMISSION_TABLE: [(&[Permission], Permission); 3] = [ + ( + &[ + Permission::READ, + Permission::READ_DATA, + Permission::READATTR, + Permission::READEXTATTR, + Permission::READSECURITY, + ], + Permission::READ, + ), + ( + &[ + Permission::WRITE, + Permission::WRITE_DATA, + Permission::WRITEATTR, + Permission::WRITEEXTATTR, + Permission::WRITESECURITY, + Permission::APPEND, + Permission::DELETE, + ], + Permission::WRITE, + ), + (&[Permission::EXECUTE], Permission::EXECUTE), +]; + +#[inline] +fn to_general_permission(src_permission: Permission) -> Permission { + let mut permission = Permission::empty(); + for (platform_permissions, generic_permission) in TO_GENERAL_PERMISSION_TABLE { + if platform_permissions + .iter() + .any(|it| src_permission.contains(*it)) + { + permission.insert(generic_permission); + } + } + permission +} + +fn ace_to_generic(src: Ace) -> Ace { + match src.platform { + AcePlatform::General => src, + AcePlatform::Windows => Ace { + platform: AcePlatform::General, + flags: Flag::empty(), + owner_type: src.owner_type, + allow: src.allow, + permission: to_general_permission(src.permission), + }, + AcePlatform::MacOs => Ace { + platform: AcePlatform::General, + flags: src.flags & { + let mut macos_flags = Flag::all(); + macos_flags.remove(Flag::DEFAULT); + macos_flags + }, + owner_type: src.owner_type, + allow: src.allow, + permission: to_general_permission(src.permission), + }, + AcePlatform::Linux => Ace { + platform: AcePlatform::Linux, + flags: src.flags & Flag::DEFAULT, + owner_type: src.owner_type, + allow: src.allow, + permission: to_general_permission(src.permission), + }, + AcePlatform::FreeBSD => Ace { + platform: AcePlatform::General, + flags: src.flags, + owner_type: src.owner_type, + allow: src.allow, + permission: to_general_permission(src.permission), + }, + AcePlatform::Unknown(_) => Ace { + platform: AcePlatform::General, + flags: Flag::empty(), + owner_type: src.owner_type, + allow: src.allow, + permission: to_general_permission(src.permission), + }, + } +} + +#[inline] +fn mapping_permission( + src_permission: Permission, + table: &[(&[Permission], Permission)], +) -> Permission { + let mut permission = Permission::empty(); + for (to, from_) in table { + if src_permission.contains(*from_) { + for p in *to { + permission.insert(*p); + } + } + } + permission +} + +const GENERIC_TO_WINDOWS_PERMISSION_TABLE: [(&[Permission], Permission); 3] = [ + ( + &[ + Permission::READ, + Permission::READ_DATA, + Permission::READATTR, + Permission::READEXTATTR, + Permission::READSECURITY, + Permission::READATTR, + Permission::SYNC, + ], + Permission::READ, + ), + ( + &[ + Permission::WRITE, + Permission::WRITE_DATA, + Permission::WRITEATTR, + Permission::WRITEEXTATTR, + Permission::WRITESECURITY, + Permission::APPEND, + Permission::DELETE, + Permission::READATTR, + Permission::SYNC, + ], + Permission::WRITE, + ), + ( + &[Permission::EXECUTE, Permission::READATTR, Permission::SYNC], + Permission::EXECUTE, + ), +]; + +fn ace_to_windows(src: Ace) -> Ace { + match src.platform { + AcePlatform::Windows => src, + AcePlatform::General + | AcePlatform::MacOs + | AcePlatform::Linux + | AcePlatform::FreeBSD + | AcePlatform::Unknown(_) => { + let src = ace_to_generic(src); + Ace { + platform: AcePlatform::Windows, + flags: src.flags, + owner_type: src.owner_type, + allow: src.allow, + permission: mapping_permission( + src.permission, + &GENERIC_TO_WINDOWS_PERMISSION_TABLE, + ), + } + } + } +} + +fn ace_to_linux(src: Ace) -> Ace { + match src.platform { + AcePlatform::Linux => src, + AcePlatform::General + | AcePlatform::Windows + | AcePlatform::MacOs + | AcePlatform::FreeBSD + | AcePlatform::Unknown(_) => { + let mut src = ace_to_generic(src); + src.platform = AcePlatform::Linux; + src + } + } +} + +const GENERIC_TO_MACOS_PERMISSION_TABLE: [(&[Permission], Permission); 3] = [ + ( + &[ + Permission::READ, + Permission::READ_DATA, + Permission::READATTR, + Permission::READEXTATTR, + Permission::READSECURITY, + ], + Permission::READ, + ), + ( + &[ + Permission::WRITE, + Permission::WRITE_DATA, + Permission::WRITEATTR, + Permission::WRITEEXTATTR, + Permission::WRITESECURITY, + Permission::APPEND, + Permission::DELETE, + ], + Permission::WRITE, + ), + (&[Permission::EXECUTE], Permission::EXECUTE), +]; + +fn ace_to_macos(src: Ace) -> Ace { + match src.platform { + AcePlatform::MacOs => src, + AcePlatform::General + | AcePlatform::Windows + | AcePlatform::Linux + | AcePlatform::FreeBSD + | AcePlatform::Unknown(_) => { + let src = ace_to_generic(src); + Ace { + platform: AcePlatform::MacOs, + flags: src.flags, + owner_type: src.owner_type, + allow: src.allow, + permission: mapping_permission(src.permission, &GENERIC_TO_MACOS_PERMISSION_TABLE), + } + } + } +} + +fn ace_to_freebsd(src: Ace) -> Ace { + match src.platform { + AcePlatform::FreeBSD => src, + AcePlatform::General + | AcePlatform::Windows + | AcePlatform::MacOs + | AcePlatform::Linux + | AcePlatform::Unknown(_) => { + let mut src = ace_to_generic(src); + src.platform = AcePlatform::FreeBSD; + src + } + } +} + +bitflags! { + #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] + pub struct Permission: u16 { + /// READ_DATA permission for a file. + /// Same as LIST_DIRECTORY permission for a directory. + const READ = 0b001; + + /// WRITE_DATA permission for a file. + /// Same as ADD_FILE permission for a directory. + const WRITE = 0b010; + + /// EXECUTE permission for a file. + /// Same as SEARCH permission for a directory. + const EXECUTE = 0b100; + + /// DELETE permission for a file. + const DELETE = 0b1000; + + /// APPEND_DATA permission for a file. + /// Same as ADD_SUBDIRECTORY permission for a directory. + const APPEND = 0b10000; + + /// DELETE_CHILD permission for a directory. + const DELETE_CHILD = 0b100000; + + /// READ_ATTRIBUTES permission for file or directory. + const READATTR = 0b1000000; + + /// WRITE_ATTRIBUTES permission for a file or directory. + const WRITEATTR = 0b10000000; + + /// READ_EXTATTRIBUTES permission for a file or directory. + const READEXTATTR = 0b100000000; + + /// WRITE_EXTATTRIBUTES permission for a file or directory. + const WRITEEXTATTR = 0b1000000000; + + /// READ_SECURITY permission for a file or directory. + const READSECURITY = 0b10000000000; + + /// WRITE_SECURITY permission for a file or directory. + const WRITESECURITY = 0b100000000000; + + /// CHANGE_OWNER permission for a file or directory. + const CHOWN = 0b1000000000000; + + /// SYNCHRONIZE permission (unsupported). + const SYNC = 0b10000000000000; + + /// NFSv4 READ_DATA permission. + const READ_DATA = 0b100000000000000; + + /// NFSv4 WRITE_DATA permission. + const WRITE_DATA = 0b1000000000000000; + } +} + +bitflags! { + #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] + pub struct Flag: u8 { + const DEFAULT = 0b1; + const INHERITED = 0b10; + const FILE_INHERIT = 0b100; + const DIRECTORY_INHERIT = 0b1000; + const LIMIT_INHERIT = 0b10000; + const ONLY_INHERIT = 0b100000; + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn ace_to_string_from_str() { + let ace = Ace { + platform: AcePlatform::CURRENT, + flags: Flag::all(), + owner_type: OwnerType::Owner, + allow: true, + permission: Permission::all(), + }; + assert_eq!(Ace::from_str(&ace.to_string()), Ok(ace)); + } +} diff --git a/cli/src/command/append.rs b/cli/src/command/append.rs index 3a92d4d8..8c45c82c 100644 --- a/cli/src/command/append.rs +++ b/cli/src/command/append.rs @@ -16,6 +16,7 @@ use std::{fs::File, io, path::PathBuf}; #[derive(Parser, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] #[command( + group(ArgGroup::new("unstable-acl").args(["keep_acl"]).requires("unstable")), group(ArgGroup::new("unstable-append-exclude").args(["exclude"]).requires("unstable")), group(ArgGroup::new("unstable-files-from").args(["files_from"]).requires("unstable")), group(ArgGroup::new("unstable-files-from-stdin").args(["files_from_stdin"]).requires("unstable")), @@ -38,6 +39,8 @@ pub(crate) struct AppendCommand { pub(crate) keep_permission: bool, #[arg(long, help = "Archiving the extended attributes of the files")] pub(crate) keep_xattr: bool, + #[arg(long, help = "Archiving the acl of the files")] + pub(crate) keep_acl: bool, #[arg(long, help = "Archiving user to the entries from given name")] pub(crate) uname: Option, #[arg(long, help = "Archiving group to the entries from given name")] @@ -136,6 +139,7 @@ fn append_to_archive(args: AppendCommand, verbosity: Verbosity) -> io::Result<() keep_timestamp: args.keep_timestamp, keep_permission: args.keep_permission, keep_xattr: args.keep_xattr, + keep_acl: args.keep_acl, }; let owner_options = OwnerOptions { uname: if args.numeric_owner { diff --git a/cli/src/command/commons.rs b/cli/src/command/commons.rs index 5dc3bf17..5524d623 100644 --- a/cli/src/command/commons.rs +++ b/cli/src/command/commons.rs @@ -23,6 +23,7 @@ pub(crate) struct KeepOptions { pub(crate) keep_timestamp: bool, pub(crate) keep_permission: bool, pub(crate) keep_xattr: bool, + pub(crate) keep_acl: bool, } #[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] @@ -179,6 +180,36 @@ pub(crate) fn apply_metadata( )); } } + #[cfg(feature = "acl")] + { + #[cfg(any( + target_os = "linux", + target_os = "freebsd", + target_os = "macos", + windows + ))] + if keep_options.keep_acl { + use crate::chunk; + use pna::RawChunk; + let ace_list = crate::utils::acl::get_facl(path)?; + for ace in ace_list { + entry.add_extra_chunk(RawChunk::from_data(chunk::faCe, ace.to_bytes())); + } + } + #[cfg(not(any( + target_os = "linux", + target_os = "freebsd", + target_os = "macos", + windows + )))] + if keep_options.keep_acl { + eprintln!("Currently acl is not supported on this platform."); + } + } + #[cfg(not(feature = "acl"))] + if keep_options.keep_acl { + eprintln!("Please enable `acl` feature and rebuild and install pna."); + } #[cfg(unix)] if keep_options.keep_xattr { if xattr::SUPPORTED_PLATFORM { diff --git a/cli/src/command/create.rs b/cli/src/command/create.rs index 7c215ad5..db6eb8e9 100644 --- a/cli/src/command/create.rs +++ b/cli/src/command/create.rs @@ -24,6 +24,7 @@ use std::{ #[derive(Parser, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] #[command( + group(ArgGroup::new("unstable-acl").args(["keep_acl"]).requires("unstable")), group(ArgGroup::new("unstable-create-exclude").args(["exclude"]).requires("unstable")), group(ArgGroup::new("unstable-files-from").args(["files_from"]).requires("unstable")), group(ArgGroup::new("unstable-files-from-stdin").args(["files_from_stdin"]).requires("unstable")), @@ -48,6 +49,8 @@ pub(crate) struct CreateCommand { pub(crate) keep_permission: bool, #[arg(long, help = "Archiving the extended attributes of the files")] pub(crate) keep_xattr: bool, + #[arg(long, help = "Archiving the acl of the files")] + pub(crate) keep_acl: bool, #[arg(long, help = "Split archive by total entry size")] pub(crate) split: Option>, #[arg(long, help = "Solid mode archive")] @@ -140,6 +143,7 @@ fn create_archive(args: CreateCommand, verbosity: Verbosity) -> io::Result<()> { keep_timestamp: args.keep_timestamp, keep_permission: args.keep_permission, keep_xattr: args.keep_xattr, + keep_acl: args.keep_acl, }; let owner_options = OwnerOptions { uname: if args.numeric_owner { diff --git a/cli/src/command/extract.rs b/cli/src/command/extract.rs index f3d394fd..e7198a1f 100644 --- a/cli/src/command/extract.rs +++ b/cli/src/command/extract.rs @@ -31,6 +31,7 @@ use std::{ #[derive(Parser, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] #[command( + group(ArgGroup::new("unstable-acl").args(["keep_acl"]).requires("unstable")), group(ArgGroup::new("user-flag").args(["numeric_owner", "uname"])), group(ArgGroup::new("group-flag").args(["numeric_owner", "gname"])), )] @@ -47,6 +48,8 @@ pub(crate) struct ExtractCommand { pub(crate) keep_permission: bool, #[arg(long, help = "Restore the extended attributes of the files")] pub(crate) keep_xattr: bool, + #[arg(long, help = "Restore the acl of the files")] + pub(crate) keep_acl: bool, #[arg(long, help = "Restore user from given name")] pub(crate) uname: Option, #[arg(long, help = "Restore group from given name")] @@ -85,6 +88,7 @@ fn extract_archive(args: ExtractCommand, verbosity: Verbosity) -> io::Result<()> keep_timestamp: args.keep_timestamp, keep_permission: args.keep_permission, keep_xattr: args.keep_xattr, + keep_acl: args.keep_acl, }; let owner_options = OwnerOptions { uname: if args.numeric_owner { @@ -303,6 +307,45 @@ pub(crate) fn extract_entry( if keep_options.keep_xattr { eprintln!("Currently extended attribute is not supported on this platform."); } + #[cfg(feature = "acl")] + { + #[cfg(any( + target_os = "linux", + target_os = "freebsd", + target_os = "macos", + windows + ))] + if keep_options.keep_acl { + use crate::chunk; + use pna::Chunk; + use std::str::FromStr; + + let mut acl = Vec::new(); + for c in item.extra_chunks() { + if c.ty() == chunk::faCe { + let body = std::str::from_utf8(c.data()).map_err(io::Error::other)?; + let ace = chunk::Ace::from_str(body).map_err(io::Error::other)?; + acl.push(ace); + } + } + if !acl.is_empty() { + utils::acl::set_facl(&path, acl)?; + } + } + #[cfg(not(any( + target_os = "linux", + target_os = "freebsd", + target_os = "macos", + windows + )))] + if keep_options.keep_acl { + eprintln!("Currently acl is not supported on this platform."); + } + } + #[cfg(not(feature = "acl"))] + if keep_options.keep_acl { + eprintln!("Please enable `acl` feature and rebuild and install pna."); + } if verbosity == Verbosity::Verbose { eprintln!("end: {}", path.display()); } diff --git a/cli/src/command/list.rs b/cli/src/command/list.rs index c0917dfb..fef9b4ba 100644 --- a/cli/src/command/list.rs +++ b/cli/src/command/list.rs @@ -1,4 +1,5 @@ use crate::{ + chunk, cli::{FileArgs, PasswordArgs, Verbosity}, command::{ ask_password, @@ -9,14 +10,15 @@ use crate::{ }; use ansi_term::{ANSIString, Colour, Style}; use chrono::{DateTime, Local}; -use clap::Parser; +use clap::{ArgGroup, Parser}; use pna::{ - Compression, DataKind, Encryption, ExtendedAttribute, ReadEntry, ReadOption, RegularEntry, - SolidHeader, + Chunk, Compression, DataKind, Encryption, ExtendedAttribute, ReadEntry, ReadOption, + RegularEntry, SolidHeader, }; use rayon::prelude::*; use std::{ io, + str::FromStr, time::{Duration, SystemTime, UNIX_EPOCH}, }; use tabled::{ @@ -30,6 +32,9 @@ use tabled::{ #[derive(Parser, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] #[clap(disable_help_flag = true)] +#[command( + group(ArgGroup::new("unstable-acl").args(["show_acl"]).requires("unstable")), +)] pub(crate) struct ListCommand { #[arg(short, long, help = "Display extended file metadata as a table")] pub(crate) long: bool, @@ -39,6 +44,8 @@ pub(crate) struct ListCommand { pub(crate) solid: bool, #[arg(short = '@', help = "Display extended file attributes in a table")] pub(crate) show_xattr: bool, + #[arg(short = 'e', help = "Display acl in a table (unstable)")] + pub(crate) show_acl: bool, #[arg( long, help = "Display user id and group id instead of user name and group name" @@ -86,6 +93,21 @@ impl TableRow { name: String::new(), } } + + fn from_acl(acl: chunk::Ace) -> Self { + Self { + encryption: String::new(), + compression: String::new(), + permissions: acl.to_string(), + raw_size: String::new(), + compressed_size: String::new(), + user: String::new(), + group: String::new(), + created: String::new(), + modified: String::new(), + name: String::new(), + } + } } impl @@ -186,6 +208,7 @@ fn list_archive(args: ListCommand, _: Verbosity) -> io::Result<()> { header: args.header, solid: args.solid, show_xattr: args.show_xattr, + show_acl: args.show_acl, numeric_owner: args.numeric_owner, }, ) @@ -196,6 +219,7 @@ pub(crate) struct ListOptions { pub(crate) header: bool, pub(crate) solid: bool, pub(crate) show_xattr: bool, + pub(crate) show_acl: bool, pub(crate) numeric_owner: bool, } @@ -227,6 +251,21 @@ pub(crate) fn run_list_archive( } else { Vec::new() }; + let acl = if args.show_acl { + let mut acl = Vec::new(); + for c in entry.extra_chunks() { + if c.ty() == chunk::faCe { + let body = std::str::from_utf8(c.data()) + .map_err(io::Error::other)?; + let ace = + chunk::Ace::from_str(body).map_err(io::Error::other)?; + acl.push(ace); + } + } + acl + } else { + Vec::new() + }; entries.push( ( entry, @@ -237,6 +276,9 @@ pub(crate) fn run_list_archive( ) .into(), ); + for ace in acl { + entries.push(TableRow::from_acl(ace)); + } entries.extend(xattrs); } } else { @@ -252,7 +294,24 @@ pub(crate) fn run_list_archive( } else { Vec::new() }; + let acl = if args.show_acl { + let mut acl = Vec::new(); + for c in item.extra_chunks() { + if c.ty() == chunk::faCe { + let body = + std::str::from_utf8(c.data()).map_err(io::Error::other)?; + let ace = chunk::Ace::from_str(body).map_err(io::Error::other)?; + acl.push(ace); + } + } + acl + } else { + Vec::new() + }; entries.push((item, password, now, None, args.numeric_owner).into()); + for ace in acl { + entries.push(TableRow::from_acl(ace)); + } entries.extend(xattrs); } } diff --git a/cli/src/command/stdio.rs b/cli/src/command/stdio.rs index b31456f0..3577c8b4 100644 --- a/cli/src/command/stdio.rs +++ b/cli/src/command/stdio.rs @@ -22,6 +22,7 @@ use std::{ #[derive(Args, Clone, Eq, PartialEq, Hash, Debug)] #[command( + group(ArgGroup::new("unstable-acl").args(["keep_acl"]).requires("unstable")), group(ArgGroup::new("bundled-flags").args(["create", "extract", "list"]).required(true)), group(ArgGroup::new("unstable-exclude-from").args(["files_from"]).requires("unstable")), group(ArgGroup::new("unstable-files-from").args(["files_from"]).requires("unstable")), @@ -47,6 +48,8 @@ pub(crate) struct StdioCommand { keep_permission: bool, #[arg(long, help = "Archiving the extended attributes of the files")] keep_xattr: bool, + #[arg(long, help = "Archiving the acl of the files")] + pub(crate) keep_acl: bool, #[arg(long, help = "Solid mode archive")] pub(crate) solid: bool, #[command(flatten)] @@ -152,6 +155,7 @@ fn run_create_archive(args: StdioCommand, verbosity: Verbosity) -> io::Result<() keep_timestamp: args.keep_timestamp, keep_permission: args.keep_permission, keep_xattr: args.keep_xattr, + keep_acl: args.keep_acl, }; let owner_options = OwnerOptions { uname: if args.numeric_owner { @@ -199,6 +203,7 @@ fn run_extract_archive(args: StdioCommand, verbosity: Verbosity) -> io::Result<( keep_timestamp: args.keep_timestamp, keep_permission: args.keep_permission, keep_xattr: args.keep_xattr, + keep_acl: args.keep_acl, }, owner_options: OwnerOptions { uname: if args.numeric_owner { @@ -241,6 +246,7 @@ fn run_list_archive(args: StdioCommand, _verbosity: Verbosity) -> io::Result<()> header: false, solid: true, show_xattr: false, + show_acl: false, numeric_owner: args.numeric_owner, }; if let Some(path) = args.file { diff --git a/cli/src/command/strip.rs b/cli/src/command/strip.rs index 8b43deee..97cf9df0 100644 --- a/cli/src/command/strip.rs +++ b/cli/src/command/strip.rs @@ -4,7 +4,7 @@ use crate::{ utils::{self, PathPartExt}, }; use clap::{Parser, ValueHint}; -use pna::{Archive, Metadata}; +use pna::{Archive, Chunk, Metadata}; use std::{env::temp_dir, fs, io, path::PathBuf}; #[derive(Parser, Clone, Eq, PartialEq, Hash, Debug)] @@ -15,6 +15,8 @@ pub(crate) struct StripCommand { pub(crate) keep_permission: bool, #[arg(long, help = "Keep the extended attributes of the files")] pub(crate) keep_xattr: bool, + #[arg(long, help = "Keep the acl of the files")] + pub(crate) keep_acl: bool, #[arg(long, help = "Output file path", value_hint = ValueHint::AnyPath)] pub(crate) output: Option, #[command(flatten)] @@ -61,6 +63,15 @@ fn strip_metadata(args: StripCommand, _verbosity: Verbosity) -> io::Result<()> { if !args.keep_xattr { entry = entry.with_xattrs(&[]); } + if !args.keep_acl { + let filtered = entry + .extra_chunks() + .iter() + .filter(|it| it.ty() != crate::chunk::faCe) + .cloned() + .collect::>(); + entry = entry.with_extra_chunks(&filtered); + } out_archive.add_entry(entry)?; Ok(()) }, diff --git a/cli/src/command/update.rs b/cli/src/command/update.rs index 5d8d95d5..abd6947b 100644 --- a/cli/src/command/update.rs +++ b/cli/src/command/update.rs @@ -23,6 +23,7 @@ use std::{ #[derive(Parser, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] #[command( + group(ArgGroup::new("unstable-acl").args(["keep_acl"]).requires("unstable")), group(ArgGroup::new("unstable-update-exclude").args(["exclude"]).requires("unstable")), group(ArgGroup::new("unstable-files-from").args(["files_from"]).requires("unstable")), group(ArgGroup::new("unstable-files-from-stdin").args(["files_from_stdin"]).requires("unstable")), @@ -45,6 +46,8 @@ pub(crate) struct UpdateCommand { pub(crate) keep_permission: bool, #[arg(long, help = "Archiving the extended attributes of the files")] pub(crate) keep_xattr: bool, + #[arg(long, help = "Archiving the acl of the files")] + pub(crate) keep_acl: bool, #[arg(long, help = "Archiving user to the entries from given name")] pub(crate) uname: Option, #[arg(long, help = "Archiving group to the entries from given name")] @@ -123,6 +126,7 @@ fn update_archive(args: UpdateCommand, verbosity: Verbosity) -> io::Result<()> { keep_timestamp: args.keep_timestamp, keep_permission: args.keep_permission, keep_xattr: args.keep_xattr, + keep_acl: args.keep_acl, }; let owner_options = OwnerOptions { uname: if args.numeric_owner { diff --git a/cli/src/lib.rs b/cli/src/lib.rs index 0fbc6816..3a2e7d14 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -1,3 +1,4 @@ +mod chunk; pub mod cli; pub mod command; mod utils; diff --git a/cli/src/main.rs b/cli/src/main.rs index 433b497f..3fe993fb 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,3 +1,4 @@ +mod chunk; mod cli; mod command; mod utils; diff --git a/cli/src/utils.rs b/cli/src/utils.rs index 7152891d..3bb53c00 100644 --- a/cli/src/utils.rs +++ b/cli/src/utils.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "acl")] +pub(crate) mod acl; pub(crate) mod fs; mod io; mod path; diff --git a/cli/src/utils/acl.rs b/cli/src/utils/acl.rs new file mode 100644 index 00000000..91a0eb3b --- /dev/null +++ b/cli/src/utils/acl.rs @@ -0,0 +1,9 @@ +#[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "macos"))] +mod unix; +#[cfg(windows)] +mod windows; + +#[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "macos"))] +pub use unix::*; +#[cfg(windows)] +pub use windows::*; diff --git a/cli/src/utils/acl/unix.rs b/cli/src/utils/acl/unix.rs new file mode 100644 index 00000000..c6c2a49b --- /dev/null +++ b/cli/src/utils/acl/unix.rs @@ -0,0 +1,350 @@ +use crate::chunk::{ + ace_convert_current_platform, Ace, AcePlatform, Flag, Identifier, OwnerType, Permission, +}; +use std::io; +use std::path::Path; + +pub fn set_facl>(path: P, acl: Vec) -> io::Result<()> { + let path = path.as_ref(); + let mut acl_entries: Vec = acl.into_iter().map(Into::into).collect::>(); + #[cfg(target_os = "macos")] + { + use std::os::unix::fs::MetadataExt; + let meta = std::fs::metadata(path)?; + + acl_entries = acl_entries + .into_iter() + .map(|mut it| { + if it.kind == exacl::AclEntryKind::User && it.name.is_empty() { + it.name = meta.uid().to_string(); + it + } else if it.kind == exacl::AclEntryKind::Group && it.name.is_empty() { + it.name = meta.gid().to_string(); + it + } else { + it + } + }) + .collect(); + } + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + { + let mut exist_user = false; + let mut exist_group = false; + let mut exist_other = false; + for entry in acl_entries.iter() { + match entry.kind { + exacl::AclEntryKind::User if entry.name.is_empty() => exist_user = true, + exacl::AclEntryKind::Group if entry.name.is_empty() => exist_group = true, + exacl::AclEntryKind::Other => exist_other = true, + _ => (), + } + } + if !exist_user || !exist_group || !exist_other { + let facl = exacl::getfacl(path, None)?; + if !exist_user { + acl_entries.push( + facl.iter() + .find(|it| { + it.allow + && it.flags.is_empty() + && it.name.is_empty() + && it.kind == exacl::AclEntryKind::User + }) + .expect("failed to find owner ace") + .clone(), + ); + } + if !exist_group { + acl_entries.push( + facl.iter() + .find(|it| { + it.allow + && it.flags.is_empty() + && it.name.is_empty() + && it.kind == exacl::AclEntryKind::Group + }) + .expect("failed to find owner group ace") + .clone(), + ); + } + if !exist_other { + acl_entries.push( + facl.iter() + .find(|it| { + it.allow + && it.flags.is_empty() + && it.name.is_empty() + && it.kind == exacl::AclEntryKind::Other + }) + .expect("failed to find other ace") + .clone(), + ); + } + } + } + exacl::setfacl(&[path], &acl_entries, None) +} + +pub fn get_facl>(path: P) -> io::Result> { + let ace_list = exacl::getfacl(path.as_ref(), None)?; + Ok(ace_list.into_iter().map(Into::into).collect()) +} + +#[allow(clippy::from_over_into)] +impl Into for exacl::AclEntry { + fn into(self) -> Ace { + let mut flags = Flag::empty(); + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + if self.flags.contains(exacl::Flag::DEFAULT) { + flags.insert(Flag::DEFAULT); + } + #[cfg(any(target_os = "macos", target_os = "freebsd"))] + if self.flags.contains(exacl::Flag::FILE_INHERIT) { + flags.insert(Flag::FILE_INHERIT); + } + #[cfg(any(target_os = "macos", target_os = "freebsd"))] + if self.flags.contains(exacl::Flag::DIRECTORY_INHERIT) { + flags.insert(Flag::DIRECTORY_INHERIT); + } + #[cfg(any(target_os = "macos", target_os = "freebsd"))] + if self.flags.contains(exacl::Flag::ONLY_INHERIT) { + flags.insert(Flag::ONLY_INHERIT); + } + #[cfg(any(target_os = "macos", target_os = "freebsd"))] + if self.flags.contains(exacl::Flag::LIMIT_INHERIT) { + flags.insert(Flag::LIMIT_INHERIT); + } + #[cfg(any(target_os = "macos", target_os = "freebsd"))] + if self.flags.contains(exacl::Flag::INHERITED) { + flags.insert(Flag::INHERITED); + } + let mut permission = Permission::empty(); + if self.perms.contains(exacl::Perm::READ) { + permission.insert(Permission::READ); + } + if self.perms.contains(exacl::Perm::WRITE) { + permission.insert(Permission::WRITE); + } + if self.perms.contains(exacl::Perm::EXECUTE) { + permission.insert(Permission::EXECUTE); + } + #[cfg(any(target_os = "macos", target_os = "freebsd"))] + if self.perms.contains(exacl::Perm::DELETE) { + permission.insert(Permission::DELETE); + } + #[cfg(any(target_os = "macos", target_os = "freebsd"))] + if self.perms.contains(exacl::Perm::APPEND) { + permission.insert(Permission::APPEND); + } + #[cfg(any(target_os = "macos", target_os = "freebsd"))] + if self.perms.contains(exacl::Perm::DELETE_CHILD) { + permission.insert(Permission::DELETE_CHILD); + } + #[cfg(any(target_os = "macos", target_os = "freebsd"))] + if self.perms.contains(exacl::Perm::READATTR) { + permission.insert(Permission::READATTR); + } + #[cfg(any(target_os = "macos", target_os = "freebsd"))] + if self.perms.contains(exacl::Perm::WRITEATTR) { + permission.insert(Permission::WRITEATTR); + } + #[cfg(any(target_os = "macos", target_os = "freebsd"))] + if self.perms.contains(exacl::Perm::READEXTATTR) { + permission.insert(Permission::READEXTATTR); + } + #[cfg(any(target_os = "macos", target_os = "freebsd"))] + if self.perms.contains(exacl::Perm::WRITEEXTATTR) { + permission.insert(Permission::WRITEEXTATTR); + } + #[cfg(any(target_os = "macos", target_os = "freebsd"))] + if self.perms.contains(exacl::Perm::READSECURITY) { + permission.insert(Permission::READSECURITY); + } + #[cfg(any(target_os = "macos", target_os = "freebsd"))] + if self.perms.contains(exacl::Perm::WRITESECURITY) { + permission.insert(Permission::WRITESECURITY); + } + #[cfg(any(target_os = "macos", target_os = "freebsd"))] + if self.perms.contains(exacl::Perm::CHOWN) { + permission.insert(Permission::CHOWN); + } + #[cfg(any(target_os = "macos", target_os = "freebsd"))] + if self.perms.contains(exacl::Perm::SYNC) { + permission.insert(Permission::SYNC); + } + #[cfg(target_os = "freebsd")] + if self.perms.contains(exacl::Perm::READ_DATA) { + permission.insert(Permission::READ_DATA); + } + #[cfg(target_os = "freebsd")] + if self.perms.contains(exacl::Perm::WRITE_DATA) { + permission.insert(Permission::WRITE_DATA); + } + + Ace { + platform: AcePlatform::CURRENT, + flags, + owner_type: match self.kind { + exacl::AclEntryKind::User if self.name.is_empty() => OwnerType::Owner, + exacl::AclEntryKind::User => OwnerType::User(Identifier(self.name)), + exacl::AclEntryKind::Group if self.name.is_empty() => OwnerType::OwnerGroup, + exacl::AclEntryKind::Group => OwnerType::Group(Identifier(self.name)), + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + exacl::AclEntryKind::Mask => OwnerType::Mask, + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + exacl::AclEntryKind::Other => OwnerType::Other, + #[cfg(target_os = "freebsd")] + exacl::AclEntryKind::Everyone => OwnerType::Other, + exacl::AclEntryKind::Unknown => panic!("Unknown acl owner"), + }, + allow: self.allow, + permission, + } + } +} + +#[allow(clippy::from_over_into)] +impl Into for Ace { + fn into(self) -> exacl::AclEntry { + let slf = ace_convert_current_platform(self); + let (kind, name) = match slf.owner_type { + OwnerType::Owner => (exacl::AclEntryKind::User, String::new()), + OwnerType::User(u) => (exacl::AclEntryKind::User, u.0), + OwnerType::OwnerGroup => (exacl::AclEntryKind::Group, String::new()), + OwnerType::Group(u) => { + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + if u.0 == "everyone" { + (exacl::AclEntryKind::Other, String::new()) + } else { + (exacl::AclEntryKind::Group, u.0) + } + #[cfg(not(any(target_os = "linux", target_os = "freebsd")))] + (exacl::AclEntryKind::Group, u.0) + } + #[cfg(not(any(target_os = "linux", target_os = "freebsd")))] + OwnerType::Mask => (exacl::AclEntryKind::Unknown, String::new()), + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + OwnerType::Mask => (exacl::AclEntryKind::Mask, String::new()), + #[cfg(not(any(target_os = "linux", target_os = "freebsd")))] + OwnerType::Other => (exacl::AclEntryKind::Group, "everyone".to_string()), + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + OwnerType::Other => (exacl::AclEntryKind::Other, String::new()), + }; + let mut perms = exacl::Perm::empty(); + if slf.permission.contains(Permission::READ) { + perms.insert(exacl::Perm::READ); + } + if slf.permission.contains(Permission::WRITE) { + perms.insert(exacl::Perm::WRITE); + } + if slf.permission.contains(Permission::EXECUTE) { + perms.insert(exacl::Perm::EXECUTE); + } + #[cfg(any(target_os = "macos", target_os = "freebsd"))] + if slf.permission.contains(Permission::DELETE) { + perms.insert(exacl::Perm::DELETE); + } + #[cfg(any(target_os = "macos", target_os = "freebsd"))] + if slf.permission.contains(Permission::APPEND) { + perms.insert(exacl::Perm::APPEND); + } + #[cfg(any(target_os = "macos", target_os = "freebsd"))] + if slf.permission.contains(Permission::DELETE_CHILD) { + perms.insert(exacl::Perm::DELETE_CHILD); + } + #[cfg(any(target_os = "macos", target_os = "freebsd"))] + if slf.permission.contains(Permission::READATTR) { + perms.insert(exacl::Perm::READATTR); + } + #[cfg(any(target_os = "macos", target_os = "freebsd"))] + if slf.permission.contains(Permission::WRITEATTR) { + perms.insert(exacl::Perm::WRITEATTR); + } + #[cfg(any(target_os = "macos", target_os = "freebsd"))] + if slf.permission.contains(Permission::READEXTATTR) { + perms.insert(exacl::Perm::READEXTATTR); + } + #[cfg(any(target_os = "macos", target_os = "freebsd"))] + if slf.permission.contains(Permission::WRITEEXTATTR) { + perms.insert(exacl::Perm::WRITEEXTATTR); + } + #[cfg(any(target_os = "macos", target_os = "freebsd"))] + if slf.permission.contains(Permission::READSECURITY) { + perms.insert(exacl::Perm::READSECURITY); + } + #[cfg(any(target_os = "macos", target_os = "freebsd"))] + if slf.permission.contains(Permission::WRITESECURITY) { + perms.insert(exacl::Perm::WRITESECURITY); + } + #[cfg(any(target_os = "macos", target_os = "freebsd"))] + if slf.permission.contains(Permission::CHOWN) { + perms.insert(exacl::Perm::CHOWN); + } + #[cfg(any(target_os = "macos", target_os = "freebsd"))] + if slf.permission.contains(Permission::SYNC) { + perms.insert(exacl::Perm::SYNC); + } + #[cfg(target_os = "freebsd")] + if slf.permission.contains(Permission::READ_DATA) { + perms.insert(exacl::Perm::READ_DATA); + } + #[cfg(target_os = "freebsd")] + if slf.permission.contains(Permission::WRITE_DATA) { + perms.insert(exacl::Perm::WRITE_DATA); + } + + let mut flags = exacl::Flag::empty(); + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + if slf.flags.contains(Flag::DEFAULT) { + flags.insert(exacl::Flag::DEFAULT); + } + #[cfg(any(target_os = "macos", target_os = "freebsd"))] + if slf.flags.contains(Flag::FILE_INHERIT) { + flags.insert(exacl::Flag::FILE_INHERIT); + } + #[cfg(any(target_os = "macos", target_os = "freebsd"))] + if slf.flags.contains(Flag::DIRECTORY_INHERIT) { + flags.insert(exacl::Flag::DIRECTORY_INHERIT); + } + #[cfg(any(target_os = "macos", target_os = "freebsd"))] + if slf.flags.contains(Flag::LIMIT_INHERIT) { + flags.insert(exacl::Flag::LIMIT_INHERIT); + } + #[cfg(any(target_os = "macos", target_os = "freebsd"))] + if slf.flags.contains(Flag::ONLY_INHERIT) { + flags.insert(exacl::Flag::ONLY_INHERIT); + } + #[cfg(any(target_os = "macos", target_os = "freebsd"))] + if slf.flags.contains(Flag::INHERITED) { + flags.insert(exacl::Flag::INHERITED); + } + exacl::AclEntry { + kind, + name, + perms, + flags, + allow: slf.allow, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn ace_mutual_convert() { + let acl_entry = exacl::AclEntry { + kind: exacl::AclEntryKind::User, + name: "name".to_string(), + perms: exacl::Perm::all(), + flags: exacl::Flag::all(), + allow: false, + }; + assert_eq!( + acl_entry.clone(), + >::into(acl_entry).into() + ); + } +} diff --git a/cli/src/utils/acl/windows.rs b/cli/src/utils/acl/windows.rs new file mode 100644 index 00000000..2f184496 --- /dev/null +++ b/cli/src/utils/acl/windows.rs @@ -0,0 +1,649 @@ +use crate::chunk; +use crate::chunk::{ace_convert_current_platform, AcePlatform, Identifier, OwnerType}; +use crate::utils::fs::encode_wide; +use field_offset::offset_of; +use std::fmt::{Display, Formatter}; +use std::path::{Path, PathBuf}; +use std::ptr::null_mut; +use std::str::FromStr; +use std::{io, mem}; +use windows::core::{PCWSTR, PWSTR}; +use windows::Win32::Foundation::{ + LocalFree, SetLastError, ERROR_INSUFFICIENT_BUFFER, ERROR_SUCCESS, HLOCAL, PSID, +}; +use windows::Win32::Security::Authorization::{ + ConvertSidToStringSidW, ConvertStringSidToSidW, GetNamedSecurityInfoW, SetNamedSecurityInfoW, + SE_FILE_OBJECT, +}; +use windows::Win32::Security::{ + AddAccessAllowedAceEx, AddAccessDeniedAceEx, CopySid, GetAce, GetLengthSid, InitializeAcl, + IsValidSid, LookupAccountNameW, LookupAccountSidW, SidTypeAlias, SidTypeComputer, + SidTypeDeletedAccount, SidTypeDomain, SidTypeGroup, SidTypeInvalid, SidTypeLabel, + SidTypeLogonSession, SidTypeUnknown, SidTypeUser, SidTypeWellKnownGroup, ACCESS_ALLOWED_ACE, + ACCESS_DENIED_ACE, ACE_FLAGS, ACE_HEADER, ACL as Win32ACL, ACL_REVISION_DS, + CONTAINER_INHERIT_ACE, DACL_SECURITY_INFORMATION, GROUP_SECURITY_INFORMATION, INHERITED_ACE, + INHERIT_ONLY_ACE, NO_PROPAGATE_INHERIT_ACE, OBJECT_INHERIT_ACE, OWNER_SECURITY_INFORMATION, + PROTECTED_DACL_SECURITY_INFORMATION, PSECURITY_DESCRIPTOR, SID_NAME_USE, +}; +use windows::Win32::Storage::FileSystem::{ + DELETE, FILE_ACCESS_RIGHTS, FILE_APPEND_DATA, FILE_DELETE_CHILD, FILE_EXECUTE, + FILE_GENERIC_READ, FILE_GENERIC_WRITE, FILE_READ_ATTRIBUTES, FILE_READ_DATA, FILE_READ_EA, + FILE_WRITE_ATTRIBUTES, FILE_WRITE_DATA, FILE_WRITE_EA, READ_CONTROL, SYNCHRONIZE, WRITE_DAC, + WRITE_OWNER, +}; +use windows::Win32::System::SystemServices::{ACCESS_ALLOWED_ACE_TYPE, ACCESS_DENIED_ACE_TYPE}; + +pub fn set_facl>(path: P, acl: Vec) -> io::Result<()> { + let acl_entries = acl.into_iter().map(Into::into).collect::>(); + let acl = ACL::try_from(path.as_ref())?; + acl.set_d_acl(&acl_entries) +} + +pub fn get_facl>(path: P) -> io::Result> { + let acl = ACL::try_from(path.as_ref())?; + let ace_list = acl.get_d_acl()?; + Ok(ace_list.into_iter().map(Into::into).collect()) +} + +type PACL = *mut Win32ACL; + +#[allow(non_camel_case_types)] +type PACE_HEADER = *mut ACE_HEADER; + +pub struct SecurityDescriptor { + p_security_descriptor: PSECURITY_DESCRIPTOR, + p_dacl: PACL, + #[allow(unused)] + p_sacl: PACL, + #[allow(unused)] + p_sid_owner: PSID, + #[allow(unused)] + p_sid_group: PSID, +} + +impl SecurityDescriptor { + pub fn try_from(path: &Path) -> io::Result { + let os_str = encode_wide(path.as_os_str())?; + let mut p_security_descriptor = PSECURITY_DESCRIPTOR::default(); + let mut p_dacl: PACL = null_mut(); + let mut p_sacl: PACL = null_mut(); + let mut p_sid_owner: PSID = PSID::default(); + let mut p_sid_group: PSID = PSID::default(); + let error = unsafe { + GetNamedSecurityInfoW( + PCWSTR::from_raw(os_str.as_ptr()), + SE_FILE_OBJECT, + DACL_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | OWNER_SECURITY_INFORMATION, + Some(&mut p_sid_owner as _), + Some(&mut p_sid_group as _), + Some(&mut p_dacl as _), + Some(&mut p_sacl as _), + &mut p_security_descriptor as _, + ) + }; + if error != ERROR_SUCCESS { + unsafe { SetLastError(error) }; + return Err(io::Error::last_os_error()); + } + Ok(Self { + p_security_descriptor, + p_sid_owner, + p_sid_group, + p_sacl, + p_dacl, + }) + } + + pub fn apply(&self, path: &Path, pacl: PACL) -> io::Result<()> { + let c_str = encode_wide(path.as_os_str())?; + let status = unsafe { + SetNamedSecurityInfoW( + PCWSTR::from_raw(c_str.as_ptr()), + SE_FILE_OBJECT, + DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION, + None, + None, + Some(pacl), + None, + ) + }; + if status != ERROR_SUCCESS { + unsafe { SetLastError(status) }; + return Err(io::Error::last_os_error()); + } + Ok(()) + } +} + +impl Drop for SecurityDescriptor { + fn drop(&mut self) { + if !self.p_security_descriptor.is_invalid() { + unsafe { + LocalFree(HLOCAL(self.p_security_descriptor.0)); + } + } + } +} + +pub struct ACL { + path: PathBuf, + security_descriptor: SecurityDescriptor, +} + +impl ACL { + pub fn try_from(path: &Path) -> io::Result { + Ok(Self { + security_descriptor: SecurityDescriptor::try_from(path)?, + path: path.to_path_buf(), + }) + } + + pub fn get_d_acl(&self) -> io::Result> { + let mut result = Vec::new(); + let p_acl = self.security_descriptor.p_dacl; + let count = unsafe { *p_acl }.AceCount as u32; + for i in 0..count { + let mut header: PACE_HEADER = null_mut(); + unsafe { GetAce(p_acl, i, mem::transmute(&mut header)) }.map_err(io::Error::other)?; + let ace = match unsafe { *header }.AceType as u32 { + ACCESS_ALLOWED_ACE_TYPE => { + let entry_ptr: *mut ACCESS_ALLOWED_ACE = header as *mut ACCESS_ALLOWED_ACE; + let sid_offset = offset_of!(ACCESS_ALLOWED_ACE => SidStart); + let p_sid = PSID(sid_offset.apply_ptr_mut(entry_ptr) as _); + let sid = Sid::try_from(p_sid)?; + ACLEntry { + ace_type: AceType::AccessAllow, + sid, + size: unsafe { *header }.AceSize, + flags: unsafe { *header }.AceFlags, + mask: unsafe { *entry_ptr }.Mask, + } + } + ACCESS_DENIED_ACE_TYPE => { + let entry_ptr: *mut ACCESS_DENIED_ACE = header as *mut ACCESS_DENIED_ACE; + let sid_offset = offset_of!(ACCESS_DENIED_ACE => SidStart); + let p_sid = PSID(sid_offset.apply_ptr_mut(entry_ptr) as _); + let sid = Sid::try_from(p_sid)?; + ACLEntry { + ace_type: AceType::AccessDeny, + sid, + size: unsafe { *header }.AceSize, + flags: unsafe { *header }.AceFlags, + mask: unsafe { *entry_ptr }.Mask, + } + } + t => ACLEntry { + ace_type: AceType::Unknown(t as u8), + size: 0, + mask: 0, + flags: 0, + sid: Sid::null_sid(), + }, + }; + result.push(ace) + } + Ok(result) + } + + pub fn set_d_acl(&self, acl_entries: &[ACLEntry]) -> io::Result<()> { + let acl_size = acl_entries.iter().map(|it| it.size as usize).sum::() + + mem::size_of::(); + let mut new_acl_buffer = Vec::::with_capacity(acl_size); + let new_acl = new_acl_buffer.as_mut_ptr(); + unsafe { InitializeAcl(new_acl as _, acl_size as u32, ACL_REVISION_DS) } + .map_err(io::Error::other)?; + for ace in acl_entries { + match ace.ace_type { + AceType::AccessAllow => unsafe { + AddAccessAllowedAceEx( + new_acl as _, + ACL_REVISION_DS, + ACE_FLAGS(ace.flags as u32), + ace.mask, + ace.sid.as_psid(), + ) + }, + AceType::AccessDeny => unsafe { + AddAccessDeniedAceEx( + new_acl as _, + ACL_REVISION_DS, + ACE_FLAGS(ace.flags as u32), + ace.mask, + ace.sid.as_psid(), + ) + }, + AceType::Unknown(n) => return Err(io::Error::other(format!("{}", n))), + } + .map_err(io::Error::other)?; + } + self.security_descriptor.apply(&self.path, new_acl as _)?; + Ok(()) + } +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum AceType { + AccessAllow, + AccessDeny, + Unknown(u8), +} + +impl AceType { + pub fn entry_size(&self) -> usize { + match self { + AceType::AccessAllow => mem::size_of::(), + AceType::AccessDeny => mem::size_of::(), + AceType::Unknown(_) => 0, + } + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum SidType { + User, + Group, + Domain, + Alias, + WellKnownGroup, + DeletedAccount, + Invalid, + Unknown(SID_NAME_USE), + Computer, + Label, + LogonSession, +} + +impl From for SidType { + #[allow(non_upper_case_globals)] + fn from(value: SID_NAME_USE) -> Self { + match value { + SidTypeUser => Self::User, + SidTypeGroup => Self::Group, + SidTypeDomain => Self::Domain, + SidTypeAlias => Self::Alias, + SidTypeWellKnownGroup => Self::WellKnownGroup, + SidTypeDeletedAccount => Self::DeletedAccount, + SidTypeInvalid => Self::Invalid, + SidTypeUnknown => Self::Unknown(value), + SidTypeComputer => Self::Computer, + SidTypeLabel => Self::Label, + SidTypeLogonSession => Self::LogonSession, + v => Self::Unknown(v), + } + } +} + +fn lookup_account_sid(psid: PSID) -> io::Result<(String, SidType)> { + let mut name_len = 0u32; + let mut sysname_len = 0u32; + let mut sid_type = SID_NAME_USE::default(); + match unsafe { + LookupAccountSidW( + PCWSTR::null(), + psid, + PWSTR::null(), + &mut name_len as _, + PWSTR::null(), + &mut sysname_len as _, + &mut sid_type as _, + ) + } { + Ok(_) => Err(io::Error::other("failed to convert sid to name")), + Err(e) if e.code() == ERROR_INSUFFICIENT_BUFFER.to_hresult() => Ok(()), + Err(e) => Err(io::Error::other(e)), + }?; + let mut name = Vec::::with_capacity(name_len as usize); + let mut sysname = Vec::::with_capacity(sysname_len as usize); + let name_ptr = PWSTR::from_raw(name.as_mut_ptr() as _); + unsafe { + LookupAccountSidW( + PCWSTR::null(), + psid, + name_ptr, + &mut name_len as _, + PWSTR::from_raw(sysname.as_mut_ptr() as _), + &mut sysname_len as _, + &mut sid_type as _, + ) + } + .map_err(io::Error::other)?; + let name = unsafe { name_ptr.to_string() }.map_err(io::Error::other)?; + Ok((name, SidType::from(sid_type))) +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Sid { + ty: SidType, + name: String, + raw: Vec, +} + +impl Sid { + fn null_sid() -> Self { + Self::from_str("S-1-0-0").expect("null group sid creation failed") + } + + fn try_from_name(name: &str, system: Option<&str>) -> io::Result { + let encoded_name = encode_wide(name.as_ref())?; + let system = system.map(|it| encode_wide(it.as_ref())).transpose()?; + let mut sid_len = 0u32; + let mut sys_name_len = 0u32; + let mut sid_type = SID_NAME_USE::default(); + match unsafe { + LookupAccountNameW( + system + .as_ref() + .map_or(PCWSTR::null(), |it| PCWSTR::from_raw(it.as_ptr())), + PCWSTR::from_raw(encoded_name.as_ptr()), + PSID::default(), + &mut sid_len as _, + PWSTR::null(), + &mut sys_name_len as _, + &mut sid_type as _, + ) + } { + Ok(_) => Err(io::Error::other("failed to resolve sid from name")), + Err(e) if e.code() == ERROR_INSUFFICIENT_BUFFER.to_hresult() => Ok(()), + Err(e) => Err(io::Error::other(e)), + }?; + if sid_len == 0 { + return Err(io::Error::other("lookup error")); + } + let mut sid = Vec::with_capacity(sid_len as usize); + let mut sys_name = Vec::::with_capacity(sys_name_len as usize); + unsafe { + LookupAccountNameW( + system + .as_ref() + .map_or(PCWSTR::null(), |it| PCWSTR::from_raw(it.as_ptr())), + PCWSTR::from_raw(encoded_name.as_ptr()), + PSID(sid.as_mut_ptr() as _), + &mut sid_len as _, + PWSTR::from_raw(sys_name.as_mut_ptr() as _), + &mut sys_name_len as _, + &mut sid_type as _, + ) + .map_err(io::Error::other)?; + } + let ty = SidType::from(sid_type); + unsafe { sid.set_len(sid_len as usize) } + Ok(Self { + ty, + name: name.to_string(), + raw: sid, + }) + } + + #[inline] + fn as_ptr(&self) -> *const u8 { + self.raw.as_ptr() + } + + #[inline] + fn as_psid(&self) -> PSID { + PSID(self.as_ptr() as _) + } +} + +impl Display for Sid { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let mut raw_str = PWSTR::null(); + unsafe { ConvertSidToStringSidW(self.as_psid(), &mut raw_str) } + .map_err(|_| std::fmt::Error::default())?; + let r = write!(f, "{}", unsafe { raw_str.display() }); + unsafe { LocalFree(HLOCAL(raw_str.as_ptr() as _)) }; + r + } +} + +impl FromStr for Sid { + type Err = (); + fn from_str(s: &str) -> Result { + let mut psid = PSID::default(); + let s = encode_wide(s.as_ref()).map_err(|_| ())?; + unsafe { ConvertStringSidToSidW(PCWSTR::from_raw(s.as_ptr()), &mut psid as _) } + .map_err(|_| ())?; + Self::try_from(psid).map_err(|_e| ()) + } +} + +impl TryFrom for Sid { + type Error = io::Error; + fn try_from(value: PSID) -> Result { + if !unsafe { IsValidSid(value) }.as_bool() { + return Err(io::Error::other("invalid sid")); + } + let sid_len = unsafe { GetLengthSid(value) }; + let mut sid = Vec::with_capacity(sid_len as usize); + unsafe { CopySid(sid_len, PSID(sid.as_mut_ptr() as _), value) } + .map_err(io::Error::other)?; + unsafe { sid.set_len(sid_len as usize) } + let (name, ty) = lookup_account_sid(PSID(sid.as_ptr() as _))?; + Ok(Self { ty, name, raw: sid }) + } +} + +pub struct ACLEntry { + pub ace_type: AceType, + pub sid: Sid, + size: u16, + pub flags: u8, + pub mask: u32, +} + +const PERMISSION_MAPPING_TABLE: [(chunk::Permission, FILE_ACCESS_RIGHTS); 16] = [ + (chunk::Permission::READ, FILE_GENERIC_READ), + (chunk::Permission::WRITE, FILE_GENERIC_WRITE), + (chunk::Permission::EXECUTE, FILE_EXECUTE), + (chunk::Permission::DELETE, DELETE), + (chunk::Permission::APPEND, FILE_APPEND_DATA), + (chunk::Permission::DELETE_CHILD, FILE_DELETE_CHILD), + (chunk::Permission::READATTR, FILE_READ_ATTRIBUTES), + (chunk::Permission::WRITEATTR, FILE_WRITE_ATTRIBUTES), + (chunk::Permission::READEXTATTR, FILE_READ_EA), + (chunk::Permission::WRITEEXTATTR, FILE_WRITE_EA), + (chunk::Permission::READSECURITY, READ_CONTROL), + (chunk::Permission::WRITESECURITY, WRITE_DAC), + (chunk::Permission::CHOWN, WRITE_OWNER), + (chunk::Permission::SYNC, SYNCHRONIZE), + (chunk::Permission::READ_DATA, FILE_READ_DATA), + (chunk::Permission::WRITE_DATA, FILE_WRITE_DATA), +]; + +const FLAGS_MAPPING_TABLE: [(chunk::Flag, ACE_FLAGS); 6] = [ + (chunk::Flag::DEFAULT, INHERIT_ONLY_ACE), + (chunk::Flag::INHERITED, INHERITED_ACE), + (chunk::Flag::FILE_INHERIT, OBJECT_INHERIT_ACE), + (chunk::Flag::DIRECTORY_INHERIT, CONTAINER_INHERIT_ACE), + (chunk::Flag::LIMIT_INHERIT, NO_PROPAGATE_INHERIT_ACE), + (chunk::Flag::ONLY_INHERIT, INHERIT_ONLY_ACE), +]; + +#[allow(clippy::from_over_into)] +impl Into for chunk::Ace { + fn into(self) -> ACLEntry { + let slf = ace_convert_current_platform(self); + let name = match slf.owner_type { + OwnerType::Owner => String::new(), + OwnerType::User(i) => i.0, + OwnerType::OwnerGroup => String::new(), + OwnerType::Group(i) => i.0, + OwnerType::Mask => String::new(), + OwnerType::Other => "Guest".to_string(), + }; + let sid = Sid::try_from_name(&name, None).unwrap(); + let ace_type = if slf.allow { + AceType::AccessAllow + } else { + AceType::AccessDeny + }; + ACLEntry { + ace_type, + size: (ace_type.entry_size() - mem::size_of::() + sid.raw.len()) as u16, + flags: { + let mut flags = 0; + for (f, g) in FLAGS_MAPPING_TABLE { + if slf.flags.contains(f) { + flags |= g.0 as u8; + } + } + flags + }, + mask: { + let mut mask = 0; + for (permission, rights) in PERMISSION_MAPPING_TABLE { + if slf.permission.contains(permission) { + mask |= rights.0; + } + } + mask + }, + sid, + } + } +} + +#[allow(clippy::from_over_into)] +impl Into for ACLEntry { + fn into(self) -> chunk::Ace { + let allow = match self.ace_type { + AceType::AccessAllow => true, + AceType::AccessDeny => false, + t => panic!("Unsupported ace type {:?}", t), + }; + chunk::Ace { + platform: AcePlatform::Windows, + flags: { + let mut flags = chunk::Flag::empty(); + for (f, g) in FLAGS_MAPPING_TABLE { + if self.flags & (g.0 as u8) != 0 { + flags.insert(f); + } + } + flags + }, + owner_type: match self.sid.ty { + SidType::User + | SidType::Alias + | SidType::Domain + | SidType::DeletedAccount + | SidType::Invalid + | SidType::Computer + | SidType::Label + | SidType::LogonSession + | SidType::Unknown(_) => OwnerType::User(Identifier(self.sid.name)), + SidType::Group | SidType::WellKnownGroup => { + OwnerType::Group(Identifier(self.sid.name)) + } + }, + allow, + permission: { + let mut permission = chunk::Permission::empty(); + for (p, rights) in PERMISSION_MAPPING_TABLE { + if self.mask & rights.0 != 0 { + permission.insert(p); + } + } + permission + }, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::chunk::Ace; + use windows::Win32::System::WindowsProgramming::GetUserNameW; + + pub fn get_current_username() -> io::Result { + let mut username_len = 0u32; + match unsafe { GetUserNameW(PWSTR::null(), &mut username_len as _) } { + Ok(_) => Err(io::Error::other("failed to get current username")), + Err(e) if e.code() == ERROR_INSUFFICIENT_BUFFER.to_hresult() => Ok(()), + Err(e) => Err(io::Error::other(e)), + }?; + let mut username = Vec::::with_capacity(username_len as usize); + let str = PWSTR::from_raw(username.as_mut_ptr()); + unsafe { GetUserNameW(str, &mut username_len as _) }.map_err(io::Error::other)?; + unsafe { str.to_string().map_err(io::Error::other) } + } + + #[test] + fn null_sid() { + Sid::null_sid(); + } + + #[test] + fn current_user() { + let username = get_current_username().unwrap(); + let sid = Sid::try_from_name(&username, None).unwrap(); + let string_sid = sid.to_string(); + let s = Sid::from_str(&string_sid).unwrap(); + assert_eq!(sid, s); + assert_eq!(username, s.name); + assert_eq!(SidType::User, s.ty); + } + + #[test] + fn username() { + let username = get_current_username().unwrap(); + let sid = Sid::try_from_name(&username, None).unwrap(); + assert_eq!(username, sid.name); + } + + #[test] + fn acl_for_everyone() { + let path = "everyone.txt"; + std::fs::write(&path, "everyone").unwrap(); + let sid = Sid::try_from_name("Everyone", None).unwrap(); + + set_facl( + &path, + vec![Ace { + platform: AcePlatform::General, + flags: chunk::Flag::empty(), + owner_type: OwnerType::Group(Identifier(sid.name.clone())), + allow: true, + permission: chunk::Permission::READ + | chunk::Permission::WRITE + | chunk::Permission::EXECUTE, + }], + ) + .unwrap(); + let acl = get_facl(&path).unwrap(); + assert_eq!(acl.len(), 1); + + assert_eq!( + &acl[0], + &Ace { + platform: AcePlatform::Windows, + flags: chunk::Flag::empty(), + owner_type: OwnerType::Group(Identifier(sid.name)), + allow: true, + permission: chunk::Permission::READ + | chunk::Permission::WRITE + | chunk::Permission::EXECUTE + | chunk::Permission::DELETE + | chunk::Permission::APPEND + | chunk::Permission::READATTR + | chunk::Permission::WRITEATTR + | chunk::Permission::READEXTATTR + | chunk::Permission::WRITEEXTATTR + | chunk::Permission::READSECURITY + | chunk::Permission::WRITESECURITY + | chunk::Permission::SYNC + | chunk::Permission::READ_DATA + | chunk::Permission::WRITE_DATA, + } + ); + } + + #[test] + fn get_acl() { + let path = "default.txt"; + std::fs::write(&path, "default").unwrap(); + let acl = get_facl(&path).unwrap(); + assert_ne!(acl.len(), 0); + } +} diff --git a/cli/src/utils/fs.rs b/cli/src/utils/fs.rs index 8f57508d..8347dda9 100644 --- a/cli/src/utils/fs.rs +++ b/cli/src/utils/fs.rs @@ -57,7 +57,7 @@ pub(crate) fn mv, Dist: AsRef>(src: Src, dist: Dist) -> i } #[cfg(windows)] -fn encode_wide(s: &std::ffi::OsStr) -> io::Result> { +pub(crate) fn encode_wide(s: &std::ffi::OsStr) -> io::Result> { use std::os::windows::prelude::*; let mut buf = Vec::with_capacity(s.len() + 1); buf.extend(s.encode_wide()); diff --git a/cli/tests/restore_acl.rs b/cli/tests/restore_acl.rs new file mode 100644 index 00000000..d426dcc7 --- /dev/null +++ b/cli/tests/restore_acl.rs @@ -0,0 +1,67 @@ +#![cfg(feature = "acl")] +use clap::Parser; +use portable_network_archive::{cli, command}; + +#[test] +fn extract_windows_acl() { + command::entry(cli::Cli::parse_from([ + "pna", + "--quiet", + "x", + "../resources/test/windows_acl.pna", + "--overwrite", + "--out-dir", + &format!("{}/windows_acl/", env!("CARGO_TARGET_TMPDIR")), + "--keep-acl", + "--unstable", + ])) + .unwrap(); +} + +#[test] +fn extract_linux_acl() { + command::entry(cli::Cli::parse_from([ + "pna", + "--quiet", + "x", + "../resources/test/linux_acl.pna", + "--overwrite", + "--out-dir", + &format!("{}/linux_acl/", env!("CARGO_TARGET_TMPDIR")), + "--keep-acl", + "--unstable", + ])) + .unwrap(); +} + +#[test] +fn extract_macos_acl() { + command::entry(cli::Cli::parse_from([ + "pna", + "--quiet", + "x", + "../resources/test/macos_acl.pna", + "--overwrite", + "--out-dir", + &format!("{}/macos_acl/", env!("CARGO_TARGET_TMPDIR")), + "--keep-acl", + "--unstable", + ])) + .unwrap(); +} + +#[test] +fn extract_freebsd_acl() { + command::entry(cli::Cli::parse_from([ + "pna", + "--quiet", + "x", + "../resources/test/freebsd_acl.pna", + "--overwrite", + "--out-dir", + &format!("{}/freebsd_acl/", env!("CARGO_TARGET_TMPDIR")), + "--keep-acl", + "--unstable", + ])) + .unwrap(); +} diff --git a/resources/test/freebsd_acl.pna b/resources/test/freebsd_acl.pna new file mode 100644 index 00000000..4e41d939 Binary files /dev/null and b/resources/test/freebsd_acl.pna differ diff --git a/resources/test/linux_acl.pna b/resources/test/linux_acl.pna new file mode 100644 index 00000000..75d48987 Binary files /dev/null and b/resources/test/linux_acl.pna differ diff --git a/resources/test/macos_acl.pna b/resources/test/macos_acl.pna new file mode 100644 index 00000000..596bf058 Binary files /dev/null and b/resources/test/macos_acl.pna differ diff --git a/resources/test/windows_acl.pna b/resources/test/windows_acl.pna new file mode 100644 index 00000000..4ffeb9c7 Binary files /dev/null and b/resources/test/windows_acl.pna differ