use std::collections::HashMap;
use std::sync::Arc;
use arrow::datatypes::{DataType, Field};
#[derive(Debug, Clone)]
pub struct ExtensionRow {
pub fields: HashMap<String, ExtensionValue>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ExtensionValue {
Str(String),
OptStr(Option<String>),
U32(u32),
StrList(Vec<String>),
Bytes(Vec<u8>),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Col {
Utf8,
U32,
Utf8List,
}
impl Col {
pub fn data_type(self) -> DataType {
match self {
Col::Utf8 => DataType::Utf8,
Col::U32 => DataType::UInt32,
Col::Utf8List => DataType::List(Arc::new(Field::new("item", DataType::Utf8, true))),
}
}
}
#[derive(Debug, Clone, Copy)]
pub enum Claim {
Extensions,
ExtensionsAnyCase,
Layout(fn(&str) -> bool),
Content,
}
#[derive(Debug)]
pub struct HandlerSpec {
pub name: &'static str,
pub aliases: &'static [&'static str],
pub type_id: i8,
pub ecosystem: &'static str,
pub extensions: &'static [&'static str],
pub claim: Claim,
pub description: &'static str,
pub fields: &'static [(&'static str, Col)],
pub commands: &'static [(&'static str, &'static str)],
}
impl HandlerSpec {
pub fn claims(&self, path: &str) -> bool {
match self.claim {
Claim::Extensions => self.extensions.iter().any(|e| path.ends_with(e)),
Claim::ExtensionsAnyCase => {
let p = path.as_bytes();
self.extensions.iter().any(|e| {
p.len() >= e.len() && p[p.len() - e.len()..].eq_ignore_ascii_case(e.as_bytes())
})
}
Claim::Layout(f) => f(path),
Claim::Content => true,
}
}
}
#[derive(Debug, Clone)]
pub struct HandlerCommand {
pub name: String,
pub about: String,
}
impl HandlerCommand {
pub fn new(name: impl Into<String>, about: impl Into<String>) -> Self {
Self { name: name.into(), about: about.into() }
}
}
#[derive(Debug, Clone)]
pub struct HandlerMeta {
pub name: String,
pub aliases: Vec<String>,
pub type_id: i8,
pub ecosystem: String,
pub extensions: Vec<String>,
pub description: String,
pub commands: Vec<HandlerCommand>,
}
pub trait ArchiveTypePlugin: Send + Sync {
fn spec(&self) -> &HandlerSpec;
fn extract_metadata(&self, path: &str, data: &[u8]) -> Option<ExtensionRow>;
fn run_command(&self, cmd: &str, args: &[String]) -> anyhow::Result<()> {
default_command(self, cmd, args)
}
fn matches_path(&self, path: &str) -> bool {
self.spec().claims(path)
}
fn schema_fields(&self) -> Vec<Field> {
self.spec().fields.iter().map(|(n, c)| Field::new(*n, c.data_type(), true)).collect()
}
fn name(&self) -> &str {
self.spec().name
}
fn type_id(&self) -> i8 {
self.spec().type_id
}
fn meta(&self) -> HandlerMeta {
let s = self.spec();
let owned = |v: &[&str]| v.iter().map(|x| x.to_string()).collect::<Vec<_>>();
let mut commands = Vec::new();
if !self.schema_fields().is_empty() {
commands.push(HandlerCommand::new("coords", format!("Print {} coords read from a file", s.name)));
}
commands.extend(s.commands.iter().map(|(n, a)| HandlerCommand::new(*n, *a)));
HandlerMeta {
name: s.name.into(),
aliases: owned(s.aliases),
type_id: s.type_id,
ecosystem: s.ecosystem.into(),
extensions: owned(s.extensions),
description: s.description.into(),
commands,
}
}
}
pub fn default_command<P: ArchiveTypePlugin + ?Sized>(p: &P, cmd: &str, args: &[String]) -> anyhow::Result<()> {
if cmd != "coords" {
anyhow::bail!("handler '{}' has no subcommand '{}'", p.name(), cmd);
}
let path = args.first().ok_or_else(|| anyhow::anyhow!("usage: {} coords <file>", p.name()))?;
let data = std::fs::read(path).unwrap_or_default();
let row = p
.extract_metadata(path, &data)
.ok_or_else(|| anyhow::anyhow!("{}: no coords in '{}'", p.name(), path))?;
let fields = p.schema_fields();
let end = fields.iter().position(|f| f.name() == "version").map_or(1, |i| i + 1);
let out: Vec<&str> = fields[..end.min(fields.len())]
.iter()
.filter_map(|f| match row.fields.get(f.name()) {
Some(ExtensionValue::Str(s)) | Some(ExtensionValue::OptStr(Some(s))) => Some(s.as_str()),
_ => None,
})
.collect();
println!("{}", out.join(" "));
Ok(())
}
pub fn file_name(path: &str) -> &str {
path.rsplit('/').next().unwrap_or(path)
}
pub struct PluginRegistry {
plugin: Option<Box<dyn ArchiveTypePlugin>>,
}
impl PluginRegistry {
pub fn new() -> Self {
Self { plugin: None }
}
pub fn with_plugin(plugin: Box<dyn ArchiveTypePlugin>) -> Self {
Self { plugin: Some(plugin) }
}
pub fn extract(&self, path: &str, data: &[u8]) -> Option<ExtensionRow> {
self.extract_typed(path, data).map(|(_, row)| row)
}
pub fn extract_typed(&self, path: &str, data: &[u8]) -> Option<(i8, ExtensionRow)> {
let p = self.plugin.as_ref()?;
if !p.matches_path(path) {
return None;
}
Some((p.type_id(), p.extract_metadata(path, data)?))
}
pub fn type_id(&self) -> Option<i8> {
self.plugin.as_ref().map(|p| p.type_id())
}
pub fn schema_fields(&self) -> Vec<Field> {
self.plugin.as_ref().map(|p| p.schema_fields()).unwrap_or_default()
}
pub fn matches(&self, path: &str) -> bool {
self.plugin.as_ref().map(|p| p.matches_path(path)).unwrap_or(false)
}
}
impl Default for PluginRegistry {
fn default() -> Self {
Self::new()
}
}