use std::{error::Error, fmt, ops::Range};
use crate::{Finding, ScanReport};
mod pseudonymization;
mod redact;
mod share_bundle;
mod synthesize;
mod template;
pub use pseudonymization::{PseudonymizationOptions, pseudonymize};
pub use redact::{redact, redact_with};
pub use share_bundle::{
ShareBundle, ShareBundleBuilder, ShareManifest, ShareMode, ShareModeKind, TransformedSource,
};
pub use synthesize::{SynthesisOptions, synthesize};
pub use template::{TemplateOptions, template, template_with};
#[derive(Debug, Clone, Eq, PartialEq)]
#[non_exhaustive]
pub enum TransformError {
InvalidSpan {
start: usize,
end: usize,
source_len: usize,
},
InvalidUtf8Boundary {
start: usize,
end: usize,
},
OverlappingSpans {
first: Range<usize>,
second: Range<usize>,
},
MissingShareMode,
SourceCountMismatch {
expected: usize,
actual: usize,
},
SourceLengthMismatch {
index: usize,
expected_bytes: usize,
actual_bytes: usize,
},
}
impl fmt::Display for TransformError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidSpan {
start,
end,
source_len,
} => write!(
formatter,
"invalid transform span {start}..{end} for source length {source_len}",
),
Self::InvalidUtf8Boundary { start, end } => write!(
formatter,
"transform span {start}..{end} is not aligned to UTF-8 character boundaries",
),
Self::OverlappingSpans { first, second } => write!(
formatter,
"transform spans {}..{} and {}..{} overlap",
first.start, first.end, second.start, second.end,
),
Self::MissingShareMode => {
formatter.write_str("share bundle transformation mode was not configured")
}
Self::SourceCountMismatch { expected, actual } => write!(
formatter,
"share bundle expected {expected} sources but received {actual}",
),
Self::SourceLengthMismatch {
index,
expected_bytes,
actual_bytes,
} => write!(
formatter,
"share bundle source {index} has {actual_bytes} bytes but scan results record {expected_bytes}",
),
}
}
}
impl Error for TransformError {}
#[derive(Debug, Copy, Clone, PartialEq)]
pub(crate) struct TransformSpan<'a> {
pub(crate) finding: &'a Finding,
pub(crate) start: usize,
pub(crate) end: usize,
}
impl TransformSpan<'_> {
pub(crate) const fn range(self) -> Range<usize> {
self.start..self.end
}
}
pub(crate) fn validated_spans<'a>(
source: &str,
report: &'a ScanReport,
) -> Result<Vec<TransformSpan<'a>>, TransformError> {
let mut spans = Vec::with_capacity(report.len());
for finding in report {
let location = finding.location();
let start = location.start();
let end = location.end();
if start >= end || end > source.len() {
return Err(TransformError::InvalidSpan {
start,
end,
source_len: source.len(),
});
}
if !source.is_char_boundary(start) || !source.is_char_boundary(end) {
return Err(TransformError::InvalidUtf8Boundary { start, end });
}
spans.push(TransformSpan {
finding,
start,
end,
});
}
spans.sort_by(|left, right| {
left.start
.cmp(&right.start)
.then_with(|| left.end.cmp(&right.end))
.then_with(|| {
left.finding
.rule_id()
.as_str()
.cmp(right.finding.rule_id().as_str())
})
});
Ok(spans)
}
pub(crate) fn ensure_non_overlapping(spans: &[TransformSpan<'_>]) -> Result<(), TransformError> {
for pair in spans.windows(2) {
let first = pair[0];
let second = pair[1];
if second.start < first.end {
return Err(TransformError::OverlappingSpans {
first: first.range(),
second: second.range(),
});
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Confidence, Location, RuleId, Severity};
fn finding(id: &str, start: usize, end: usize) -> Finding {
Finding::new(
RuleId::from(id),
Location::from_span(start, end),
Severity::High,
Confidence::High,
None,
)
}
#[test]
fn validates_and_sorts_spans() {
let report = ScanReport::new_with_candidates(
vec![finding("later", 5, 8), finding("first", 0, 2)],
Vec::new(),
);
let spans = validated_spans("abcdefgh", &report).unwrap();
assert_eq!(spans[0].range(), 0..2);
assert_eq!(spans[1].range(), 5..8);
}
#[test]
fn rejects_out_of_bounds_span() {
let report = ScanReport::new_with_candidates(vec![finding("bad", 0, 10)], Vec::new());
assert_eq!(
validated_spans("short", &report),
Err(TransformError::InvalidSpan {
start: 0,
end: 10,
source_len: 5,
}),
);
}
#[test]
fn rejects_non_utf8_boundary_span() {
let report = ScanReport::new_with_candidates(vec![finding("bad", 1, 4)], Vec::new());
assert_eq!(
validated_spans("😀x", &report),
Err(TransformError::InvalidUtf8Boundary { start: 1, end: 4 }),
);
}
#[test]
fn detects_overlapping_spans_for_strict_transformations() {
let report = ScanReport::new_with_candidates(
vec![finding("first", 1, 5), finding("second", 3, 7)],
Vec::new(),
);
let spans = validated_spans("abcdefgh", &report).unwrap();
assert_eq!(
ensure_non_overlapping(&spans),
Err(TransformError::OverlappingSpans {
first: 1..5,
second: 3..7,
}),
);
}
}