Fingerprint a file without following its final symlink. `None` means deleted. Reject directories and paths outside the working tree.
(root: &Path, path: &str)
| 47 | /// Fingerprint a file without following its final symlink. `None` means deleted. |
| 48 | /// Reject directories and paths outside the working tree. |
| 49 | pub fn fingerprint(root: &Path, path: &str) -> Result<Option<String>, String> { |
| 50 | let full = checked_path(root, path)?; |
| 51 | let meta = match std::fs::symlink_metadata(&full) { |
| 52 | Ok(m) => m, |
| 53 | Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None), |
| 54 | Err(e) => return Err(e.to_string()), |
| 55 | }; |
| 56 | if meta.file_type().is_symlink() { |
| 57 | let target = std::fs::read_link(full).map_err(|e| e.to_string())?; |
| 58 | return Ok(Some(format!( |
| 59 | "link:{:x}", |
| 60 | Sha256::digest(target.as_os_str().as_encoded_bytes()) |
| 61 | ))); |
| 62 | } |
| 63 | if !meta.is_file() { |
| 64 | return Err(format!("not a regular scoped file: {path}")); |
| 65 | } |
| 66 | #[cfg(unix)] |
| 67 | let executable = { |
| 68 | use std::os::unix::fs::PermissionsExt; |
| 69 | meta.permissions().mode() & 0o111 != 0 |
| 70 | }; |
| 71 | #[cfg(not(unix))] |
| 72 | let executable = false; |
| 73 | let mut file = std::fs::File::open(full).map_err(|e| e.to_string())?; |
| 74 | let mut hash = Sha256::new(); |
| 75 | std::io::copy(&mut file, &mut hash).map_err(|e| e.to_string())?; |
| 76 | Ok(Some(format!( |
| 77 | "file:{hash:x}:{executable}", |
| 78 | hash = hash.finalize() |
| 79 | ))) |
| 80 | } |
| 81 | |
| 82 | /// Return fingerprints for dirty files and explicitly requested prior candidates. |
| 83 | /// The protocol version also lets plugins refuse an older, unscoped CLI. |