1use std::collections::HashMap;
9use std::sync::Arc;
10
11use arrow::datatypes::{DataType, Field};
12
13#[derive(Debug, Clone)]
15pub struct ExtensionRow {
16 pub fields: HashMap<String, ExtensionValue>,
18}
19
20#[derive(Debug, Clone, PartialEq)]
22pub enum ExtensionValue {
23 Str(String),
24 OptStr(Option<String>),
25 U32(u32),
26 StrList(Vec<String>),
27 Bytes(Vec<u8>),
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum Col {
33 Utf8,
35 U32,
37 Utf8List,
39}
40
41impl Col {
42 pub fn data_type(self) -> DataType {
43 match self {
44 Col::Utf8 => DataType::Utf8,
45 Col::U32 => DataType::UInt32,
46 Col::Utf8List => DataType::List(Arc::new(Field::new("item", DataType::Utf8, true))),
47 }
48 }
49}
50
51#[derive(Debug, Clone, Copy)]
54pub enum Claim {
55 Extensions,
57 ExtensionsAnyCase,
59 Layout(fn(&str) -> bool),
62 Content,
64}
65
66#[derive(Debug)]
68pub struct HandlerSpec {
69 pub name: &'static str,
71 pub aliases: &'static [&'static str],
73 pub type_id: i8,
75 pub ecosystem: &'static str,
77 pub extensions: &'static [&'static str],
79 pub claim: Claim,
81 pub description: &'static str,
83 pub fields: &'static [(&'static str, Col)],
85 pub commands: &'static [(&'static str, &'static str)],
87}
88
89impl HandlerSpec {
90 pub fn claims(&self, path: &str) -> bool {
92 match self.claim {
93 Claim::Extensions => self.extensions.iter().any(|e| path.ends_with(e)),
94 Claim::ExtensionsAnyCase => {
95 let p = path.as_bytes();
96 self.extensions.iter().any(|e| {
97 p.len() >= e.len() && p[p.len() - e.len()..].eq_ignore_ascii_case(e.as_bytes())
98 })
99 }
100 Claim::Layout(f) => f(path),
101 Claim::Content => true,
102 }
103 }
104}
105
106#[derive(Debug, Clone)]
108pub struct HandlerCommand {
109 pub name: String,
111 pub about: String,
113}
114
115impl HandlerCommand {
116 pub fn new(name: impl Into<String>, about: impl Into<String>) -> Self {
117 Self { name: name.into(), about: about.into() }
118 }
119}
120
121#[derive(Debug, Clone)]
124pub struct HandlerMeta {
125 pub name: String,
126 pub aliases: Vec<String>,
127 pub type_id: i8,
128 pub ecosystem: String,
129 pub extensions: Vec<String>,
130 pub description: String,
131 pub commands: Vec<HandlerCommand>,
132}
133
134pub trait ArchiveTypePlugin: Send + Sync {
136 fn spec(&self) -> &HandlerSpec;
138
139 fn extract_metadata(&self, path: &str, data: &[u8]) -> Option<ExtensionRow>;
141
142 fn run_command(&self, cmd: &str, args: &[String]) -> anyhow::Result<()> {
144 default_command(self, cmd, args)
145 }
146
147 fn matches_path(&self, path: &str) -> bool {
151 self.spec().claims(path)
152 }
153
154 fn schema_fields(&self) -> Vec<Field> {
156 self.spec().fields.iter().map(|(n, c)| Field::new(*n, c.data_type(), true)).collect()
157 }
158
159 fn name(&self) -> &str {
160 self.spec().name
161 }
162
163 fn type_id(&self) -> i8 {
164 self.spec().type_id
165 }
166
167 fn meta(&self) -> HandlerMeta {
169 let s = self.spec();
170 let owned = |v: &[&str]| v.iter().map(|x| x.to_string()).collect::<Vec<_>>();
171 let mut commands = Vec::new();
172 if !self.schema_fields().is_empty() {
173 commands.push(HandlerCommand::new("coords", format!("Print {} coords read from a file", s.name)));
174 }
175 commands.extend(s.commands.iter().map(|(n, a)| HandlerCommand::new(*n, *a)));
176 HandlerMeta {
177 name: s.name.into(),
178 aliases: owned(s.aliases),
179 type_id: s.type_id,
180 ecosystem: s.ecosystem.into(),
181 extensions: owned(s.extensions),
182 description: s.description.into(),
183 commands,
184 }
185 }
186}
187
188pub fn default_command<P: ArchiveTypePlugin + ?Sized>(p: &P, cmd: &str, args: &[String]) -> anyhow::Result<()> {
193 if cmd != "coords" {
194 anyhow::bail!("handler '{}' has no subcommand '{}'", p.name(), cmd);
195 }
196 let path = args.first().ok_or_else(|| anyhow::anyhow!("usage: {} coords <file>", p.name()))?;
197 let data = std::fs::read(path).unwrap_or_default();
198 let row = p
199 .extract_metadata(path, &data)
200 .ok_or_else(|| anyhow::anyhow!("{}: no coords in '{}'", p.name(), path))?;
201 let fields = p.schema_fields();
202 let end = fields.iter().position(|f| f.name() == "version").map_or(1, |i| i + 1);
203 let out: Vec<&str> = fields[..end.min(fields.len())]
204 .iter()
205 .filter_map(|f| match row.fields.get(f.name()) {
206 Some(ExtensionValue::Str(s)) | Some(ExtensionValue::OptStr(Some(s))) => Some(s.as_str()),
207 _ => None,
208 })
209 .collect();
210 println!("{}", out.join(" "));
211 Ok(())
212}
213
214pub fn file_name(path: &str) -> &str {
216 path.rsplit('/').next().unwrap_or(path)
217}
218
219pub struct PluginRegistry {
224 plugin: Option<Box<dyn ArchiveTypePlugin>>,
225}
226
227impl PluginRegistry {
228 pub fn new() -> Self {
229 Self { plugin: None }
230 }
231
232 pub fn with_plugin(plugin: Box<dyn ArchiveTypePlugin>) -> Self {
233 Self { plugin: Some(plugin) }
234 }
235
236 pub fn extract(&self, path: &str, data: &[u8]) -> Option<ExtensionRow> {
238 self.extract_typed(path, data).map(|(_, row)| row)
239 }
240
241 pub fn extract_typed(&self, path: &str, data: &[u8]) -> Option<(i8, ExtensionRow)> {
243 let p = self.plugin.as_ref()?;
244 if !p.matches_path(path) {
245 return None;
246 }
247 Some((p.type_id(), p.extract_metadata(path, data)?))
248 }
249
250 pub fn type_id(&self) -> Option<i8> {
251 self.plugin.as_ref().map(|p| p.type_id())
252 }
253
254 pub fn schema_fields(&self) -> Vec<Field> {
256 self.plugin.as_ref().map(|p| p.schema_fields()).unwrap_or_default()
257 }
258
259 pub fn matches(&self, path: &str) -> bool {
260 self.plugin.as_ref().map(|p| p.matches_path(path)).unwrap_or(false)
261 }
262}
263
264impl Default for PluginRegistry {
265 fn default() -> Self {
266 Self::new()
267 }
268}