use std::path::Path;
use sha2::Digest;
#[cfg(windows)]
pub(crate) fn metadata_is_link_or_reparse(metadata: &std::fs::Metadata) -> bool {
use std::os::windows::fs::MetadataExt as _;
metadata.file_type().is_symlink() || metadata.file_attributes() & 0x0000_0400 != 0
}
#[cfg(not(windows))]
pub(crate) fn metadata_is_link_or_reparse(metadata: &std::fs::Metadata) -> bool {
metadata.file_type().is_symlink()
}
#[cfg(windows)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct WindowsFileIdentity {
pub(crate) volume: u32,
pub(crate) index: u64,
pub(crate) links: u32,
pub(crate) attributes: u32,
}
#[cfg(windows)]
pub(crate) fn windows_file_identity(file: &std::fs::File) -> std::io::Result<WindowsFileIdentity> {
use std::os::windows::io::AsRawHandle as _;
use windows::Win32::Foundation::HANDLE;
use windows::Win32::Storage::FileSystem::{
BY_HANDLE_FILE_INFORMATION, GetFileInformationByHandle,
};
let mut information = BY_HANDLE_FILE_INFORMATION::default();
unsafe { GetFileInformationByHandle(HANDLE(file.as_raw_handle()), &mut information) }
.map_err(std::io::Error::other)?;
Ok(WindowsFileIdentity {
volume: information.dwVolumeSerialNumber,
index: (u64::from(information.nFileIndexHigh) << 32) | u64::from(information.nFileIndexLow),
links: information.nNumberOfLinks,
attributes: information.dwFileAttributes,
})
}
pub(crate) fn hash_os_path(hasher: &mut impl Digest, domain: &'static [u8], path: &Path) {
hasher.update(b"codewhale-os-path-v1\0");
hasher.update((domain.len() as u64).to_le_bytes());
hasher.update(domain);
#[cfg(unix)]
{
use std::os::unix::ffi::OsStrExt as _;
let bytes = path.as_os_str().as_bytes();
hasher.update(b"unix-bytes\0");
hasher.update((bytes.len() as u64).to_le_bytes());
hasher.update(bytes);
}
#[cfg(windows)]
{
use std::os::windows::ffi::OsStrExt as _;
let units = path.as_os_str().encode_wide().collect::<Vec<_>>();
hasher.update(b"windows-utf16le\0");
hasher.update((units.len() as u64).to_le_bytes());
for unit in units {
hasher.update(unit.to_le_bytes());
}
}
#[cfg(all(not(unix), not(windows)))]
{
let bytes = path.as_os_str().as_encoded_bytes();
hasher.update(b"rust-osstr-encoded\0");
hasher.update((bytes.len() as u64).to_le_bytes());
hasher.update(bytes);
}
}
#[cfg(test)]
mod tests {
use super::*;
use sha2::Sha256;
fn digest(path: &Path) -> Vec<u8> {
let mut hasher = Sha256::new();
hash_os_path(&mut hasher, b"test-domain", path);
hasher.finalize().to_vec()
}
#[cfg(windows)]
#[test]
fn junctions_are_reparse_points_even_when_not_symbolic_links() {
let directory = tempfile::tempdir().unwrap();
let target = directory.path().join("target");
let junction = directory.path().join("junction");
std::fs::create_dir(&target).unwrap();
let output = std::process::Command::new("cmd")
.args(["/C", "mklink", "/J"])
.arg(&junction)
.arg(&target)
.output()
.expect("invoke Windows junction creation");
assert!(
output.status.success(),
"failed to create junction: {}",
String::from_utf8_lossy(&output.stderr)
);
let metadata = std::fs::symlink_metadata(&junction).unwrap();
assert!(metadata_is_link_or_reparse(&metadata));
}
#[cfg(unix)]
#[test]
fn invalid_unicode_paths_do_not_collapse_to_replacement_text() {
use std::ffi::OsString;
use std::os::unix::ffi::OsStringExt as _;
let first = OsString::from_vec(vec![b'a', 0xff]);
let second = OsString::from_vec(vec![b'a', 0xfe]);
assert_eq!(first.to_string_lossy(), second.to_string_lossy());
assert_ne!(digest(Path::new(&first)), digest(Path::new(&second)));
}
#[cfg(windows)]
#[test]
fn unpaired_utf16_paths_do_not_collapse_to_replacement_text() {
use std::ffi::OsString;
use std::os::windows::ffi::OsStringExt as _;
let first = OsString::from_wide(&[b'a' as u16, 0xd800]);
let second = OsString::from_wide(&[b'a' as u16, 0xd801]);
assert_eq!(first.to_string_lossy(), second.to_string_lossy());
assert_ne!(digest(Path::new(&first)), digest(Path::new(&second)));
}
}