Skip to content

Commit 2e03ed6

Browse files
committed
Add a Package conception served as a set of linked KSYs and implement imports loading
1 parent e0fbffa commit 2e03ed6

File tree

3 files changed

+277
-3
lines changed

3 files changed

+277
-3
lines changed

src/lib.rs

+70-3
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,66 @@ pub mod parser;
1212
/// the `test-data` in the crate root directory, next to `src`.
1313
#[cfg(test)]
1414
mod formats {
15-
use crate::model::Root;
16-
use crate::parser::Ksy;
15+
use crate::model::{ImportLoader, Package};
16+
use crate::parser::{Import, Ksy};
1717
use std::fs::File;
18+
use std::io;
19+
use std::path::{Path, PathBuf};
1820
use test_generator::test_resources;
1921

22+
#[derive(Debug)]
23+
// Values of enumeration is not used directly, but still valuable if test fail
24+
#[allow(dead_code)]
25+
enum LoaderError {
26+
Io(String, io::Error),
27+
Parse(serde_yml::Error),
28+
}
29+
30+
fn to_path(mut base: PathBuf, import: &Import) -> PathBuf {
31+
for comp in &import.components {
32+
base.push(comp);
33+
}
34+
base.set_extension("ksy");
35+
base
36+
}
37+
38+
struct FileLoader {
39+
/// Bases for absolute imports
40+
abs_roots: Vec<PathBuf>,
41+
}
42+
impl ImportLoader for FileLoader {
43+
type Id = PathBuf;
44+
type Error = LoaderError;
45+
46+
fn new_id(&mut self, mut base: Self::Id, import: &Import) -> Self::Id {
47+
if import.absolute {
48+
for base in self.abs_roots.iter().cloned() {
49+
let path = to_path(base, import);
50+
// Required for tests which expect that absolute import will look into several places
51+
// test-data/formats/imports_abs.ksy
52+
// test-data/formats/imports_abs_abs.ksy
53+
// test-data/formats/imports_abs_rel.ksy
54+
// test-data/formats/ks_path/for_abs_imports/imported_and_abs.ksy
55+
if path.exists() {
56+
return path;
57+
}
58+
}
59+
// In tests we should find file in one of the provided roots
60+
panic!("cannot find file for {import}");
61+
} else {
62+
// Remove name of the file from which we are imported
63+
base.pop();
64+
to_path(base, import)
65+
}
66+
}
67+
68+
fn load(&mut self, id: Self::Id) -> Result<Ksy, Self::Error> {
69+
let display = id.display().to_string();
70+
let file = File::open(id).map_err(|e| LoaderError::Io(display, e))?;
71+
serde_yml::from_reader(file).map_err(LoaderError::Parse)
72+
}
73+
}
74+
2075
#[test_resources("formats/**/*.ksy")]
2176
#[test_resources("test-data/formats/**/*.ksy")]
2277
fn parse(resource: &str) {
@@ -44,7 +99,19 @@ mod formats {
4499
return;
45100
}
46101

47-
let _ = Root::validate(&ksy).expect(&format!("incorrect KSY {}", resource));
102+
let id = Path::new(resource).to_path_buf();
103+
// Directory with `ksc` crate
104+
let ksc_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
105+
106+
let package = Package::new(id, ksy, FileLoader {
107+
abs_roots: vec![
108+
ksc_dir.join("formats"),
109+
ksc_dir.join("test-data").join("formats"),
110+
ksc_dir.join("test-data").join("formats").join("ks_path"),
111+
],
112+
}).expect(&format!("invalid imports in {}", resource));
113+
114+
package.validate().expect(&format!("incorrect KSY {}", resource));
48115
}
49116

50117
#[test_resources("test-data/formats_err/**/*.ksy")]

src/model/mod.rs

+4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
//! Unlike structures from [`parser`] module that module contains validated
33
//! structures, that represent only valid constructs of kaitai struct language.
44
//!
5+
//! The entry point is a [`Package`] struct.
6+
//!
57
//! [`parser`]: crate::parser
68
79
use std::cmp;
@@ -24,11 +26,13 @@ use crate::parser::expressions::{parse_process, parse_type_ref, AttrType};
2426
mod r#enum;
2527
pub mod expressions;
2628
mod name;
29+
mod package;
2730
mod r#type;
2831

2932
pub use name::{
3033
EnumName, EnumPath, EnumValueName, FieldName, Name, OptionalName, ParamName, SeqName, TypeName,
3134
};
35+
pub use package::{ImportLoader, Package};
3236
pub use r#enum::Enum;
3337
pub use r#type::{Root, UserType, UserTypeRef};
3438

src/model/package.rs

+203
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
use std::collections::{HashSet, VecDeque};
2+
use std::hash::Hash;
3+
4+
use crate::error::ModelError;
5+
use crate::model::Root;
6+
use crate::parser::{Import, Ksy};
7+
8+
/// Loader used to handle imported kaitai types in `meta.imports` section.
9+
pub trait ImportLoader {
10+
/// The identifier of a file to load. User to cache information about already
11+
/// loaded files to prevent double loading. Inferred using [`Self::new_id`]
12+
/// from a file which imports another file and import specification.
13+
///
14+
/// File-based implementations usually will use `std::path::PathBuf` for that.
15+
type Id: Clone + Hash + Eq;
16+
/// The type of error that loader could return during reading imported file.
17+
type Error;
18+
19+
/// Derives id of imported file from the import specification and ID of a file
20+
/// that imports it.
21+
///
22+
/// This method will be called at least the same count as [`Self::load`], but
23+
/// not each call of this method will be followed by [`Self::load`].
24+
///
25+
/// # Parameters
26+
/// - `base`: ID of a file which imports another file, described by the `import` parameter
27+
/// - `import`: specification of the imported file as written in a KSY
28+
///
29+
/// Returns an ID of a file to load. The file by that ID does not necessary exist,
30+
/// the returned ID is only the pointer where the loader is expect to find it.
31+
/// Actual checks should be performed in the [`Self::load`] method.
32+
fn new_id(&mut self, base: Self::Id, import: &Import) -> Self::Id;
33+
34+
/// Perform loading of one imported file (entry from `meta.imports[i]`).
35+
/// This method may be called not for every call to [`Self::new_id`].
36+
///
37+
/// # Parameters
38+
/// - `id`: ID the file to load, previously generated by the [`Self::new_id`] method.
39+
fn load(&mut self, id: Self::Id) -> Result<Ksy, Self::Error>;
40+
}
41+
42+
////////////////////////////////////////////////////////////////////////////////////////////////////
43+
44+
/// Package contains information about the main kaitai struct file and all directly
45+
/// and indirectly imported files. Validation of a package produces model that is used
46+
/// by code generators.
47+
#[derive(Debug, Clone)]
48+
pub struct Package {
49+
/// The list of files that represents one translation unit -- a set of files
50+
/// that should be processed together because they are linked by import relations.
51+
///
52+
/// Usually this list is filled automatically by creating a package using [`Self::new`] method.
53+
pub files: Vec<Ksy>,
54+
}
55+
impl Package {
56+
/// Recursively loads all imported files using the provided loader,
57+
/// returning a package with all loaded files.
58+
///
59+
/// Returns the first error returned by the loader.
60+
pub fn new<L>(mut id: L::Id, mut ksy: Ksy, mut loader: L) -> Result<Self, L::Error>
61+
where
62+
L: ImportLoader,
63+
{
64+
let mut to_load = VecDeque::new();
65+
let mut loaded = HashSet::new();
66+
let mut files = Vec::new();
67+
68+
'external: loop {
69+
if let Some(imports) = &ksy.meta.imports {
70+
for import in imports {
71+
let new_id = loader.new_id(id.clone(), import);
72+
// If such file was already loaded, do not try to load it again
73+
if new_id != id && !loaded.contains(&new_id) {
74+
to_load.push_back(new_id);
75+
}
76+
}
77+
}
78+
loaded.insert(id);
79+
files.push(ksy);
80+
81+
while let Some(new_id) = to_load.pop_front() {
82+
// Even when we filter already loaded files before insert into `to_load`,
83+
// we still can insert duplicated entries to `to_load` because we can have
84+
// two files, that requested loading of the same file, but which have not
85+
// yet tried to load
86+
if loaded.contains(&new_id) {
87+
continue;
88+
}
89+
id = new_id;
90+
ksy = loader.load(id.clone())?;
91+
continue 'external;
92+
}
93+
break;
94+
}
95+
96+
Ok(Self { files })
97+
}
98+
99+
/// Performs validation of a set of KS files and create a list of models for them.
100+
pub fn validate(self) -> Result<Vec<Root>, ModelError> {
101+
self.files.iter().map(Root::validate).collect()
102+
}
103+
}
104+
105+
////////////////////////////////////////////////////////////////////////////////////////////////////
106+
107+
#[test]
108+
fn import() {
109+
use pretty_assertions::assert_eq;
110+
use std::collections::{BTreeMap, HashSet};
111+
112+
const KSY1: &str = "
113+
meta:
114+
id: ksy1
115+
imports:
116+
- ksy2
117+
- nested/ksy3
118+
";
119+
const KSY2: &str = "
120+
meta:
121+
id: ksy2
122+
imports:
123+
- /ksy1
124+
- /nested/ksy3
125+
";
126+
const KSY3: &str = "
127+
meta:
128+
id: ksy3
129+
imports:
130+
- ../ksy1
131+
- ../ksy2
132+
- ksy4
133+
";
134+
const KSY4: &str = "
135+
meta:
136+
id: ksy4
137+
";
138+
139+
struct SimpleLoader<'s> {
140+
fs: BTreeMap<&'static str, &'static str>,
141+
already_read: &'s mut HashSet<String>,
142+
}
143+
impl<'s> ImportLoader for SimpleLoader<'s> {
144+
type Id = Vec<String>;
145+
type Error = serde_yml::Error;
146+
147+
fn new_id(&mut self, mut base: Self::Id, import: &Import) -> Self::Id {
148+
if import.absolute {
149+
// Start from scratch
150+
base = Vec::new();
151+
} else {
152+
// Remove name of the file from which we are imported
153+
base.pop();
154+
}
155+
// Add relative path
156+
for comp in &import.components {
157+
if comp == ".." {
158+
base.pop();
159+
} else {
160+
base.push(comp.into());
161+
}
162+
}
163+
base
164+
}
165+
166+
fn load(&mut self, id: Self::Id) -> Result<Ksy, Self::Error> {
167+
let key = id.join("/");
168+
let ksy = self.fs.get(key.as_str()).expect(&format!("{key} was not found in File System"));
169+
// loader should not be called twice for already processed ID
170+
assert_eq!(
171+
self.already_read.insert(key.clone()),
172+
true,
173+
"{key} read more that once: {:?}",
174+
self.already_read
175+
);
176+
177+
serde_yml::from_str(ksy)
178+
}
179+
}
180+
181+
let start_id = "ksy1".to_string();
182+
let ksy1: Ksy = serde_yml::from_str(KSY1).expect("`ksy1` not loaded");
183+
184+
// list of already read files
185+
let mut already_read = HashSet::new();
186+
already_read.insert(start_id.clone());
187+
188+
// Test File System with set of files (ID -> file content)
189+
let loader = SimpleLoader {
190+
fs: BTreeMap::from_iter([
191+
("ksy1", KSY1),
192+
("ksy2", KSY2),
193+
("nested/ksy3", KSY3),
194+
("nested/ksy4", KSY4),
195+
]),
196+
already_read: &mut already_read,
197+
};
198+
199+
let package = Package::new(vec![start_id], ksy1, loader).unwrap();
200+
201+
assert_eq!(package.files.len(), 4);
202+
assert_eq!(already_read.len(), 4);
203+
}

0 commit comments

Comments
 (0)