1use std::path::{Path, PathBuf};
10
11use crate::FileFormat;
12pub use crate::sqlite::{Pick, Table};
13
14pub fn holder(path: &Path) -> Option<FileFormat> {
17 crate::readers::sniff_file(path, crate::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 .map(|name| (name, String::new()))
72 .collect::<Vec<_>>()
73 };
74 let common = named(&spec.records.fields);
75 spec.records
76 .variants
77 .iter()
78 .map(|v| Table {
79 name: v.name.clone(),
80 kind: "record type".to_string(),
81 internal: false,
82 columns: common.iter().cloned().chain(named(&v.fields)).collect(),
83 })
84 .collect()
85}
86
87fn split_inside(path: &Path, holds: impl Fn(&Path) -> bool) -> Option<(PathBuf, String)> {
89 if path.file_name().is_some() && path.exists() {
92 return None;
93 }
94 let file = path
95 .ancestors()
96 .skip(1)
97 .take_while(|p| !p.as_os_str().is_empty())
98 .find(|p| p.file_name().is_some() && p.is_file())?;
101 if !holds(file) {
102 return None;
103 }
104 let rest = path.to_str()?.strip_prefix(file.to_str()?)?;
107 let mut chars = rest.chars();
108 chars.next().filter(|c| std::path::is_separator(*c))?;
109 let name = chars.as_str().trim_end_matches(std::path::is_separator);
110 (!name.is_empty()).then(|| (file.to_path_buf(), name.to_string()))
111}
112
113pub fn tables(file: &Path, format: FileFormat) -> color_eyre::Result<Vec<Table>> {
116 match crate::readers::of(format).tables {
117 Some(tables) => tables(file),
118 None => Err(color_eyre::eyre::eyre!(
119 "A {} file holds one table.",
120 format.name()
121 )),
122 }
123 .map_err(|e| crate::error_display::in_file(file, e))
124}
125
126pub fn pick(
130 tables: Vec<Table>,
131 wanted: Option<&str>,
132 display: &Path,
133 empty: &str,
134) -> color_eyre::Result<Pick> {
135 use crate::error_display::FileError;
136 let own: Vec<&Table> = tables.iter().filter(|t| !t.internal).collect();
137 let names = || {
138 const SHOWN: usize = 20;
139 let mut names: Vec<&str> = own.iter().take(SHOWN).map(|t| t.name.as_str()).collect();
140 let more = own.len().saturating_sub(SHOWN);
141 let more = format!("and {more} more");
142 if own.len() > SHOWN {
143 names.push(&more);
144 }
145 names.join(", ")
146 };
147 if let Some(wanted) = wanted {
148 let found = tables.iter().find(|t| t.name == wanted).or_else(|| {
150 let mut alike = tables
151 .iter()
152 .filter(|t| t.name.eq_ignore_ascii_case(wanted));
153 match (alike.next(), alike.next()) {
154 (Some(one), None) => Some(one),
155 _ => None,
156 }
157 });
158 return match found {
159 Some(table) => Ok(Pick::One(table.clone())),
160 None if own.is_empty() => Err(FileError::new(
161 display,
162 format!("no table \"{wanted}\"; the file holds no tables."),
163 )
164 .into()),
165 None => Err(FileError::new(
166 display,
167 format!(
168 "no table \"{wanted}\". --table names one of its tables: {}.",
169 names()
170 ),
171 )
172 .into()),
173 };
174 }
175 match own.as_slice() {
176 [] => Err(FileError::new(display, format!("the file holds no tables.{empty}")).into()),
177 [one] => Ok(Pick::One((*one).clone())),
178 _ => Ok(Pick::Several(tables)),
179 }
180}
181
182#[derive(Default)]
186pub struct Opened {
187 pub window: Option<(std::sync::Arc<dyn crate::pushdown::Windowed>, usize)>,
190 pub detail: Option<std::sync::Arc<crate::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::lines::Lines>>,
197 pub numbering: Option<std::sync::Arc<crate::lines::Lines>>,
200}
201
202impl std::fmt::Debug for Opened {
203 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
204 f.debug_struct("Opened")
205 .field("rows", &self.window.as_ref().map(|(_, rows)| rows))
206 .field("other_tables", &self.other_tables)
207 .finish_non_exhaustive()
208 }
209}
210
211pub fn others(tables: &[Table], opened: &str) -> Vec<String> {
213 tables
214 .iter()
215 .filter(|t| !t.internal && t.name != opened)
216 .map(|t| t.name.clone())
217 .collect()
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223
224 #[test]
225 fn a_place_inside_a_file_reads_back_whole() {
226 let dir = tempfile::tempdir().unwrap();
227 let db = dir.path().join("app.db");
228 let mut head = crate::sqlite::MAGIC.to_vec();
229 head.resize(100, 0);
230 std::fs::write(&db, head).unwrap();
231 for name in ["users", "a/b", "/etc", "a/../b"] {
232 let place = place(&db, name);
233 assert_eq!(
234 split(&place),
235 Some((db.clone(), name.to_string())),
236 "{name}"
237 );
238 }
239 assert_eq!(split(&db), None);
240 let text = dir.path().join("notes.txt");
241 std::fs::write(&text, "hello").unwrap();
242 assert_eq!(split(&text.join("users")), None);
243 }
244
245 #[test]
246 fn a_place_inside_a_file_of_variants_names_one() {
247 let spec = crate::formats::Spec::parse(
248 r#"name = "acme.v"
249match = { glob = ["*.v"] }
250[records]
251framing = "length_prefixed"
252size = "len"
253type = "kind"
254fields = [{ name = "len", type = "u1" }, { name = "kind", type = "u1" }]
255[[variants]]
256name = "Add"
257when = 1
258fields = [{ name = "a", type = "u1" }]
259[[variants]]
260name = "exec"
261when = 2
262fields = [{ name = "b", type = "u1" }]"#,
263 None,
264 )
265 .unwrap();
266 let formats = crate::formats::Registry::of(vec![spec]);
267 let dir = tempfile::tempdir().unwrap();
268 let file = dir.path().join("day.v");
269 std::fs::write(&file, [3u8, 1, 7]).unwrap();
270 assert_eq!(
271 split_variant(&place(&file, "add"), &formats),
272 Some((file.clone(), "Add".to_string())),
273 "as the spec writes it"
274 );
275 assert_eq!(split_variant(&place(&file, "nope"), &formats), None);
276 assert_eq!(split_variant(&file, &formats), None);
277 let other = dir.path().join("day.w");
278 std::fs::write(&other, [0u8]).unwrap();
279 assert_eq!(split_variant(&place(&other, "exec"), &formats), None);
280 let (spec, tables) = variants(&file, &formats).unwrap();
281 assert_eq!(spec, "acme.v");
282 assert_eq!(tables[1].columns.len(), 3);
283 }
284
285 #[test]
286 fn pick_names_what_is_there() {
287 let t = |name: &str| Table {
288 name: name.into(),
289 kind: "array".into(),
290 internal: false,
291 columns: Vec::new(),
292 };
293 let display = Path::new("run.npz");
294 let several = vec![t("x"), t("y")];
295 assert!(matches!(
296 pick(several.clone(), None, display, "").unwrap(),
297 Pick::Several(_)
298 ));
299 assert_eq!(
300 pick(several.clone(), Some("Y"), display, "").unwrap(),
301 Pick::One(t("y"))
302 );
303 let missing = pick(several, Some("z"), display, "")
304 .unwrap_err()
305 .to_string();
306 assert_eq!(
307 missing,
308 "\"run.npz\": No table \"z\". --table names one of its tables: x, y."
309 );
310 let empty = pick(Vec::new(), None, display, "").unwrap_err().to_string();
311 assert_eq!(empty, "\"run.npz\": The file holds no tables.");
312 }
313}