1use std::collections::HashMap;
8use std::sync::Arc;
9
10use crate::archive::{Codec, Container};
11use crate::error::{Error, Result};
12use crate::format::{FormatId, FormatKind, FormatMeta, MagicRule};
13
14#[derive(Clone, PartialEq, Eq, Debug)]
16pub struct FormatRow {
17 pub id: FormatId,
18 pub kind: FormatKind,
19 pub extensions: &'static [&'static str],
20 pub read: bool,
21 pub write: bool,
22 pub parallel: bool,
23}
24
25#[derive(Default)]
26pub struct Registry {
27 codecs: HashMap<FormatId, Arc<dyn Codec>>,
28 containers: HashMap<FormatId, Arc<dyn Container>>,
29 metas: HashMap<FormatId, FormatMeta>,
30 by_ext: HashMap<String, FormatId>,
31 magics: Vec<MagicRule>,
32}
33
34impl Registry {
35 pub fn new() -> Self {
36 Self::default()
37 }
38
39 fn index(&mut self, meta: FormatMeta) {
40 self.by_ext.retain(|_, v| *v != meta.id);
43 for ext in meta.extensions {
44 self.by_ext.insert(ext.to_ascii_lowercase(), meta.id);
45 }
46 self.magics.retain(|m| m.format != meta.id);
48 self.magics.extend_from_slice(meta.magics);
49 self.metas.insert(meta.id, meta);
50 }
51
52 pub fn register_codec(&mut self, codec: Arc<dyn Codec>, meta: FormatMeta) {
53 self.codecs.insert(meta.id, codec);
54 self.index(meta);
55 }
56
57 pub fn register_container(&mut self, container: Arc<dyn Container>, meta: FormatMeta) {
58 self.containers.insert(meta.id, container);
59 self.index(meta);
60 }
61
62 pub fn codec(&self, id: FormatId) -> Option<&Arc<dyn Codec>> {
63 self.codecs.get(&id)
64 }
65
66 pub fn container(&self, id: FormatId) -> Option<&Arc<dyn Container>> {
67 self.containers.get(&id)
68 }
69
70 pub fn require_codec(&self, id: FormatId) -> Result<&Arc<dyn Codec>> {
73 self.codec(id).ok_or(Error::FormatNotEnabled(id))
74 }
75
76 pub fn require_encoder(&self, id: FormatId) -> Result<&Arc<dyn Codec>> {
84 let codec = self.require_codec(id)?;
85 if !codec.caps().encode {
86 return Err(Error::CapabilityUnavailable {
87 format: id,
88 available: "read",
89 requested: "written",
90 });
91 }
92 Ok(codec)
93 }
94
95 pub fn require_decoder(&self, id: FormatId) -> Result<&Arc<dyn Codec>> {
97 let codec = self.require_codec(id)?;
98 if !codec.caps().decode {
99 return Err(Error::CapabilityUnavailable {
100 format: id,
101 available: "written",
102 requested: "read",
103 });
104 }
105 Ok(codec)
106 }
107
108 pub fn require_container(&self, id: FormatId) -> Result<&Arc<dyn Container>> {
109 self.container(id).ok_or(Error::FormatNotEnabled(id))
110 }
111
112 pub fn by_extension(&self, ext: &str) -> Option<FormatId> {
114 let key = ext.trim_start_matches('.').to_ascii_lowercase();
115 self.by_ext.get(&key).copied()
116 }
117
118 pub fn match_magic(&self, prefix: &[u8]) -> Vec<FormatId> {
120 let mut hits: Vec<(i16, FormatId)> = self
121 .magics
122 .iter()
123 .filter(|m| {
124 let end = match m.offset.checked_add(m.bytes.len()) {
125 Some(e) => e,
126 None => return false,
127 };
128 prefix.len() >= end && &prefix[m.offset..end] == m.bytes
129 })
130 .map(|m| {
131 (
132 self.metas.get(&m.format).map_or(0, |x| x.priority),
133 m.format,
134 )
135 })
136 .collect();
137 hits.sort_by(|a, b| b.0.cmp(&a.0).then_with(|| a.1.as_str().cmp(b.1.as_str())));
139 hits.dedup_by_key(|(_, id)| *id);
140 hits.into_iter().map(|(_, id)| id).collect()
141 }
142
143 pub fn priority_of(&self, id: FormatId) -> i16 {
145 self.metas.get(&id).map_or(0, |m| m.priority)
146 }
147
148 pub fn matrix(&self) -> Vec<FormatRow> {
150 let mut rows: Vec<FormatRow> = self
151 .metas
152 .values()
153 .map(|meta| {
154 let (read, write, parallel) = match meta.kind {
155 FormatKind::Codec => match self.codecs.get(&meta.id) {
156 Some(c) => {
157 let caps = c.caps();
158 (
159 caps.decode,
160 caps.encode,
161 caps.parallel_encode || caps.parallel_decode,
162 )
163 }
164 None => (false, false, false),
165 },
166 FormatKind::Container => match self.containers.get(&meta.id) {
167 Some(c) => {
168 let caps = c.caps();
169 (caps.read, caps.write, false)
170 }
171 None => (false, false, false),
172 },
173 };
174 FormatRow {
175 id: meta.id,
176 kind: meta.kind,
177 extensions: meta.extensions,
178 read,
179 write,
180 parallel,
181 }
182 })
183 .collect();
184 rows.sort_by_key(|r| r.id.as_str());
185 rows
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192 use crate::archive::{DecodeOpts, EncodeOpts, Sink};
193 use crate::format::CodecCaps;
194 use crate::source::Source;
195 use crate::testing::{MOCK_CODEC, MOCK_CONTAINER, MockCodec, MockContainer};
196 use std::io::Write;
197
198 const CODEC_MAGIC: &[MagicRule] = &[MagicRule {
199 offset: 0,
200 bytes: b"MOCK",
201 format: MOCK_CODEC,
202 }];
203 const CONTAINER_MAGIC: &[MagicRule] = &[MagicRule {
204 offset: 0,
205 bytes: b"ME",
206 format: MOCK_CONTAINER,
207 }];
208
209 fn codec_meta() -> FormatMeta {
210 FormatMeta::codec(MOCK_CODEC, &["mk", "mock"], CODEC_MAGIC)
211 }
212
213 fn container_meta() -> FormatMeta {
214 FormatMeta::container(MOCK_CONTAINER, &["mar"], CONTAINER_MAGIC)
215 }
216
217 fn populated() -> Registry {
218 let mut r = Registry::new();
219 r.register_codec(Arc::new(MockCodec), codec_meta());
220 r.register_container(Arc::new(MockContainer), container_meta());
221 r
222 }
223
224 #[test]
225 fn an_empty_registry_reports_no_formats() {
226 assert!(Registry::new().matrix().is_empty());
227 }
228
229 #[test]
230 fn registered_formats_are_retrievable_by_id() {
231 let r = populated();
232 assert!(r.codec(MOCK_CODEC).is_some());
233 assert!(r.container(MOCK_CONTAINER).is_some());
234 assert!(r.container(MOCK_CODEC).is_none());
236 }
237
238 #[test]
239 fn a_missing_format_is_format_not_enabled_not_a_generic_error() {
240 let r = Registry::new();
247 let Err(err) = r.require_codec(MOCK_CODEC) else {
248 panic!("expected FormatNotEnabled for an unregistered codec");
249 };
250 assert!(matches!(err, crate::Error::FormatNotEnabled(id) if id == MOCK_CODEC));
251 assert_eq!(err.exit_code(), 3);
252 }
253
254 #[test]
255 fn extension_lookup_is_case_insensitive_and_dot_tolerant() {
256 let r = populated();
257 assert_eq!(r.by_extension("mk"), Some(MOCK_CODEC));
258 assert_eq!(r.by_extension("MOCK"), Some(MOCK_CODEC));
259 assert_eq!(r.by_extension(".mar"), Some(MOCK_CONTAINER));
260 assert_eq!(r.by_extension("nope"), None);
261 }
262
263 #[test]
264 fn magic_matching_finds_the_format_from_a_prefix() {
265 let r = populated();
266 assert_eq!(r.match_magic(b"MOCKpayload"), vec![MOCK_CODEC]);
267 assert_eq!(r.match_magic(b"ME\x05\x00hello"), vec![MOCK_CONTAINER]);
268 }
269
270 #[test]
271 fn magic_matching_returns_empty_rather_than_guessing() {
272 let r = populated();
273 assert!(r.match_magic(b"unrecognised bytes").is_empty());
274 assert!(
275 r.match_magic(b"").is_empty(),
276 "an empty prefix matches nothing"
277 );
278 assert!(
279 r.match_magic(b"M").is_empty(),
280 "a truncated prefix must not match"
281 );
282 }
283
284 #[test]
285 fn magic_respects_the_declared_offset() {
286 const OFFSET_MAGIC: &[MagicRule] = &[MagicRule {
287 offset: 257,
288 bytes: b"ustar",
289 format: FormatId::new("tar"),
290 }];
291 let mut r = Registry::new();
292 r.register_container(
293 Arc::new(MockContainer),
294 FormatMeta::container(FormatId::new("tar"), &["tar"], OFFSET_MAGIC),
295 );
296
297 let mut buf = vec![0u8; 512];
298 buf[257..262].copy_from_slice(b"ustar");
299 assert_eq!(r.match_magic(&buf), vec![FormatId::new("tar")]);
300
301 let mut wrong = vec![0u8; 512];
303 wrong[0..5].copy_from_slice(b"ustar");
304 assert!(r.match_magic(&wrong).is_empty());
305 }
306
307 #[test]
308 fn matrix_reports_capabilities_and_is_sorted() {
309 let r = populated();
310 let m = r.matrix();
311 assert_eq!(m.len(), 2);
312 assert!(m.windows(2).all(|w| w[0].id.as_str() <= w[1].id.as_str()));
313
314 let codec_row = m.iter().find(|row| row.id == MOCK_CODEC).unwrap();
315 assert_eq!(codec_row.kind, FormatKind::Codec);
316 assert!(codec_row.read && codec_row.write);
317 assert!(!codec_row.parallel, "the mock declares no parallelism");
318 assert_eq!(codec_row.extensions, &["mk", "mock"]);
319 }
320
321 #[test]
322 fn re_registering_an_id_replaces_it_rather_than_duplicating() {
323 let mut r = populated();
324 r.register_codec(Arc::new(MockCodec), codec_meta());
325 assert_eq!(r.matrix().len(), 2);
326 }
327
328 #[test]
329 fn re_registering_with_a_different_extension_list_drops_the_old_mapping() {
330 let mut r = populated();
331 r.register_codec(
332 Arc::new(MockCodec),
333 FormatMeta::codec(MOCK_CODEC, &["mk2"], CODEC_MAGIC),
334 );
335 assert_eq!(r.by_extension("mk2"), Some(MOCK_CODEC));
336 assert_eq!(
337 r.by_extension("mk"),
338 None,
339 "a stale extension must not survive re-registration"
340 );
341 assert_eq!(r.by_extension("mock"), None);
342 }
343
344 #[test]
345 fn match_magic_orders_by_priority_before_id() {
346 const SHARED: &[MagicRule] = &[MagicRule {
348 offset: 0,
349 bytes: b"PK\x03\x04",
350 format: FormatId::new("zip"),
351 }];
352 const SHARED2: &[MagicRule] = &[MagicRule {
353 offset: 0,
354 bytes: b"PK\x03\x04",
355 format: FormatId::new("apk"),
356 }];
357
358 let mut r = Registry::new();
359 r.register_container(
361 Arc::new(MockContainer),
362 FormatMeta::container(FormatId::new("apk"), &["apk"], SHARED2).with_priority(-10),
363 );
364 r.register_container(
365 Arc::new(MockContainer),
366 FormatMeta::container(FormatId::new("zip"), &["zip"], SHARED),
367 );
368
369 let hits = r.match_magic(b"PK\x03\x04rest");
370 assert_eq!(
371 hits.first().copied(),
372 Some(FormatId::new("zip")),
373 "the base format must outrank its derivative"
374 );
375 assert_eq!(hits.len(), 2);
376 }
377
378 #[test]
379 fn equal_priority_still_orders_deterministically_by_id() {
380 const A: &[MagicRule] = &[MagicRule {
381 offset: 0,
382 bytes: b"XX",
383 format: FormatId::new("bbb"),
384 }];
385 const B: &[MagicRule] = &[MagicRule {
386 offset: 0,
387 bytes: b"XX",
388 format: FormatId::new("aaa"),
389 }];
390 let mut r = Registry::new();
391 r.register_codec(
392 Arc::new(MockCodec),
393 FormatMeta::codec(FormatId::new("bbb"), &["b"], A),
394 );
395 r.register_codec(
396 Arc::new(MockCodec),
397 FormatMeta::codec(FormatId::new("aaa"), &["a"], B),
398 );
399 assert_eq!(
400 r.match_magic(b"XXrest"),
401 vec![FormatId::new("aaa"), FormatId::new("bbb")]
402 );
403 }
404
405 #[test]
406 fn a_decode_only_codec_is_refused_an_encoder_rather_than_reaching_it() {
407 use crate::testing::{MOCK_CODEC, MockCodec};
408
409 struct ReadOnlyCodec;
412 impl Codec for ReadOnlyCodec {
413 fn id(&self) -> FormatId {
414 MOCK_CODEC
415 }
416 fn caps(&self) -> CodecCaps {
417 CodecCaps::decode_only()
418 }
419 fn decoder(&self, src: Box<dyn Source>, o: &DecodeOpts) -> Result<Box<dyn Source>> {
420 MockCodec.decoder(src, o)
421 }
422 fn encoder(&self, _d: Box<dyn Write + Send>, _o: &EncodeOpts) -> Result<Box<dyn Sink>> {
423 panic!("a capability check must stop the caller before it reaches here");
432 }
433 }
434
435 let mut reg = Registry::new();
436 reg.register_codec(
437 std::sync::Arc::new(ReadOnlyCodec),
438 FormatMeta::codec(MOCK_CODEC, &["mock"], &[]),
439 );
440
441 assert!(reg.require_decoder(MOCK_CODEC).is_ok());
443
444 let Err(err) = reg.require_encoder(MOCK_CODEC) else {
453 panic!("expected CapabilityUnavailable for a decode-only codec");
454 };
455 assert!(
456 matches!(err, Error::CapabilityUnavailable { .. }),
457 "got {err:?}"
458 );
459 assert!(
460 err.to_string().contains("mock-codec"),
461 "must name the format: {err}"
462 );
463 assert_eq!(
464 err.exit_code(),
465 3,
466 "a capability refusal must be distinguishable from other errors so a script can branch on it"
467 );
468 }
469}