Skip to main content

objects/blame/
finalize.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Build canonical FileProvenance from a completed origin stream.
3
4use crate::object::{ContentHash, FileProvenance, Origin, OriginSet};
5
6use super::types::{BlameSliceError, OriginRange};
7
8/// Assemble [`FileProvenance`] from finalized ranges without holding history.
9///
10/// Ranges are sorted, checked for exact coverage, and coalesced when adjacent
11/// ranges share the same origin.
12pub fn finalize_file_provenance(
13    file_blob: ContentHash,
14    line_count: u32,
15    ranges: impl IntoIterator<Item = OriginRange>,
16) -> Result<FileProvenance, BlameSliceError> {
17    let mut ranges: Vec<OriginRange> = ranges.into_iter().collect();
18    ranges.sort_by_key(|range| range.target_start);
19
20    if line_count == 0 {
21        if ranges
22            .iter()
23            .any(|range| range.target_start != 0 || range.len != 0)
24        {
25            return Err(BlameSliceError::InvalidCoverage);
26        }
27        let origins = ranges
28            .first()
29            .map(|range| vec![range.origin.clone()])
30            .unwrap_or_default();
31        let origin_sets = if origins.is_empty() {
32            Vec::new()
33        } else {
34            vec![OriginSet {
35                origin_indexes: vec![0],
36            }]
37        };
38        let provenance = FileProvenance::new(file_blob, 0, Vec::new(), origins, origin_sets);
39        provenance
40            .validate()
41            .map_err(|_| BlameSliceError::InvalidCoverage)?;
42        return Ok(provenance);
43    }
44
45    let coalesced = coalesce(ranges);
46    let mut next_line = 0u32;
47    let mut origins: Vec<Origin> = Vec::new();
48    let mut origin_sets = Vec::new();
49    let mut spans = Vec::new();
50
51    for range in coalesced {
52        if range.target_start != next_line || range.len == 0 {
53            return Err(BlameSliceError::InvalidCoverage);
54        }
55        let origin_index = origin_index(&mut origins, range.origin);
56        let origin_set_index = origin_set_index(&mut origin_sets, origin_index);
57        spans.push(crate::object::LineSpan {
58            start_line: range.target_start,
59            line_len: range.len,
60            origin_set_index,
61        });
62        next_line = next_line.saturating_add(range.len);
63    }
64    if next_line != line_count {
65        return Err(BlameSliceError::InvalidCoverage);
66    }
67
68    let provenance = FileProvenance::new(file_blob, line_count, spans, origins, origin_sets);
69    provenance
70        .validate()
71        .map_err(|_| BlameSliceError::InvalidCoverage)?;
72    Ok(provenance)
73}
74
75fn coalesce(ranges: Vec<OriginRange>) -> Vec<OriginRange> {
76    let mut out: Vec<OriginRange> = Vec::new();
77    for range in ranges {
78        match out.last_mut() {
79            Some(last)
80                if last.target_start + last.len == range.target_start
81                    && last.origin == range.origin =>
82            {
83                last.len += range.len;
84            }
85            _ => out.push(range),
86        }
87    }
88    out
89}
90
91fn origin_index(origins: &mut Vec<Origin>, origin: Origin) -> u32 {
92    if let Some((index, _)) = origins
93        .iter()
94        .enumerate()
95        .find(|(_, existing)| **existing == origin)
96    {
97        return index as u32;
98    }
99    let next = origins.len() as u32;
100    origins.push(origin);
101    next
102}
103
104fn origin_set_index(origin_sets: &mut Vec<OriginSet>, origin_index: u32) -> u32 {
105    let indexes = vec![origin_index];
106    if let Some((index, _)) = origin_sets
107        .iter()
108        .enumerate()
109        .find(|(_, set)| set.origin_indexes == indexes)
110    {
111        return index as u32;
112    }
113    let next = origin_sets.len() as u32;
114    origin_sets.push(OriginSet {
115        origin_indexes: indexes,
116    });
117    next
118}