1use std::path::{Path, PathBuf};
10
11use crate::FileFormat;
12pub use crate::formats::sqlite::{Pick, Table};
13
14pub fn holder(path: &Path) -> Option<FileFormat> {
17 crate::formats::readers::sniff_file(path, crate::formats::readers::Asked::Tables)
18}
19
20pub fn place(file: &Path, name: &str) -> PathBuf {
24 let mut place = file.as_os_str().to_owned();
25 place.push("/");
26 place.push(name);
27 place.into()
28}
29
30pub fn split(path: &Path) -> Option<(PathBuf, String)> {
34 split_inside(path, |file| holder(file).is_some())
35}
36
37pub fn split_variant(path: &Path, formats: &crate::formats::Registry) -> Option<(PathBuf, String)> {
41 if formats.is_empty() {
42 return None;
43 }
44 let (file, name) = split_inside(path, |file| formats.variants_of(file).is_some())?;
45 let (_, tables) = variants(&file, formats)?;
46 let found = tables.iter().find(|t| t.name == name).or_else(|| {
47 let mut alike = tables.iter().filter(|t| t.name.eq_ignore_ascii_case(&name));
48 match (alike.next(), alike.next()) {
49 (Some(one), None) => Some(one),
50 _ => None,
51 }
52 })?;
53 Some((file, found.name.clone()))
54}
55
56pub fn variants(file: &Path, formats: &crate::formats::Registry) -> Option<(String, Vec<Table>)> {
60 let spec = formats.variants_of(file)?;
61 Some((spec.name.clone(), variant_tables(&spec)))
62}
63
64pub fn variant_tables(spec: &crate::formats::Spec) -> Vec<Table> {
67 let named = |fields: &[crate::formats::Field]| {
68 fields
69 .iter()
70 .filter_map(|f| f.name.clone())
71 .collect::<Vec<_>>()
72 };
73 let common = named(&spec.records.fields);
74 spec.records
75 .variants
76 .iter()
77 .map(|v| {
78 let columns = common.iter().cloned().chain(named(&v.fields));
79 Table::plain(&v.name, "record type", columns)
80 })
81 .collect()
82}
83
84fn split_inside(path: &Path, holds: impl Fn(&Path) -> bool) -> Option<(PathBuf, String)> {
86 if path.file_name().is_some() && path.exists() {
89 return None;
90 }
91 let file = path
92 .ancestors()
93 .skip(1)
94 .take_while(|p| !p.as_os_str().is_empty())
95 .find(|p| p.file_name().is_some() && p.is_file())?;
98 if !holds(file) {
99 return None;
100 }
101 let rest = path.to_str()?.strip_prefix(file.to_str()?)?;
104 let mut chars = rest.chars();
105 chars.next().filter(|c| std::path::is_separator(*c))?;
106 let name = chars.as_str().trim_end_matches(std::path::is_separator);
107 (!name.is_empty()).then(|| (file.to_path_buf(), name.to_string()))
108}
109
110pub fn tables(file: &Path, format: FileFormat) -> color_eyre::Result<Vec<Table>> {
113 match crate::formats::readers::of(format).tables {
114 Some(tables) => tables(file),
115 None => Err(color_eyre::eyre::eyre!(
116 "A {} file holds one table.",
117 format.name()
118 )),
119 }
120 .map_err(|e| crate::error_display::in_file(file, e))
121}
122
123pub fn pick(
127 tables: Vec<Table>,
128 wanted: Option<&str>,
129 display: &Path,
130 empty: &str,
131) -> color_eyre::Result<Pick> {
132 use crate::error_display::FileError;
133 let own: Vec<&Table> = tables.iter().filter(|t| !t.internal).collect();
134 let names = || {
135 const SHOWN: usize = 20;
136 let mut names: Vec<&str> = own.iter().take(SHOWN).map(|t| t.name.as_str()).collect();
137 let more = own.len().saturating_sub(SHOWN);
138 let more = format!("and {more} more");
139 if own.len() > SHOWN {
140 names.push(&more);
141 }
142 names.join(", ")
143 };
144 if let Some(wanted) = wanted {
145 let found = tables.iter().find(|t| t.name == wanted).or_else(|| {
147 let mut alike = tables
148 .iter()
149 .filter(|t| t.name.eq_ignore_ascii_case(wanted));
150 match (alike.next(), alike.next()) {
151 (Some(one), None) => Some(one),
152 _ => None,
153 }
154 });
155 return match found {
156 Some(table) => Ok(Pick::One(table.clone())),
157 None if own.is_empty() => Err(FileError::new(
158 display,
159 format!("no table \"{wanted}\"; the file holds no tables."),
160 )
161 .into()),
162 None => Err(FileError::new(
163 display,
164 format!(
165 "no table \"{wanted}\". --table names one of its tables: {}.",
166 names()
167 ),
168 )
169 .into()),
170 };
171 }
172 match own.as_slice() {
173 [] => Err(FileError::new(display, format!("the file holds no tables.{empty}")).into()),
174 [one] => Ok(Pick::One((*one).clone())),
175 _ => Ok(Pick::Several(tables)),
176 }
177}
178
179#[derive(Default)]
183pub struct Opened {
184 pub window: Option<(
187 std::sync::Arc<dyn crate::formats::pushdown::Windowed>,
188 usize,
189 )>,
190 pub detail: Option<std::sync::Arc<crate::formats::text_formats::Detail>>,
191 pub other_tables: Vec<String>,
192 pub notes: Vec<crate::notes::Note>,
193 pub units: Vec<(String, String)>,
195 pub indexing: Option<std::sync::Arc<crate::formats::lines::Lines>>,
197 pub numbering: Option<std::sync::Arc<crate::formats::lines::Lines>>,
200}
201
202impl Opened {
203 pub fn for_table(
206 detail: crate::formats::text_formats::Detail,
207 tables: &[Table],
208 picked: &str,
209 notes: Vec<String>,
210 scope: &str,
211 ) -> Self {
212 Self {
213 detail: Some(std::sync::Arc::new(detail)),
214 other_tables: others(tables, picked),
215 notes: notes
216 .into_iter()
217 .map(|n| crate::formats::text_formats::note(n, scope.to_string()))
218 .collect(),
219 ..Default::default()
220 }
221 }
222
223 pub(crate) fn scan(
225 self,
226 input: crate::formats::readers::ScanIn<'_>,
227 lf: polars::prelude::LazyFrame,
228 ) -> crate::loading::scan::Scan {
229 input.report.opened = Some(std::sync::Arc::new(self));
230 lf.into()
231 }
232}
233
234pub(crate) fn several(
237 input: &crate::formats::readers::ScanIn<'_>,
238 tables: Vec<Table>,
239) -> crate::loading::scan::Scan {
240 crate::loading::scan::Scan::Tables {
241 file: input.path().to_path_buf(),
242 tables: tables
243 .into_iter()
244 .filter(|t| !t.internal)
245 .map(|t| t.name)
246 .collect(),
247 format: input.format,
248 }
249}
250
251impl std::fmt::Debug for Opened {
252 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
253 f.debug_struct("Opened")
254 .field("rows", &self.window.as_ref().map(|(_, rows)| rows))
255 .field("other_tables", &self.other_tables)
256 .finish_non_exhaustive()
257 }
258}
259
260pub fn others(tables: &[Table], opened: &str) -> Vec<String> {
262 tables
263 .iter()
264 .filter(|t| !t.internal && t.name != opened)
265 .map(|t| t.name.clone())
266 .collect()
267}
268
269#[cfg(test)]
270mod tests {
271 use super::*;
272
273 #[test]
274 fn a_place_inside_a_file_reads_back_whole() {
275 let dir = tempfile::tempdir().unwrap();
276 let db = dir.path().join("app.db");
277 let mut head = crate::formats::sqlite::MAGIC.to_vec();
278 head.resize(100, 0);
279 std::fs::write(&db, head).unwrap();
280 for name in ["users", "a/b", "/etc", "a/../b"] {
281 let place = place(&db, name);
282 assert_eq!(
283 split(&place),
284 Some((db.clone(), name.to_string())),
285 "{name}"
286 );
287 }
288 assert_eq!(split(&db), None);
289 let text = dir.path().join("notes.txt");
290 std::fs::write(&text, "hello").unwrap();
291 assert_eq!(split(&text.join("users")), None);
292 }
293
294 #[test]
295 fn a_place_inside_a_file_of_variants_names_one() {
296 let spec = crate::formats::Spec::parse(
297 r#"name = "acme.v"
298match = { glob = ["*.v"] }
299[records]
300framing = "length_prefixed"
301size = "len"
302type = "kind"
303fields = [{ name = "len", type = "u1" }, { name = "kind", type = "u1" }]
304[[variants]]
305name = "Add"
306when = 1
307fields = [{ name = "a", type = "u1" }]
308[[variants]]
309name = "exec"
310when = 2
311fields = [{ name = "b", type = "u1" }]"#,
312 None,
313 )
314 .unwrap();
315 let formats = crate::formats::Registry::of(vec![spec]);
316 let dir = tempfile::tempdir().unwrap();
317 let file = dir.path().join("day.v");
318 std::fs::write(&file, [3u8, 1, 7]).unwrap();
319 assert_eq!(
320 split_variant(&place(&file, "add"), &formats),
321 Some((file.clone(), "Add".to_string())),
322 "as the spec writes it"
323 );
324 assert_eq!(split_variant(&place(&file, "nope"), &formats), None);
325 assert_eq!(split_variant(&file, &formats), None);
326 let other = dir.path().join("day.w");
327 std::fs::write(&other, [0u8]).unwrap();
328 assert_eq!(split_variant(&place(&other, "exec"), &formats), None);
329 let (spec, tables) = variants(&file, &formats).unwrap();
330 assert_eq!(spec, "acme.v");
331 assert_eq!(tables[1].columns.len(), 3);
332 }
333
334 #[test]
335 fn pick_names_what_is_there() {
336 let t = |name: &str| Table::plain(name, "array", Vec::<String>::new());
337 let display = Path::new("run.npz");
338 let several = vec![t("x"), t("y")];
339 assert!(matches!(
340 pick(several.clone(), None, display, "").unwrap(),
341 Pick::Several(_)
342 ));
343 assert_eq!(
344 pick(several.clone(), Some("Y"), display, "").unwrap(),
345 Pick::One(t("y"))
346 );
347 let missing = pick(several, Some("z"), display, "")
348 .unwrap_err()
349 .to_string();
350 assert_eq!(
351 missing,
352 "\"run.npz\": No table \"z\". --table names one of its tables: x, y."
353 );
354 let empty = pick(Vec::new(), None, display, "").unwrap_err().to_string();
355 assert_eq!(empty, "\"run.npz\": The file holds no tables.");
356 }
357}