use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct ExternalDataInfo {
pub location: PathBuf,
pub offset: u64,
pub length: Option<u64>,
#[allow(dead_code)]
pub checksum: Option<String>,
}
impl ExternalDataInfo {
pub fn from_proto_entries<'a>(
entries: impl Iterator<Item = (&'a str, &'a str)>,
) -> Result<Self, String> {
let mut location: Option<PathBuf> = None;
let mut offset: u64 = 0;
let mut length: Option<u64> = None;
let mut checksum: Option<String> = None;
for (key, value) in entries {
match key {
"location" => {
location = Some(PathBuf::from(value));
}
"offset" => {
offset = value
.parse()
.map_err(|e| format!("Invalid offset '{}': {}", value, e))?;
}
"length" => {
length = Some(
value
.parse()
.map_err(|e| format!("Invalid length '{}': {}", value, e))?,
);
}
"checksum" => {
checksum = Some(value.to_string());
}
_ => {
log::debug!("Ignoring unknown external_data key: {}", key);
}
}
}
let location = location.ok_or("Missing required 'location' in external_data")?;
Ok(ExternalDataInfo {
location,
offset,
length,
checksum,
})
}
pub fn resolve_path(&self, base_path: &Path) -> Result<PathBuf, String> {
let path_str = self.location.to_string_lossy();
if path_str.contains('\0') {
return Err(format!(
"Security error: null bytes not allowed in external_data location: {:?}",
self.location
));
}
if self.location.is_absolute() {
return Err(format!(
"Security error: absolute paths not allowed in external_data location: {:?}",
self.location
));
}
for component in self.location.components() {
if matches!(component, std::path::Component::ParentDir) {
return Err(format!(
"Security error: parent directory references ('..') not allowed in external_data location: {:?}",
self.location
));
}
}
let resolved = base_path.join(&self.location);
if let (Ok(canonical_base), Ok(canonical_resolved)) =
(base_path.canonicalize(), resolved.canonicalize())
&& !canonical_resolved.starts_with(&canonical_base)
{
return Err(format!(
"Security error: external_data path escapes base directory: {:?}",
self.location
));
}
Ok(resolved)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_minimal() {
let entries = vec![("location", "weights.bin")];
let info = ExternalDataInfo::from_proto_entries(entries.into_iter()).unwrap();
assert_eq!(info.location, PathBuf::from("weights.bin"));
assert_eq!(info.offset, 0);
assert_eq!(info.length, None);
assert_eq!(info.checksum, None);
}
#[test]
fn test_parse_full() {
let entries = vec![
("location", "model_weights.bin"),
("offset", "4096"),
("length", "1048576"),
("checksum", "abc123"),
];
let info = ExternalDataInfo::from_proto_entries(entries.into_iter()).unwrap();
assert_eq!(info.location, PathBuf::from("model_weights.bin"));
assert_eq!(info.offset, 4096);
assert_eq!(info.length, Some(1048576));
assert_eq!(info.checksum, Some("abc123".to_string()));
}
#[test]
fn test_missing_location() {
let entries = vec![("offset", "0")];
let result = ExternalDataInfo::from_proto_entries(entries.into_iter());
assert!(result.is_err());
assert!(result.unwrap_err().contains("location"));
}
#[test]
fn test_invalid_offset() {
let entries = vec![("location", "weights.bin"), ("offset", "not_a_number")];
let result = ExternalDataInfo::from_proto_entries(entries.into_iter());
assert!(result.is_err());
}
#[test]
fn test_resolve_path() {
let info = ExternalDataInfo {
location: PathBuf::from("weights/layer1.bin"),
offset: 0,
length: None,
checksum: None,
};
let resolved = info.resolve_path(Path::new("/models/bert")).unwrap();
assert_eq!(resolved, PathBuf::from("/models/bert/weights/layer1.bin"));
}
#[test]
#[cfg(unix)]
fn test_reject_absolute_path() {
let info = ExternalDataInfo {
location: PathBuf::from("/etc/passwd"),
offset: 0,
length: None,
checksum: None,
};
let result = info.resolve_path(Path::new("/models/bert"));
assert!(result.is_err());
assert!(result.unwrap_err().contains("absolute paths not allowed"));
}
#[test]
#[cfg(windows)]
fn test_reject_absolute_path() {
let info = ExternalDataInfo {
location: PathBuf::from("C:\\Windows\\System32\\config\\SAM"),
offset: 0,
length: None,
checksum: None,
};
let result = info.resolve_path(Path::new("C:\\models\\bert"));
assert!(result.is_err());
assert!(result.unwrap_err().contains("absolute paths not allowed"));
}
#[test]
fn test_reject_parent_traversal() {
let info = ExternalDataInfo {
location: PathBuf::from("../../../etc/passwd"),
offset: 0,
length: None,
checksum: None,
};
let result = info.resolve_path(Path::new("/models/bert"));
assert!(result.is_err());
assert!(result.unwrap_err().contains("parent directory references"));
}
#[test]
fn test_reject_hidden_traversal() {
let info = ExternalDataInfo {
location: PathBuf::from("weights/../../../etc/passwd"),
offset: 0,
length: None,
checksum: None,
};
let result = info.resolve_path(Path::new("/models/bert"));
assert!(result.is_err());
assert!(result.unwrap_err().contains("parent directory references"));
}
#[test]
#[cfg(unix)]
fn test_reject_null_bytes() {
use std::ffi::OsStr;
use std::os::unix::ffi::OsStrExt;
let path_with_null = OsStr::from_bytes(b"weights.bin\x00.txt");
let info = ExternalDataInfo {
location: PathBuf::from(path_with_null),
offset: 0,
length: None,
checksum: None,
};
let result = info.resolve_path(Path::new("/models/bert"));
assert!(result.is_err());
assert!(result.unwrap_err().contains("null bytes not allowed"));
}
}