aboutsummaryrefslogtreecommitdiff
path: root/crates/fparkan-path/src/lib.rs
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-06-30 00:40:47 +0300
committerValentin Popov <valentin@popov.link>2026-06-30 00:40:47 +0300
commit146446d3e2679fc79a46603f0008e3deab1ac6b5 (patch)
tree7a88ebf05438c8e74fc619e71fbc992d54f923ad /crates/fparkan-path/src/lib.rs
parent7db0bf8e3d0c6334040a5aed569c62e8a216b1a8 (diff)
downloadfparkan-146446d3e2679fc79a46603f0008e3deab1ac6b5.tar.xz
fparkan-146446d3e2679fc79a46603f0008e3deab1ac6b5.zip
fix: harden stage1 path and archive identity
Diffstat (limited to 'crates/fparkan-path/src/lib.rs')
-rw-r--r--crates/fparkan-path/src/lib.rs73
1 files changed, 67 insertions, 6 deletions
diff --git a/crates/fparkan-path/src/lib.rs b/crates/fparkan-path/src/lib.rs
index 2047f93..8051b0f 100644
--- a/crates/fparkan-path/src/lib.rs
+++ b/crates/fparkan-path/src/lib.rs
@@ -20,7 +20,9 @@
)]
//! Legacy path normalization and ASCII lookup semantics.
+use std::cmp::Ordering;
use std::fmt;
+use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf};
/// Original bytes.
@@ -42,23 +44,41 @@ impl OriginalPathBytes {
}
/// Normalized relative path.
-#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
+#[derive(Clone, Debug)]
pub struct NormalizedPath {
raw: Vec<u8>,
display: String,
}
impl NormalizedPath {
- /// Returns string view.
+ /// Returns normalized byte view used for identity, ordering, and hashing.
#[must_use]
- pub fn as_str(&self) -> &str {
- &self.display
+ pub fn identity_bytes(&self) -> &[u8] {
+ &self.raw
+ }
+
+ /// Returns an ASCII-only lookup key for case-insensitive archive matching.
+ #[must_use]
+ pub fn lookup_key(&self) -> LookupKey {
+ ascii_lookup_key(&self.raw)
}
/// Returns normalized byte view.
#[must_use]
pub fn as_bytes(&self) -> &[u8] {
- &self.raw
+ self.identity_bytes()
+ }
+
+ /// Returns a lossy display representation.
+ #[must_use]
+ pub fn display_lossy(&self) -> &str {
+ &self.display
+ }
+
+ /// Returns a lossy string view for UI and diagnostics only.
+ #[must_use]
+ pub fn as_str(&self) -> &str {
+ self.display_lossy()
}
/// Returns an OS path owned path buffer.
@@ -68,6 +88,32 @@ impl NormalizedPath {
}
}
+impl PartialEq for NormalizedPath {
+ fn eq(&self, other: &Self) -> bool {
+ self.raw == other.raw
+ }
+}
+
+impl Eq for NormalizedPath {}
+
+impl PartialOrd for NormalizedPath {
+ fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+ Some(self.cmp(other))
+ }
+}
+
+impl Ord for NormalizedPath {
+ fn cmp(&self, other: &Self) -> Ordering {
+ self.raw.cmp(&other.raw)
+ }
+}
+
+impl Hash for NormalizedPath {
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ self.raw.hash(state);
+ }
+}
+
/// Normalized path paired with its original byte image.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct NormalizedPathWithOriginal {
@@ -353,7 +399,8 @@ mod tests {
let path = normalize_relative(b"DATA/\xFF.bin", PathPolicy::HostCompatible)
.expect("raw legacy bytes");
- assert_eq!(path.as_str(), "DATA/\u{FFFD}.bin");
+ assert_eq!(path.display_lossy(), "DATA/\u{FFFD}.bin");
+ assert_eq!(path.identity_bytes(), b"DATA/\xFF.bin");
}
#[test]
@@ -364,4 +411,18 @@ mod tests {
assert_eq!(path.normalized().as_str(), "DATA/Maps/Intro/Land.msh");
assert_eq!(path.original().as_bytes(), raw);
}
+
+ #[test]
+ fn lossy_display_does_not_affect_identity_or_ordering() {
+ let first = normalize_relative(b"DATA/\xFF.bin", PathPolicy::HostCompatible)
+ .expect("first raw path");
+ let second = normalize_relative(b"DATA/\xFE.bin", PathPolicy::HostCompatible)
+ .expect("second raw path");
+
+ assert_eq!(first.display_lossy(), second.display_lossy());
+ assert_ne!(first, second);
+ assert_ne!(first.identity_bytes(), second.identity_bytes());
+ assert_ne!(first.cmp(&second), Ordering::Equal);
+ assert_ne!(first.lookup_key(), second.lookup_key());
+ }
}