Skip to main content

mint_core/
build.rs

1use crate::data::DataSource;
2use crate::error::MintError;
3use crate::layout;
4use crate::layout::block::Config;
5use crate::layout::error::LayoutError;
6use crate::layout::used_values::{NoopValueSink, ValueCollector};
7use crate::output;
8use crate::output::error::OutputError;
9use crate::output::{DataRange, OutputFile, OutputFormat};
10use rayon::prelude::*;
11use std::collections::{HashMap, HashSet};
12use std::time::{Duration, Instant};
13
14#[derive(Debug, Clone)]
15pub struct BlockStat {
16    pub name: String,
17    pub start_address: u32,
18    pub allocated_size: u32,
19    pub used_size: u32,
20    pub checksum_values: Vec<u32>,
21}
22
23#[derive(Debug)]
24pub struct BuildStats {
25    pub blocks_processed: usize,
26    pub total_allocated: usize,
27    pub total_used: usize,
28    pub total_duration: Duration,
29    pub block_stats: Vec<BlockStat>,
30}
31
32impl Default for BuildStats {
33    fn default() -> Self {
34        Self::new()
35    }
36}
37
38impl BuildStats {
39    pub fn new() -> Self {
40        Self {
41            blocks_processed: 0,
42            total_allocated: 0,
43            total_used: 0,
44            total_duration: Duration::from_secs(0),
45            block_stats: Vec::new(),
46        }
47    }
48
49    pub fn add_block(&mut self, stat: BlockStat) {
50        self.blocks_processed += 1;
51        self.total_allocated += stat.allocated_size as usize;
52        self.total_used += stat.used_size as usize;
53        self.block_stats.push(stat);
54    }
55
56    pub fn space_used_pct(&self) -> f64 {
57        if self.total_allocated == 0 {
58            0.0
59        } else {
60            (self.total_used as f64 / self.total_allocated as f64) * 100.0
61        }
62    }
63}
64
65#[derive(Clone)]
66pub struct BuildRequest<'a> {
67    pub blocks: Vec<BlockNames>,
68    pub data_source: Option<&'a dyn DataSource>,
69    pub strict: bool,
70    pub capture_values: bool,
71}
72
73#[derive(Debug)]
74pub struct NamedLayout {
75    pub name: String,
76    pub config: Config,
77}
78
79pub struct BuildFromLayoutsRequest<'a> {
80    pub layouts: Vec<NamedLayout>,
81    pub blocks: Vec<BlockNames>,
82    pub data_source: Option<&'a dyn DataSource>,
83    pub strict: bool,
84    pub capture_values: bool,
85}
86
87#[derive(Debug)]
88pub struct BuildArtifact {
89    pub ranges: Vec<DataRange>,
90    pub stats: BuildStats,
91    pub used_values: Option<serde_json::Value>,
92}
93
94impl BuildArtifact {
95    pub fn output_file(&self, format: OutputFormat, record_width: usize) -> OutputFile {
96        OutputFile {
97            ranges: self.ranges.clone(),
98            format,
99            record_width,
100        }
101    }
102
103    pub fn render(&self, format: OutputFormat, record_width: usize) -> Result<String, OutputError> {
104        self.output_file(format, record_width).render()
105    }
106}
107
108#[derive(Debug, Clone)]
109pub struct BlockNames {
110    pub name: String,
111    pub file: String,
112}
113
114#[derive(Debug, Clone)]
115struct ResolvedBlock {
116    name: String,
117    file: String,
118}
119
120struct BlockBuildResult {
121    block_names: BlockNames,
122    data_range: DataRange,
123    stat: BlockStat,
124    used_values: Option<serde_json::Value>,
125}
126
127pub fn build(request: BuildRequest<'_>) -> Result<BuildArtifact, MintError> {
128    if request.blocks.is_empty() {
129        return Err(LayoutError::NoBlocksProvided.into());
130    }
131
132    let (resolved_blocks, layouts) = resolve_blocks(&request.blocks)?;
133    build_resolved(
134        resolved_blocks,
135        &layouts,
136        request.data_source,
137        request.strict,
138        request.capture_values,
139    )
140}
141
142pub fn build_from_layouts(
143    request: BuildFromLayoutsRequest<'_>,
144) -> Result<BuildArtifact, MintError> {
145    if request.blocks.is_empty() {
146        return Err(LayoutError::NoBlocksProvided.into());
147    }
148
149    let layouts = collect_named_layouts(request.layouts)?;
150    let resolved_blocks = resolve_blocks_from_layouts(&request.blocks, &layouts)?;
151    build_resolved(
152        resolved_blocks,
153        &layouts,
154        request.data_source,
155        request.strict,
156        request.capture_values,
157    )
158}
159
160fn build_resolved(
161    resolved_blocks: Vec<ResolvedBlock>,
162    layouts: &HashMap<String, Config>,
163    data_source: Option<&dyn DataSource>,
164    strict: bool,
165    capture_values: bool,
166) -> Result<BuildArtifact, MintError> {
167    let start_time = Instant::now();
168    let mut results = build_bytestreams(
169        &resolved_blocks,
170        layouts,
171        data_source,
172        strict,
173        capture_values,
174    )?;
175
176    let used_values = if capture_values {
177        Some(take_used_values_report(&mut results)?)
178    } else {
179        None
180    };
181
182    let (ranges, mut stats) = collect_results(results)?;
183    stats.total_duration = start_time.elapsed();
184
185    Ok(BuildArtifact {
186        ranges,
187        stats,
188        used_values,
189    })
190}
191
192fn collect_named_layouts(
193    layouts: Vec<NamedLayout>,
194) -> Result<HashMap<String, Config>, LayoutError> {
195    let mut out = HashMap::with_capacity(layouts.len());
196    for layout in layouts {
197        if out.insert(layout.name.clone(), layout.config).is_some() {
198            return Err(LayoutError::FileError(format!(
199                "duplicate layout name '{}'",
200                layout.name
201            )));
202        }
203    }
204    Ok(out)
205}
206
207fn resolve_blocks_from_layouts(
208    block_args: &[BlockNames],
209    layouts: &HashMap<String, Config>,
210) -> Result<Vec<ResolvedBlock>, LayoutError> {
211    let mut resolved = Vec::new();
212    for arg in block_args {
213        let layout = layouts.get(&arg.file).ok_or_else(|| {
214            let available = layouts.keys().cloned().collect::<Vec<_>>().join(", ");
215            LayoutError::BlockNotFound(format!(
216                "layout '{}'. Available layouts: {}",
217                arg.file, available
218            ))
219        })?;
220
221        if arg.name.is_empty() {
222            for block_name in layout.blocks.keys() {
223                resolved.push(ResolvedBlock {
224                    name: block_name.clone(),
225                    file: arg.file.clone(),
226                });
227            }
228        } else if layout.blocks.contains_key(&arg.name) {
229            resolved.push(ResolvedBlock {
230                name: arg.name.clone(),
231                file: arg.file.clone(),
232            });
233        } else {
234            let available_blocks = layout.blocks.keys().cloned().collect::<Vec<_>>().join(", ");
235            return Err(LayoutError::BlockNotFound(format!(
236                "'{}' in '{}'. Available blocks: {}",
237                arg.name, arg.file, available_blocks
238            )));
239        }
240    }
241
242    let mut seen = HashSet::new();
243    Ok(resolved
244        .into_iter()
245        .filter(|b| seen.insert((b.file.clone(), b.name.clone())))
246        .collect())
247}
248
249fn resolve_blocks(
250    block_args: &[BlockNames],
251) -> Result<(Vec<ResolvedBlock>, HashMap<String, Config>), LayoutError> {
252    let unique_files: HashSet<String> = block_args.iter().map(|b| b.file.clone()).collect();
253
254    let layouts: Result<HashMap<String, Config>, LayoutError> = unique_files
255        .par_iter()
256        .map(|file| layout::load_layout(file).map(|cfg| (file.clone(), cfg)))
257        .collect();
258
259    let layouts = layouts?;
260
261    let mut resolved = Vec::new();
262    for arg in block_args {
263        if arg.name.is_empty() {
264            let layout = &layouts[&arg.file];
265            for block_name in layout.blocks.keys() {
266                resolved.push(ResolvedBlock {
267                    name: block_name.clone(),
268                    file: arg.file.clone(),
269                });
270            }
271        } else {
272            let layout = &layouts[&arg.file];
273            if !layout.blocks.contains_key(&arg.name) {
274                let available_blocks = layout.blocks.keys().cloned().collect::<Vec<_>>().join(", ");
275                return Err(LayoutError::BlockNotFound(format!(
276                    "'{}' in '{}'. Available blocks: {}",
277                    arg.name, arg.file, available_blocks
278                )));
279            }
280            resolved.push(ResolvedBlock {
281                name: arg.name.clone(),
282                file: arg.file.clone(),
283            });
284        }
285    }
286
287    let mut seen = HashSet::new();
288    let deduplicated: Vec<ResolvedBlock> = resolved
289        .into_iter()
290        .filter(|b| seen.insert((b.file.clone(), b.name.clone())))
291        .collect();
292
293    Ok((deduplicated, layouts))
294}
295
296fn build_bytestreams(
297    blocks: &[ResolvedBlock],
298    layouts: &HashMap<String, Config>,
299    data_source: Option<&dyn DataSource>,
300    strict: bool,
301    capture_values: bool,
302) -> Result<Vec<BlockBuildResult>, MintError> {
303    blocks
304        .par_iter()
305        .map(|resolved| {
306            build_single_bytestream(resolved, layouts, data_source, strict, capture_values)
307        })
308        .collect()
309}
310
311fn build_single_bytestream(
312    resolved: &ResolvedBlock,
313    layouts: &HashMap<String, Config>,
314    data_source: Option<&dyn DataSource>,
315    strict: bool,
316    capture_values: bool,
317) -> Result<BlockBuildResult, MintError> {
318    let result = (|| {
319        let layout = layouts.get(&resolved.file).ok_or_else(|| {
320            LayoutError::FileError(format!(
321                "resolved layout missing from build map: {}",
322                resolved.file
323            ))
324        })?;
325        let block = layout.blocks.get(&resolved.name).ok_or_else(|| {
326            let available_blocks = layout.blocks.keys().cloned().collect::<Vec<_>>().join(", ");
327            LayoutError::BlockNotFound(format!(
328                "'{}' in '{}'. Available blocks: {}",
329                resolved.name, resolved.file, available_blocks
330            ))
331        })?;
332        let mut collector = ValueCollector::new();
333        let mut noop = NoopValueSink;
334        let value_sink = if capture_values {
335            &mut collector as &mut dyn crate::layout::used_values::ValueSink
336        } else {
337            &mut noop as &mut dyn crate::layout::used_values::ValueSink
338        };
339
340        let build_output = block.build_bytestream(data_source, &layout.mint, strict, value_sink)?;
341
342        let data_range = output::bytestream_to_datarange(
343            build_output.bytestream,
344            &block.header,
345            build_output.padding_count,
346        )?;
347
348        let stat = BlockStat {
349            name: resolved.name.clone(),
350            start_address: data_range.start_address,
351            allocated_size: data_range.allocated_size,
352            used_size: data_range.used_size,
353            checksum_values: build_output.checksum_values,
354        };
355
356        Ok(BlockBuildResult {
357            block_names: BlockNames {
358                name: resolved.name.clone(),
359                file: resolved.file.clone(),
360            },
361            data_range,
362            stat,
363            used_values: capture_values.then(|| collector.into_value()),
364        })
365    })();
366
367    result.map_err(|e| MintError::InBlock {
368        block_name: resolved.name.clone(),
369        layout_file: resolved.file.clone(),
370        source: Box::new(e),
371    })
372}
373
374fn collect_results(
375    results: Vec<BlockBuildResult>,
376) -> Result<(Vec<DataRange>, BuildStats), MintError> {
377    let mut stats = BuildStats::new();
378    let named_ranges: Vec<(String, DataRange)> = results
379        .into_iter()
380        .map(|r| {
381            stats.add_block(r.stat);
382            (r.block_names.name, r.data_range)
383        })
384        .collect();
385
386    check_overlaps(&named_ranges)?;
387    let ranges = named_ranges.into_iter().map(|(_, r)| r).collect();
388    Ok((ranges, stats))
389}
390
391fn check_overlaps(named_ranges: &[(String, DataRange)]) -> Result<(), MintError> {
392    for i in 0..named_ranges.len() {
393        for j in (i + 1)..named_ranges.len() {
394            let (ref name_a, ref range_a) = named_ranges[i];
395            let (ref name_b, ref range_b) = named_ranges[j];
396            let a_start = range_a.start_address;
397            let a_end = a_start + range_a.allocated_size;
398            let b_start = range_b.start_address;
399            let b_end = b_start + range_b.allocated_size;
400
401            let overlap_start = a_start.max(b_start);
402            let overlap_end = a_end.min(b_end);
403
404            if overlap_start < overlap_end {
405                let overlap_size = overlap_end - overlap_start;
406                let msg = format!(
407                    "Block '{}' (0x{:08X}-0x{:08X}) overlaps with block '{}' (0x{:08X}-0x{:08X}). Overlap: 0x{:08X}-0x{:08X} ({} bytes)",
408                    name_a,
409                    a_start,
410                    a_end - 1,
411                    name_b,
412                    b_start,
413                    b_end - 1,
414                    overlap_start,
415                    overlap_end - 1,
416                    overlap_size
417                );
418                return Err(OutputError::BlockOverlapError(msg).into());
419            }
420        }
421    }
422    Ok(())
423}
424
425fn take_used_values_report(
426    results: &mut [BlockBuildResult],
427) -> Result<serde_json::Value, MintError> {
428    let mut report = serde_json::Map::new();
429    for result in results {
430        let value = result.used_values.take().ok_or_else(|| {
431            OutputError::FileError("JSON export requested but values were not captured.".to_owned())
432        })?;
433        let file_entry = report
434            .entry(result.block_names.file.clone())
435            .or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
436        let serde_json::Value::Object(blocks) = file_entry else {
437            return Err(OutputError::FileError(
438                "JSON export contains unexpected non-object entry.".to_owned(),
439            )
440            .into());
441        };
442        if blocks.contains_key(&result.block_names.name) {
443            return Err(OutputError::FileError(format!(
444                "Duplicate block '{}' in JSON export for file '{}'.",
445                result.block_names.name, result.block_names.file
446            ))
447            .into());
448        }
449        blocks.insert(result.block_names.name.clone(), value);
450    }
451    Ok(serde_json::Value::Object(report))
452}