use std::sync::Arc;
use super::{
ByteSource, ByteSourceHandle, ProbeCachedDataSource, RelatedPathBuf, RelatedSourcePurpose,
RelatedSourceRequest, RelatedSourceResolver, Result, SourceIdentity,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum FormatKind {
Archive,
Image,
VolumeSystem,
VolumeManager,
FileSystem,
Helper,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct FormatDescriptor {
pub id: &'static str,
pub kind: FormatKind,
}
impl FormatDescriptor {
pub const fn new(id: &'static str, kind: FormatKind) -> Self {
Self { id, kind }
}
}
impl FormatKind {
pub const fn probe_sort_rank(self) -> u8 {
match self {
Self::Archive => 0,
Self::Image => 1,
Self::VolumeSystem => 2,
Self::VolumeManager => 3,
Self::FileSystem => 4,
Self::Helper => 5,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ProbeConfidence {
Weak,
Likely,
Strong,
Exact,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ProbeMatch {
pub format: FormatDescriptor,
pub confidence: ProbeConfidence,
pub detail: &'static str,
}
impl ProbeMatch {
pub const fn new(
format: FormatDescriptor, confidence: ProbeConfidence, detail: &'static str,
) -> Self {
Self {
format,
confidence,
detail,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ProbeResult {
Rejected,
Matched(ProbeMatch),
}
impl ProbeResult {
pub const fn rejected() -> Self {
Self::Rejected
}
pub const fn matched(probe_match: ProbeMatch) -> Self {
Self::Matched(probe_match)
}
pub const fn is_match(self) -> bool {
matches!(self, Self::Matched(_))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ProbeReport {
matches: Vec<ProbeMatch>,
}
impl ProbeReport {
pub fn new() -> Self {
Self::default()
}
pub fn best_match(&self) -> Option<ProbeMatch> {
self.matches.first().copied()
}
pub fn matches(&self) -> &[ProbeMatch] {
&self.matches
}
fn push(&mut self, probe_match: ProbeMatch) {
self.matches.push(probe_match);
}
}
#[derive(Clone, Copy, Default)]
pub struct SourceHints<'a> {
resolver: Option<&'a dyn RelatedSourceResolver>,
shared_resolver: Option<&'a Arc<dyn RelatedSourceResolver>>,
source_identity: Option<&'a SourceIdentity>,
}
impl<'a> SourceHints<'a> {
pub fn new() -> Self {
Self::default()
}
pub fn resolver(self) -> Option<&'a dyn RelatedSourceResolver> {
self
.shared_resolver
.map(|resolver| resolver.as_ref())
.or(self.resolver)
}
pub fn shared_resolver(self) -> Option<Arc<dyn RelatedSourceResolver>> {
self.shared_resolver.cloned()
}
pub fn has_resolver(self) -> bool {
self.resolver.is_some() || self.shared_resolver.is_some()
}
pub fn source_identity(self) -> Option<&'a SourceIdentity> {
self.source_identity
}
pub fn entry_name_hint(self) -> Option<&'a str> {
self.source_identity.and_then(SourceIdentity::entry_name)
}
pub fn with_resolver(mut self, resolver: &'a dyn RelatedSourceResolver) -> Self {
self.resolver = Some(resolver);
self.shared_resolver = None;
self
}
pub fn with_shared_resolver(mut self, resolver: &'a Arc<dyn RelatedSourceResolver>) -> Self {
self.resolver = None;
self.shared_resolver = Some(resolver);
self
}
pub fn with_source_identity(mut self, source_identity: &'a SourceIdentity) -> Self {
self.source_identity = Some(source_identity);
self
}
}
pub type ProbeOptions<'a> = SourceHints<'a>;
pub struct ProbeContext<'a> {
source: &'a dyn ByteSource,
cached: ProbeCachedDataSource<'a>,
options: SourceHints<'a>,
}
impl<'a> ProbeContext<'a> {
pub fn new(source: &'a dyn ByteSource) -> Self {
Self::with_options(source, SourceHints::new())
}
pub fn with_resolver(
source: &'a dyn ByteSource, resolver: &'a dyn RelatedSourceResolver,
) -> Self {
Self::with_options(source, SourceHints::new().with_resolver(resolver))
}
pub fn with_options(source: &'a dyn ByteSource, options: SourceHints<'a>) -> Self {
Self {
source,
cached: ProbeCachedDataSource::new(source),
options,
}
}
pub fn source(&self) -> &'a dyn ByteSource {
self.source
}
pub fn resolver(&self) -> Option<&'a dyn RelatedSourceResolver> {
self.options.resolver()
}
pub fn has_resolver(&self) -> bool {
self.options.has_resolver()
}
pub fn source_identity(&self) -> Option<&'a SourceIdentity> {
self.options.source_identity
}
pub fn entry_name_hint(&self) -> Option<&'a str> {
self.source_identity().and_then(SourceIdentity::entry_name)
}
pub fn read_exact_at(&self, offset: u64, buf: &mut [u8]) -> Result<()> {
self.cached.read_exact_at(offset, buf)
}
pub fn read_bytes_at(&self, offset: u64, len: usize) -> Result<Vec<u8>> {
self.cached.read_bytes_at(offset, len)
}
pub fn header(&self, len: usize) -> Result<Vec<u8>> {
self.read_bytes_at(0, len)
}
pub fn size(&self) -> Result<u64> {
self.source.size()
}
pub fn resolve_related(
&self, request: &RelatedSourceRequest,
) -> Result<Option<ByteSourceHandle>> {
match self.options.resolver {
Some(resolver) => resolver.resolve(request),
None => Ok(None),
}
}
pub fn resolve_related_path(
&self, purpose: RelatedSourcePurpose, path: &str,
) -> Result<Option<ByteSourceHandle>> {
let path = RelatedPathBuf::from_relative_path(path)?;
self.resolve_related(&RelatedSourceRequest::new(purpose, path))
}
}
pub trait FormatProbe: Send + Sync {
fn descriptor(&self) -> FormatDescriptor;
fn probe(&self, context: &ProbeContext<'_>) -> Result<ProbeResult>;
}
#[derive(Default)]
pub struct ProbeRegistry {
probes: Vec<Box<dyn FormatProbe>>,
}
impl ProbeRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn register(&mut self, probe: impl FormatProbe + 'static) {
self.probes.push(Box::new(probe));
}
pub fn with_probe(mut self, probe: impl FormatProbe + 'static) -> Self {
self.register(probe);
self
}
pub fn len(&self) -> usize {
self.probes.len()
}
pub fn is_empty(&self) -> bool {
self.probes.is_empty()
}
pub fn probe_all(&self, source: &dyn ByteSource) -> Result<ProbeReport> {
self.probe_all_with_options(source, SourceHints::new())
}
pub fn probe_all_with_resolver(
&self, source: &dyn ByteSource, resolver: &dyn RelatedSourceResolver,
) -> Result<ProbeReport> {
self.probe_all_with_options(source, SourceHints::new().with_resolver(resolver))
}
pub fn probe_all_with_options(
&self, source: &dyn ByteSource, options: SourceHints<'_>,
) -> Result<ProbeReport> {
let context = ProbeContext::with_options(source, options);
self.probe_all_with_context(&context)
}
pub fn probe_all_with_context(&self, context: &ProbeContext<'_>) -> Result<ProbeReport> {
let mut hits = Vec::new();
for (registration_index, probe) in self.probes.iter().enumerate() {
if let ProbeResult::Matched(probe_match) = probe.probe(context)? {
hits.push((registration_index, probe_match));
}
}
hits.sort_by(|(left_index, left_match), (right_index, right_match)| {
right_match
.confidence
.cmp(&left_match.confidence)
.then(left_index.cmp(right_index))
});
let mut report = ProbeReport::new();
for (_, probe_match) in hits {
report.push(probe_match);
}
Ok(report)
}
pub fn probe_best(&self, source: &dyn ByteSource) -> Result<Option<ProbeMatch>> {
Ok(self.probe_all(source)?.best_match())
}
pub fn probe_best_with_resolver(
&self, source: &dyn ByteSource, resolver: &dyn RelatedSourceResolver,
) -> Result<Option<ProbeMatch>> {
Ok(self.probe_all_with_resolver(source, resolver)?.best_match())
}
pub fn probe_best_with_options(
&self, source: &dyn ByteSource, options: SourceHints<'_>,
) -> Result<Option<ProbeMatch>> {
Ok(self.probe_all_with_options(source, options)?.best_match())
}
}
#[cfg(test)]
mod tests {
use super::*;
struct MemDataSource {
data: Vec<u8>,
}
impl ByteSource for MemDataSource {
fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<usize> {
let offset = offset as usize;
if offset >= self.data.len() {
return Ok(0);
}
let read = buf.len().min(self.data.len() - offset);
buf[..read].copy_from_slice(&self.data[offset..offset + read]);
Ok(read)
}
fn size(&self) -> Result<u64> {
Ok(self.data.len() as u64)
}
}
#[test]
fn probe_context_reads_cached_header_bytes() {
let source = MemDataSource {
data: b"magic-header-payload".to_vec(),
};
let context = ProbeContext::new(&source);
assert_eq!(context.header(5).unwrap(), b"magic");
assert_eq!(context.read_bytes_at(6, 6).unwrap(), b"header");
}
#[test]
fn probe_result_reports_matches() {
let descriptor = FormatDescriptor::new("archive.zip", FormatKind::Archive);
let result = ProbeResult::matched(ProbeMatch::new(
descriptor,
ProbeConfidence::Exact,
"zip local header found",
));
assert!(result.is_match());
}
struct RejectingResolver;
impl RelatedSourceResolver for RejectingResolver {
fn resolve(&self, _request: &RelatedSourceRequest) -> Result<Option<ByteSourceHandle>> {
Ok(None)
}
}
struct RejectingProbe;
impl FormatProbe for RejectingProbe {
fn descriptor(&self) -> FormatDescriptor {
FormatDescriptor::new("test.reject", FormatKind::Helper)
}
fn probe(&self, _context: &ProbeContext<'_>) -> Result<ProbeResult> {
Ok(ProbeResult::rejected())
}
}
struct MatchingProbe {
descriptor: FormatDescriptor,
confidence: ProbeConfidence,
}
impl FormatProbe for MatchingProbe {
fn descriptor(&self) -> FormatDescriptor {
self.descriptor
}
fn probe(&self, _context: &ProbeContext<'_>) -> Result<ProbeResult> {
Ok(ProbeResult::matched(ProbeMatch::new(
self.descriptor,
self.confidence,
"matched by test probe",
)))
}
}
#[test]
fn probe_registry_sorts_by_confidence_then_registration_order() {
let source = MemDataSource {
data: b"probe-data".to_vec(),
};
let mut registry = ProbeRegistry::new();
registry.register(MatchingProbe {
descriptor: FormatDescriptor::new("test.weak", FormatKind::Helper),
confidence: ProbeConfidence::Weak,
});
registry.register(MatchingProbe {
descriptor: FormatDescriptor::new("test.exact", FormatKind::Helper),
confidence: ProbeConfidence::Exact,
});
registry.register(MatchingProbe {
descriptor: FormatDescriptor::new("test.exact.second", FormatKind::Helper),
confidence: ProbeConfidence::Exact,
});
registry.register(RejectingProbe);
let report = registry.probe_all(&source).unwrap();
assert_eq!(report.best_match().unwrap().format.id, "test.exact");
assert_eq!(
report
.matches()
.iter()
.map(|probe_match| probe_match.format.id)
.collect::<Vec<_>>(),
vec!["test.exact", "test.exact.second", "test.weak"]
);
}
#[test]
fn probe_context_exposes_related_source_resolution() {
let source = MemDataSource {
data: b"probe-data".to_vec(),
};
let resolver = RejectingResolver;
let context = ProbeContext::with_resolver(&source, &resolver);
assert!(context.has_resolver());
assert!(
context
.resolve_related_path(RelatedSourcePurpose::Metadata, "sidecar.bin")
.unwrap()
.is_none()
);
}
#[test]
fn probe_context_exposes_source_identity_hints() {
let source = MemDataSource {
data: b"probe-data".to_vec(),
};
let identity = SourceIdentity::from_relative_path("segments/disk.raw.000").unwrap();
let context =
ProbeContext::with_options(&source, SourceHints::new().with_source_identity(&identity));
assert_eq!(context.entry_name_hint(), Some("disk.raw.000"));
assert_eq!(context.source_identity().unwrap().extension(), Some("000"));
}
}