diff options
author | Ben Bridle <ben@derelict.engineering> | 2024-08-07 17:09:14 +1200 |
---|---|---|
committer | Ben Bridle <ben@derelict.engineering> | 2024-08-07 17:09:14 +1200 |
commit | 38d40a2c5d4b553f524d87755b8e2e0e47928b8a (patch) | |
tree | 01fd01820be4219ca9f3dc7ad6e61eb183ade963 /src/devices/file | |
parent | 65b53003e8de9543ba25a3b3d3cace399b92dc1d (diff) | |
download | bedrock-pc-38d40a2c5d4b553f524d87755b8e2e0e47928b8a.zip |
Refactor the file device
This is the Windows side of the refactoring job. The windows crate has
been added as a dependency in order to get a list of available drives
by drive letter, and a virtual top-level root directory has been
implemented in the Windows code to make it possible for programs to
hierarchically navigate between available drives.
Diffstat (limited to 'src/devices/file')
-rw-r--r-- | src/devices/file/bedrock_file_path.rs | 165 | ||||
-rw-r--r-- | src/devices/file/directory_child.rs | 35 | ||||
-rw-r--r-- | src/devices/file/directory_listing.rs | 76 |
3 files changed, 170 insertions, 106 deletions
diff --git a/src/devices/file/bedrock_file_path.rs b/src/devices/file/bedrock_file_path.rs index c169a62..e083853 100644 --- a/src/devices/file/bedrock_file_path.rs +++ b/src/devices/file/bedrock_file_path.rs @@ -3,17 +3,13 @@ use super::*; use std::cmp::Ordering; use std::ffi::OsString; -#[cfg(target_family = "unix")] -use std::os::unix::ffi::OsStringExt; -#[cfg(target_family = "windows")] -use std::os::windows::ffi::OsStringExt; - #[derive(Clone)] pub struct BedrockFilePath { base: PathBuf, relative: PathBuf, bytes: Vec<u8>, + entry_type: Option<EntryType>, } impl BedrockFilePath { @@ -21,8 +17,9 @@ impl BedrockFilePath { let base = base.to_path_buf(); let relative = buffer_to_path(buffer)?; let bytes = path_to_bytes(&relative)?; - assert_path_is_safe(&relative)?; - Some(Self { base, relative, bytes }) + let entry_type = get_entry_type(base.join(&relative)); + assert_path_is_safe(&relative, &base)?; + Some(Self { base, relative, bytes, entry_type }) } /// Construct an instance from an absolute path and a prefix of that path. @@ -30,8 +27,9 @@ impl BedrockFilePath { let base = base.to_path_buf(); let relative = path.strip_prefix(&base).ok()?.to_path_buf(); let bytes = path_to_bytes(&relative)?; - assert_path_is_safe(&relative)?; - Some( Self { base, relative, bytes } ) + let entry_type = get_entry_type(base.join(&relative)); + assert_path_is_safe(&relative, &base)?; + Some( Self { base, relative, bytes, entry_type } ) } /// Get the base path used by this path. @@ -59,32 +57,60 @@ impl BedrockFilePath { self.base.join(&self.relative) } - /// Get a path which represents the parent of this path. - pub fn parent(&self) -> Option<Self> { - let relative = self.relative.parent()?.to_path_buf(); - let base = self.base.clone(); - let bytes = path_to_bytes(&relative)?; - Some( Self { base, relative, bytes } ) + /// Get the entry type of this path. + pub fn entry_type(&self) -> Option<EntryType> { + self.entry_type } - pub fn directory_listing(&self) -> Option<DirectoryListing> { - DirectoryListing::from_path(&self) + /// Get a path which represents the parent of this path. + pub fn parent(&self) -> Option<Self> { + #[cfg(target_family = "unix")] { + Self::from_path(self.relative.parent()?, &self.base) + } + #[cfg(target_family = "windows")] { + if self.base.components().count() != 0 { + // Sandboxed path, cannot ascend to a virtual root directory. + Self::from_path(self.relative.parent()?, &self.base) + } else { + // Unsandboxed path, we can ascend to a virtual root directory. + match self.relative.parent() { + // Ascend to concrete parent directory. + Some(parent) => Self::from_path(parent, &self.base), + // Ascend into a virtual root directory. + None => { + if self.relative.components().count() != 0 { + // Ascend from concrete path to virtual root. + let blank = PathBuf::from(""); + BedrockFilePath::from_path(&blank, &blank) + } else { + // Cannot ascend above the virtual root. + None + } + }, + } + } + } } - /// Returns true if a dot character directly follows the final - /// forward-slash character in the relative path. - pub fn filename_is_dot_prefixed(&self) -> bool { - let bytes = self.as_bytes(); - let mut dot_i = 0; - for (i, b) in bytes.iter().enumerate() { - if *b == b'/' { - // Guaranteed to be a valid index, bytes is null-terminated. - dot_i = i + 1; - } else if *b == 0x00 { - break; + /// Returns true if the file would be hidden by the default file browser. + pub fn is_hidden(&self) -> bool { + #[cfg(target_family = "unix")] { + if let Some(stem) = self.relative.file_stem() { + if let Some(string) = stem.to_str() { + return string.starts_with('.'); + } + } + } + #[cfg(target_family = "windows")] { + use std::os::windows::fs::MetadataExt; + // See https://learn.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants + // const FILE_ATTRIBUTE_HIDDEN: u32 = 0x00000002; + // const FILE_ATTRIBUTE_NOT_CONTENT_INDEXED: u32 = 0x00002000; + if let Ok(metadata) = std::fs::metadata(self.as_path()) { + return metadata.file_attributes() & 0x2002 != 0; } } - return bytes[dot_i] == b'.'; + return false; } } @@ -108,11 +134,19 @@ fn buffer_to_path(bytes: [u8; 256]) -> Option<PathBuf> { let slice = &bytes[1..null_i?]; #[cfg(target_family = "unix")] { + use std::os::unix::ffi::OsStringExt; let vec = Vec::from(slice); return Some(OsString::from_vec(vec).into()) } #[cfg(target_family = "windows")] { - let string = String::from_utf8_lossy(slice); + use std::os::windows::ffi::OsStringExt; + let mut string = String::from_utf8_lossy(slice).to_string(); + // Convert drive-current-directory paths to drive-root paths. This is + // needed because the paths C: and C:/ point to separate directories, + // but trailing forward-slashes are optional in Bedrock. + if string.ends_with(':') { + string.push('/'); + } let utf16: Vec<u16> = string.replace(r"/", r"\").encode_utf16().collect(); return Some(OsString::from_wide(&utf16).into()) } @@ -122,14 +156,15 @@ fn buffer_to_path(bytes: [u8; 256]) -> Option<PathBuf> { /// /// A byte path contains at most 255 bytes, and is not null-terminated. fn path_to_bytes(path: &Path) -> Option<Vec<u8>> { - let mut string = String::from("/"); #[cfg(target_family = "unix")] - string.push_str(path.as_os_str().to_str()?); + let string = path.as_os_str().to_str()?.to_string(); #[cfg(target_family = "windows")] - string.push_str(&path.as_os_str().to_str()?.replace(r"\", r"/")); + let string = path.as_os_str().to_str()?.replace(r"\", r"/"); - // Remove all trailing forward-slash characters. - let slice = string.trim_end_matches('/').as_bytes(); + // Remove any trailing forward-slash and add a leading forward-slash. + let mut prefixed_string = String::from("/"); + prefixed_string.push_str(string.trim_end_matches('/')); + let slice = prefixed_string.as_bytes(); // Error if bytes does not fit into a CircularPathBuffer. if slice.len() > 255 { return None; } @@ -137,24 +172,63 @@ fn path_to_bytes(path: &Path) -> Option<Vec<u8>> { Some(Vec::from(slice)) } -/// Returns true if a path contains only normal components. -fn assert_path_is_safe(relative: &Path) -> Option<()> { - // Error if path contains special components. - for component in relative.components() { - match component { - Component::Normal(_) => continue, - _ => return None, +/// Returns true if a relative path can be safely attached to a base without +/// breaking out of the sandbox. +fn assert_path_is_safe(relative: &Path, base: &Path) -> Option<()> { + #[cfg(target_family = "unix")] { + // Error if path contains special components. + for component in relative.components() { + match component { + Component::Normal(_) => continue, + _ => return None, + } + } + } + #[cfg(target_family = "windows")] { + // If the base path is empty, the relative path needs to be able to + // contain the prefix and root element. If the base path is not + // empty, the relative path must not contain these elements else + // they will override the base path when joined. + if base.components().count() != 0 { + for component in relative.components() { + match component { + Component::Normal(_) => continue, + _ => return None, + } + } } } return Some(()); } +fn get_entry_type(absolute: PathBuf) -> Option<EntryType> { + #[cfg(target_family = "windows")] { + // If path is empty, this is a virtual root directory. + if absolute.components().count() == 0 { + return Some(EntryType::Directory) + } + } + let metadata = std::fs::metadata(absolute).ok()?; + if metadata.is_file() { + Some(EntryType::File) + } else if metadata.is_dir() { + Some(EntryType::Directory) + } else { + None + } +} + +impl std::fmt::Debug for BedrockFilePath { + fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + self.as_path().fmt(f) + } +} // --------------------------------------------------------------------------- impl PartialEq for BedrockFilePath { fn eq(&self, other: &Self) -> bool { - self.bytes == other.bytes + self.bytes == other.bytes && self.entry_type == other.entry_type } } @@ -168,7 +242,10 @@ impl PartialOrd for BedrockFilePath { impl Ord for BedrockFilePath { fn cmp(&self, other: &Self) -> Ordering { - compare_ascii_slices(&self.bytes, &other.bytes) + match self.entry_type.cmp(&other.entry_type) { + Ordering::Equal => compare_ascii_slices(&self.bytes, &other.bytes), + ordering => ordering, + } } } diff --git a/src/devices/file/directory_child.rs b/src/devices/file/directory_child.rs deleted file mode 100644 index 376ec7d..0000000 --- a/src/devices/file/directory_child.rs +++ /dev/null @@ -1,35 +0,0 @@ -use super::*; - -use std::cmp::Ordering; - - -pub struct DirectoryChild { - pub path: BedrockFilePath, - pub entry_type: EntryType, -} - - -// --------------------------------------------------------------------------- - -impl PartialEq for DirectoryChild { - fn eq(&self, other: &Self) -> bool { - self.entry_type == other.entry_type && self.path == other.path - } -} - -impl Eq for DirectoryChild {} - -impl PartialOrd for DirectoryChild { - fn partial_cmp(&self, other: &Self) -> Option<Ordering> { - Some(self.cmp(other)) - } -} - -impl Ord for DirectoryChild { - fn cmp(&self, other: &Self) -> Ordering { - match self.entry_type.cmp(&other.entry_type) { - Ordering::Equal => self.path.cmp(&other.path), - ordering => ordering, - } - } -} diff --git a/src/devices/file/directory_listing.rs b/src/devices/file/directory_listing.rs index 0cbbde9..febc5c2 100644 --- a/src/devices/file/directory_listing.rs +++ b/src/devices/file/directory_listing.rs @@ -2,12 +2,13 @@ use super::*; pub struct DirectoryListing { - children: Vec<DirectoryChild>, + children: Vec<BedrockFilePath>, length: u32, selected: Option<u32>, name_buffer: CircularPathBuffer, } + impl DirectoryListing { pub fn from_path(path: &BedrockFilePath) -> Option<Self> { macro_rules! unres { @@ -17,32 +18,29 @@ impl DirectoryListing { ($option:expr) => { match $option { Some(v) => v, None => continue} }; } - let mut children = Vec::new(); - let dir_listing = std::fs::read_dir(path.as_path()).ok()?; - for (i, entry_result) in dir_listing.enumerate() { - // Firebreak to prevent emulator from consuming an absurd amount - // of memory when opening too large of a directory. - if i == (u16::MAX as usize) { - break; + #[cfg(target_family = "windows")] { + if path.as_path().components().count() == 0 { + return Some(Self::construct_virtual_root()) } + } - let entry = unres!(entry_result); - let entry_path = unopt!(BedrockFilePath::from_path(&entry.path(), path.base())); - if entry_path.filename_is_dot_prefixed() { - continue; - } - let metadata = unres!(std::fs::metadata(&entry.path())); - let entry_type = - if metadata.is_file() { - EntryType::File - } else if metadata.is_dir() { - EntryType::Directory - } else { + let mut children = Vec::new(); + if let Ok(dir_listing) = std::fs::read_dir(path.as_path()) { + for (i, entry_result) in dir_listing.enumerate() { + // Firebreak to prevent emulator from consuming an absurd amount + // of memory when opening too large of a directory. + if i == (u16::MAX as usize) { + break; + } + + let entry = unres!(entry_result); + let entry_path = unopt!(BedrockFilePath::from_path(&entry.path(), path.base())); + if entry_path.is_hidden() { continue; - }; + } - let child = DirectoryChild { path: entry_path, entry_type }; - children.push(child); + children.push(entry_path); + } } children.sort(); @@ -52,8 +50,32 @@ impl DirectoryListing { Some( Self { children, length, selected, name_buffer } ) } + /// Generate entries for a virtual root directory. + #[cfg(target_family = "windows")] + fn construct_virtual_root() -> Self { + let mut children = Vec::new(); + let base = PathBuf::from(""); + let drive_bits = unsafe { + windows::Win32::Storage::FileSystem::GetLogicalDrives() + }; + for i in 0..26 { + if drive_bits & (0x1 << i) != 0 { + let letter: char = (b'A' + i).into(); + let path = PathBuf::from(format!("{letter}:/")); + if let Some(drive) = BedrockFilePath::from_path(&path, &base) { + children.push(drive); + } + } + } + + let length = children.len() as u32; + let selected = None; + let name_buffer = CircularPathBuffer::new(); + Self { children, length, selected, name_buffer } + } + /// Attempts to return a directory child by index. - pub fn get(&self, index: u32) -> Option<&DirectoryChild> { + pub fn get(&self, index: u32) -> Option<&BedrockFilePath> { self.children.get(index as usize) } @@ -69,7 +91,7 @@ impl DirectoryListing { /// Attempts to select a child by index. pub fn set_selected(&mut self, index: u32) { if let Some(child) = self.get(index) { - let buffer = child.path.as_buffer(); + let buffer = child.as_buffer(); self.name_buffer.populate(buffer); self.selected = Some(index); } else { @@ -83,11 +105,11 @@ impl DirectoryListing { } pub fn child_type(&self) -> Option<EntryType> { - self.selected.and_then(|s| self.get(s).and_then(|i| Some(i.entry_type))) + self.selected.and_then(|s| self.get(s).and_then(|i| i.entry_type())) } pub fn child_path(&self) -> Option<BedrockFilePath> { - self.selected.and_then(|s| self.get(s).and_then(|i| Some(i.path.clone()))) + self.selected.and_then(|s| self.get(s).and_then(|i| Some(i.clone()))) } } |