diff --git a/CHANGELOG.md b/CHANGELOG.md index d192316..03610e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Shell support introduced for ssh & distant servers, including a new shell command for distant cli - Support for JSON communication of ssh auth during launch (cli) +- Add windows and unix metadata files to overall metadata response data ### Changed - Replace cbor library with alternative as old cbor lib has been abandoned diff --git a/Cargo.lock b/Cargo.lock index 7769c85..6d24bbf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -529,6 +529,7 @@ name = "distant-core" version = "0.16.0" dependencies = [ "assert_fs", + "bitflags", "bytes", "chacha20poly1305", "ciborium", diff --git a/distant-core/Cargo.toml b/distant-core/Cargo.toml index de56985..1114828 100644 --- a/distant-core/Cargo.toml +++ b/distant-core/Cargo.toml @@ -12,6 +12,7 @@ readme = "README.md" license = "MIT OR Apache-2.0" [dependencies] +bitflags = "1.3.2" bytes = "1.1.0" chacha20poly1305 = "0.9.0" ciborium = "0.2.0" diff --git a/distant-core/src/data.rs b/distant-core/src/data.rs index 34572d4..cddb882 100644 --- a/distant-core/src/data.rs +++ b/distant-core/src/data.rs @@ -1,3 +1,4 @@ +use bitflags::bitflags; use derive_more::{Display, Error, IsVariant}; use portable_pty::PtySize as PortablePtySize; use serde::{Deserialize, Serialize}; @@ -531,6 +532,274 @@ pub struct Metadata { #[serde(serialize_with = "serialize_u128_option")] #[serde(deserialize_with = "deserialize_u128_option")] pub modified: Option, + + /// Represents metadata that is specific to a unix remote machine + pub unix: Option, + + /// Represents metadata that is specific to a windows remote machine + pub windows: Option, +} + +/// Represents unix-specific metadata about some path on a remote machine +#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct UnixMetadata { + /// Represents whether or not owner can read from the file + pub owner_read: bool, + + /// Represents whether or not owner can write to the file + pub owner_write: bool, + + /// Represents whether or not owner can execute the file + pub owner_exec: bool, + + /// Represents whether or not associated group can read from the file + pub group_read: bool, + + /// Represents whether or not associated group can write to the file + pub group_write: bool, + + /// Represents whether or not associated group can execute the file + pub group_exec: bool, + + /// Represents whether or not other can read from the file + pub other_read: bool, + + /// Represents whether or not other can write to the file + pub other_write: bool, + + /// Represents whether or not other can execute the file + pub other_exec: bool, +} + +impl From for UnixMetadata { + /// Create from a unix mode bitset + fn from(mode: u32) -> Self { + let flags = UnixFilePermissionFlags::from_bits_truncate(mode); + Self { + owner_read: flags.contains(UnixFilePermissionFlags::OWNER_READ), + owner_write: flags.contains(UnixFilePermissionFlags::OWNER_WRITE), + owner_exec: flags.contains(UnixFilePermissionFlags::OWNER_EXEC), + group_read: flags.contains(UnixFilePermissionFlags::GROUP_READ), + group_write: flags.contains(UnixFilePermissionFlags::GROUP_WRITE), + group_exec: flags.contains(UnixFilePermissionFlags::GROUP_EXEC), + other_read: flags.contains(UnixFilePermissionFlags::OTHER_READ), + other_write: flags.contains(UnixFilePermissionFlags::OTHER_WRITE), + other_exec: flags.contains(UnixFilePermissionFlags::OTHER_EXEC), + } + } +} + +impl From for u32 { + /// Convert to a unix mode bitset + fn from(metadata: UnixMetadata) -> Self { + let mut flags = UnixFilePermissionFlags::empty(); + + if metadata.owner_read { + flags.insert(UnixFilePermissionFlags::OWNER_READ); + } + if metadata.owner_write { + flags.insert(UnixFilePermissionFlags::OWNER_WRITE); + } + if metadata.owner_exec { + flags.insert(UnixFilePermissionFlags::OWNER_EXEC); + } + + if metadata.group_read { + flags.insert(UnixFilePermissionFlags::GROUP_READ); + } + if metadata.group_write { + flags.insert(UnixFilePermissionFlags::GROUP_WRITE); + } + if metadata.group_exec { + flags.insert(UnixFilePermissionFlags::GROUP_EXEC); + } + + if metadata.other_read { + flags.insert(UnixFilePermissionFlags::OTHER_READ); + } + if metadata.other_write { + flags.insert(UnixFilePermissionFlags::OTHER_WRITE); + } + if metadata.other_exec { + flags.insert(UnixFilePermissionFlags::OTHER_EXEC); + } + + flags.bits + } +} + +impl UnixMetadata { + pub fn is_readonly(self) -> bool { + !(self.owner_read || self.group_read || self.other_read) + } +} + +bitflags! { + struct UnixFilePermissionFlags: u32 { + const OWNER_READ = 0o400; + const OWNER_WRITE = 0o200; + const OWNER_EXEC = 0o100; + const GROUP_READ = 0o40; + const GROUP_WRITE = 0o20; + const GROUP_EXEC = 0o10; + const OTHER_READ = 0o4; + const OTHER_WRITE = 0o2; + const OTHER_EXEC = 0o1; + } +} + +/// Represents windows-specific metadata about some path on a remote machine +#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct WindowsMetadata { + /// Represents whether or not a file or directory is an archive + pub archive: bool, + + /// Represents whether or not a file or directory is compressed + pub compressed: bool, + + /// Represents whether or not the file or directory is encrypted + pub encrypted: bool, + + /// Represents whether or not a file or directory is hidden + pub hidden: bool, + + /// Represents whether or not a directory or user data stream is configured with integrity + pub integrity_stream: bool, + + /// Represents whether or not a file does not have other attributes set + pub normal: bool, + + /// Represents whether or not a file or directory is not to be indexed by content indexing + /// service + pub not_content_indexed: bool, + + /// Represents whether or not a user data stream is not to be read by the background data + /// integrity scanner + pub no_scrub_data: bool, + + /// Represents whether or not the data of a file is not available immediately + pub offline: bool, + + /// Represents whether or not a file or directory is not fully present locally + pub recall_on_data_access: bool, + + /// Represents whether or not a file or directory has no physical representation on the local + /// system (is virtual) + pub recall_on_open: bool, + + /// Represents whether or not a file or directory has an associated reparse point, or a file is + /// a symbolic link + pub reparse_point: bool, + + /// Represents whether or not a file is a sparse file + pub sparse_file: bool, + + /// Represents whether or not a file or directory is used partially or exclusively by the + /// operating system + pub system: bool, + + /// Represents whether or not a file is being used for temporary storage + pub temporary: bool, +} + +impl From for WindowsMetadata { + /// Create from a windows file attribute bitset + fn from(file_attributes: u32) -> Self { + let flags = WindowsFileAttributeFlags::from_bits_truncate(file_attributes); + Self { + archive: flags.contains(WindowsFileAttributeFlags::ARCHIVE), + compressed: flags.contains(WindowsFileAttributeFlags::COMPRESSED), + encrypted: flags.contains(WindowsFileAttributeFlags::ENCRYPTED), + hidden: flags.contains(WindowsFileAttributeFlags::HIDDEN), + integrity_stream: flags.contains(WindowsFileAttributeFlags::INTEGRITY_SYSTEM), + normal: flags.contains(WindowsFileAttributeFlags::NORMAL), + not_content_indexed: flags.contains(WindowsFileAttributeFlags::NOT_CONTENT_INDEXED), + no_scrub_data: flags.contains(WindowsFileAttributeFlags::NO_SCRUB_DATA), + offline: flags.contains(WindowsFileAttributeFlags::OFFLINE), + recall_on_data_access: flags.contains(WindowsFileAttributeFlags::RECALL_ON_DATA_ACCESS), + recall_on_open: flags.contains(WindowsFileAttributeFlags::RECALL_ON_OPEN), + reparse_point: flags.contains(WindowsFileAttributeFlags::REPARSE_POINT), + sparse_file: flags.contains(WindowsFileAttributeFlags::SPARSE_FILE), + system: flags.contains(WindowsFileAttributeFlags::SYSTEM), + temporary: flags.contains(WindowsFileAttributeFlags::TEMPORARY), + } + } +} + +impl From for u32 { + /// Convert to a windows file attribute bitset + fn from(metadata: WindowsMetadata) -> Self { + let mut flags = WindowsFileAttributeFlags::empty(); + + if metadata.archive { + flags.insert(WindowsFileAttributeFlags::ARCHIVE); + } + if metadata.compressed { + flags.insert(WindowsFileAttributeFlags::COMPRESSED); + } + if metadata.encrypted { + flags.insert(WindowsFileAttributeFlags::ENCRYPTED); + } + if metadata.hidden { + flags.insert(WindowsFileAttributeFlags::HIDDEN); + } + if metadata.integrity_stream { + flags.insert(WindowsFileAttributeFlags::INTEGRITY_SYSTEM); + } + if metadata.normal { + flags.insert(WindowsFileAttributeFlags::NORMAL); + } + if metadata.not_content_indexed { + flags.insert(WindowsFileAttributeFlags::NOT_CONTENT_INDEXED); + } + if metadata.no_scrub_data { + flags.insert(WindowsFileAttributeFlags::NO_SCRUB_DATA); + } + if metadata.offline { + flags.insert(WindowsFileAttributeFlags::OFFLINE); + } + if metadata.recall_on_data_access { + flags.insert(WindowsFileAttributeFlags::RECALL_ON_DATA_ACCESS); + } + if metadata.recall_on_open { + flags.insert(WindowsFileAttributeFlags::RECALL_ON_OPEN); + } + if metadata.reparse_point { + flags.insert(WindowsFileAttributeFlags::REPARSE_POINT); + } + if metadata.sparse_file { + flags.insert(WindowsFileAttributeFlags::SPARSE_FILE); + } + if metadata.system { + flags.insert(WindowsFileAttributeFlags::SYSTEM); + } + if metadata.temporary { + flags.insert(WindowsFileAttributeFlags::TEMPORARY); + } + + flags.bits + } +} + +bitflags! { + struct WindowsFileAttributeFlags: u32 { + const ARCHIVE = 0x20; + const COMPRESSED = 0x800; + const ENCRYPTED = 0x4000; + const HIDDEN = 0x2; + const INTEGRITY_SYSTEM = 0x8000; + const NORMAL = 0x80; + const NOT_CONTENT_INDEXED = 0x2000; + const NO_SCRUB_DATA = 0x20000; + const OFFLINE = 0x1000; + const RECALL_ON_DATA_ACCESS = 0x400000; + const RECALL_ON_OPEN = 0x40000; + const REPARSE_POINT = 0x400; + const SPARSE_FILE = 0x200; + const SYSTEM = 0x4; + const TEMPORARY = 0x100; + const VIRTUAL = 0x10000; + } } pub(crate) fn deserialize_u128_option<'de, D>(deserializer: D) -> Result, D::Error> diff --git a/distant-core/src/server/distant/handler.rs b/distant-core/src/server/distant/handler.rs index fc2cb8a..e8fe34e 100644 --- a/distant-core/src/server/distant/handler.rs +++ b/distant-core/src/server/distant/handler.rs @@ -424,6 +424,24 @@ async fn metadata( } else { FileType::Symlink }, + + #[cfg(unix)] + unix: Some({ + use std::os::unix::prelude::*; + let mode = metadata.mode(); + crate::data::UnixMetadata::from(mode) + }), + #[cfg(not(unix))] + unix: None, + + #[cfg(windows)] + windows: Some({ + use std::os::windows::prelude::*; + let attributes = metadata.file_attributes(); + crate::data::WindowsMetadata::from(attributes) + }), + #[cfg(not(windows))] + windows: None, }))) } @@ -1991,6 +2009,74 @@ mod tests { ); } + #[cfg(unix)] + #[tokio::test] + async fn metadata_should_include_unix_specific_metadata_on_unix_platform() { + let (conn_id, state, tx, mut rx) = setup(1); + let temp = assert_fs::TempDir::new().unwrap(); + let file = temp.child("file"); + file.write_str("some text").unwrap(); + + let req = Request::new( + "test-tenant", + vec![RequestData::Metadata { + path: file.path().to_path_buf(), + canonicalize: false, + resolve_file_type: false, + }], + ); + + process(conn_id, state, req, tx).await.unwrap(); + + let res = rx.recv().await.unwrap(); + assert_eq!(res.payload.len(), 1, "Wrong payload size"); + + match &res.payload[0] { + ResponseData::Metadata(Metadata { unix, windows, .. }) => { + assert!(unix.is_some(), "Unexpectedly missing unix metadata on unix"); + assert!( + windows.is_none(), + "Unexpectedly got windows metadata on unix" + ); + } + x => panic!("Unexpected response: {:?}", x), + } + } + + #[cfg(windows)] + #[tokio::test] + async fn metadata_should_include_unix_specific_metadata_on_windows_platform() { + let (conn_id, state, tx, mut rx) = setup(1); + let temp = assert_fs::TempDir::new().unwrap(); + let file = temp.child("file"); + file.write_str("some text").unwrap(); + + let req = Request::new( + "test-tenant", + vec![RequestData::Metadata { + path: file.path().to_path_buf(), + canonicalize: false, + resolve_file_type: false, + }], + ); + + process(conn_id, state, req, tx).await.unwrap(); + + let res = rx.recv().await.unwrap(); + assert_eq!(res.payload.len(), 1, "Wrong payload size"); + + match &res.payload[0] { + ResponseData::Metadata(Metadata { unix, windows, .. }) => { + assert!( + windows.is_some(), + "Unexpectedly missing windows metadata on windows" + ); + assert!(unix.is_none(), "Unexpectedly got unix metadata on windows"); + } + x => panic!("Unexpected response: {:?}", x), + } + } + #[tokio::test] async fn metadata_should_send_back_metadata_on_dir_if_exists() { let (conn_id, state, tx, mut rx) = setup(1); diff --git a/distant-ssh2/src/handler.rs b/distant-ssh2/src/handler.rs index 72220a1..282bd22 100644 --- a/distant-ssh2/src/handler.rs +++ b/distant-ssh2/src/handler.rs @@ -4,7 +4,7 @@ use distant_core::{ data::{ DirEntry, Error as DistantError, FileType, Metadata, PtySize, RunningProcess, SystemInfo, }, - Request, RequestData, Response, ResponseData, + Request, RequestData, Response, ResponseData, UnixMetadata, }; use futures::future; use log::*; @@ -606,6 +606,18 @@ async fn metadata( accessed: metadata.accessed.map(u128::from), modified: metadata.modified.map(u128::from), created: None, + unix: metadata.permissions.as_ref().map(|p| UnixMetadata { + owner_read: p.owner_read, + owner_write: p.owner_write, + owner_exec: p.owner_exec, + group_read: p.group_read, + group_write: p.group_write, + group_exec: p.group_exec, + other_read: p.other_read, + other_write: p.other_write, + other_exec: p.other_exec, + }), + windows: None, }))) } diff --git a/src/output.rs b/src/output.rs index e7d5273..db5ade4 100644 --- a/src/output.rs +++ b/src/output.rs @@ -142,6 +142,8 @@ fn format_shell(data: ResponseData) -> ResponseOut { accessed, created, modified, + unix, + windows, }) => ResponseOut::StdoutLine( format!( concat!( @@ -151,7 +153,10 @@ fn format_shell(data: ResponseData) -> ResponseOut { "Readonly: {}\n", "Created: {}\n", "Last Accessed: {}\n", - "Last Modified: {}", + "Last Modified: {}\n", + "{}", + "{}", + "{}", ), canonicalized_path .map(|p| format!("Canonicalized Path: {:?}\n", p)) @@ -162,6 +167,70 @@ fn format_shell(data: ResponseData) -> ResponseOut { created.unwrap_or_default(), accessed.unwrap_or_default(), modified.unwrap_or_default(), + unix.map(|u| format!( + concat!( + "Owner Read: {}\n", + "Owner Write: {}\n", + "Owner Exec: {}\n", + "Group Read: {}\n", + "Group Write: {}\n", + "Group Exec: {}\n", + "Other Read: {}\n", + "Other Write: {}\n", + "Other Exec: {}", + ), + u.owner_read, + u.owner_write, + u.owner_exec, + u.group_read, + u.group_write, + u.group_exec, + u.other_read, + u.other_write, + u.other_exec + )) + .unwrap_or_default(), + windows + .map(|w| format!( + concat!( + "Archive: {}\n", + "Compressed: {}\n", + "Encrypted: {}\n", + "Hidden: {}\n", + "Integrity Stream: {}\n", + "Normal: {}\n", + "Not Content Indexed: {}\n", + "No Scrub Data: {}\n", + "Offline: {}\n", + "Recall on Data Access: {}\n", + "Recall on Open: {}\n", + "Reparse Point: {}\n", + "Sparse File: {}\n", + "System: {}\n", + "Temporary: {}", + ), + w.archive, + w.compressed, + w.encrypted, + w.hidden, + w.integrity_stream, + w.normal, + w.not_content_indexed, + w.no_scrub_data, + w.offline, + w.recall_on_data_access, + w.recall_on_open, + w.reparse_point, + w.sparse_file, + w.system, + w.temporary, + )) + .unwrap_or_default(), + if unix.is_none() && windows.is_none() { + String::from("\n") + } else { + String::new() + } ) .into_bytes(), ),