use std::{fmt, sync::Arc};
use super::{ByteSource, Error, Result};
pub type ByteSourceHandle = Arc<dyn ByteSource>;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
pub struct RelatedPathBuf {
components: Vec<RelatedPathComponent>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum RelatedPathComponent {
Current,
Parent,
Normal(String),
}
impl RelatedPathBuf {
pub fn new() -> Self {
Self::default()
}
pub fn from_relative_path(path: &str) -> Result<Self> {
if path.is_empty() {
return Ok(Self::new());
}
if path.starts_with('/') || path.starts_with('\\') {
return Err(Error::invalid_source_reference(format!(
"related path must be relative: {path}"
)));
}
let mut parsed = Self::new();
for component in path.split(['/', '\\']) {
if component.is_empty() {
return Err(Error::invalid_source_reference(format!(
"related path contains an empty component: {path}"
)));
}
match component {
"." => {
parsed.push_current();
}
".." => {
parsed.push_parent();
}
normal => {
parsed.push_normal(normal)?;
}
}
}
Ok(parsed)
}
pub fn push_current(&mut self) -> &mut Self {
self.components.push(RelatedPathComponent::Current);
self
}
pub fn push_parent(&mut self) -> &mut Self {
self.components.push(RelatedPathComponent::Parent);
self
}
pub fn push_normal(&mut self, component: impl Into<String>) -> Result<&mut Self> {
let component = component.into();
validate_normal_component(&component)?;
self
.components
.push(RelatedPathComponent::Normal(component));
Ok(self)
}
pub fn is_empty(&self) -> bool {
self.components.is_empty()
}
pub fn file_name(&self) -> Option<&str> {
self
.components
.last()
.and_then(RelatedPathComponent::normal_component)
}
pub fn components(&self) -> impl Iterator<Item = &str> {
self.components.iter().map(RelatedPathComponent::as_str)
}
pub fn join(&self, other: &Self) -> Self {
let mut joined = self.clone();
joined.components.extend(other.components.iter().cloned());
joined
}
pub fn parent(&self) -> Option<Self> {
if self.components.is_empty() {
return None;
}
let mut parent = self.clone();
parent.components.pop();
Some(parent)
}
}
impl fmt::Display for RelatedPathBuf {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.components.is_empty() {
return write!(f, ".");
}
let mut first = true;
for component in &self.components {
if !first {
write!(f, "/")?;
}
first = false;
write!(f, "{}", component.as_str())?;
}
Ok(())
}
}
impl RelatedPathComponent {
fn as_str(&self) -> &str {
match self {
Self::Current => ".",
Self::Parent => "..",
Self::Normal(component) => component.as_str(),
}
}
fn normal_component(&self) -> Option<&str> {
match self {
Self::Normal(component) => Some(component.as_str()),
Self::Current | Self::Parent => None,
}
}
}
fn validate_normal_component(component: &str) -> Result<()> {
if component.is_empty() {
return Err(Error::invalid_source_reference(
"related path component must not be empty".to_string(),
));
}
if component == "." || component == ".." {
return Err(Error::invalid_source_reference(format!(
"special path component must use explicit helpers: {component}"
)));
}
if component.contains(['/', '\\', '\0']) {
return Err(Error::invalid_source_reference(format!(
"invalid related path component: {component}"
)));
}
Ok(())
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct SourceIdentity {
logical_path: RelatedPathBuf,
}
impl SourceIdentity {
pub fn new(logical_path: RelatedPathBuf) -> Self {
Self { logical_path }
}
pub fn from_relative_path(path: &str) -> Result<Self> {
Ok(Self::new(RelatedPathBuf::from_relative_path(path)?))
}
pub fn logical_path(&self) -> &RelatedPathBuf {
&self.logical_path
}
pub fn entry_name(&self) -> Option<&str> {
self.logical_path.file_name()
}
pub fn extension(&self) -> Option<&str> {
let (_, extension) = self.entry_name()?.rsplit_once('.')?;
if extension.is_empty() {
None
} else {
Some(extension)
}
}
pub fn sibling_path(&self, entry_name: impl Into<String>) -> Result<RelatedPathBuf> {
let mut path = self.logical_path.parent().unwrap_or_default();
path.push_normal(entry_name)?;
Ok(path)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RelatedSourcePurpose {
BackingFile,
Segment,
Descriptor,
Band,
Extent,
Metadata,
Auxiliary,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct RelatedSourceRequest {
pub purpose: RelatedSourcePurpose,
pub path: RelatedPathBuf,
}
impl RelatedSourceRequest {
pub fn new(purpose: RelatedSourcePurpose, path: RelatedPathBuf) -> Self {
Self { purpose, path }
}
}
pub trait RelatedSourceResolver: Send + Sync {
fn resolve(&self, request: &RelatedSourceRequest) -> Result<Option<ByteSourceHandle>>;
fn telemetry_name(&self) -> &'static str {
std::any::type_name::<Self>()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn related_path_parses_relative_components() {
let path = RelatedPathBuf::from_relative_path("bands/../bands/0").unwrap();
assert_eq!(
path.components().collect::<Vec<_>>(),
vec!["bands", "..", "bands", "0"]
);
assert_eq!(path.to_string(), "bands/../bands/0");
}
#[test]
fn related_path_rejects_absolute_paths() {
let error = RelatedPathBuf::from_relative_path("/bands/0").unwrap_err();
assert!(matches!(error, Error::InvalidSourceReference(_)));
}
#[test]
fn related_path_join_preserves_lexical_components() {
let left = RelatedPathBuf::from_relative_path("images").unwrap();
let right = RelatedPathBuf::from_relative_path("disk/segments").unwrap();
assert_eq!(left.join(&right).to_string(), "images/disk/segments");
}
#[test]
fn related_path_reports_final_normal_component() {
let path = RelatedPathBuf::from_relative_path("images/disk.raw.000").unwrap();
assert_eq!(path.file_name(), Some("disk.raw.000"));
}
#[test]
fn source_identity_exposes_entry_name_and_extension() {
let identity = SourceIdentity::from_relative_path("segments/disk.raw.000").unwrap();
assert_eq!(identity.entry_name(), Some("disk.raw.000"));
assert_eq!(identity.extension(), Some("000"));
}
#[test]
fn source_identity_builds_sibling_paths() {
let identity = SourceIdentity::from_relative_path("segments/disk.raw.000").unwrap();
assert_eq!(
identity.sibling_path("disk.raw.001").unwrap().to_string(),
"segments/disk.raw.001"
);
}
}