use crate::{
file::parser::Parser,
utils::{write_compressed_int, write_compressed_uint},
Result,
};
pub const HIDDEN_SEQUENCE_POINT_LINE: u32 = 0x00FE_EFEE;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SequencePoint {
pub il_offset: u32,
pub start_line: u32,
pub start_col: u16,
pub end_line: u32,
pub end_col: u16,
pub is_hidden: bool,
}
impl SequencePoint {
#[must_use]
pub fn new(
il_offset: u32,
start_line: u32,
start_col: u16,
end_line: u32,
end_col: u16,
) -> Option<Self> {
let is_hidden = start_line == HIDDEN_SEQUENCE_POINT_LINE;
if !is_hidden {
if end_line < start_line {
return None;
}
if end_line == start_line && end_col < start_col {
return None;
}
}
Some(Self {
il_offset,
start_line,
start_col,
end_line,
end_col,
is_hidden,
})
}
#[must_use]
pub fn hidden(il_offset: u32) -> Self {
Self {
il_offset,
start_line: HIDDEN_SEQUENCE_POINT_LINE,
start_col: 0,
end_line: HIDDEN_SEQUENCE_POINT_LINE,
end_col: 0,
is_hidden: true,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct SequencePoints(pub Vec<SequencePoint>);
impl SequencePoints {
#[must_use]
pub fn find_by_il_offset(&self, il_offset: u32) -> Option<&SequencePoint> {
self.0.iter().find(|sp| sp.il_offset == il_offset)
}
#[must_use]
pub fn to_bytes(&self) -> Vec<u8> {
let mut buffer = Vec::new();
if self.0.is_empty() {
return buffer;
}
let mut prev_il_offset = 0u32;
let mut prev_start_line = 0u32;
let mut prev_start_col = 0u16;
for (i, point) in self.0.iter().enumerate() {
let is_first = i == 0;
let il_offset_value = if is_first {
point.il_offset
} else {
point.il_offset - prev_il_offset
};
write_compressed_uint(il_offset_value, &mut buffer);
if is_first {
write_compressed_uint(point.start_line, &mut buffer);
} else {
#[allow(clippy::cast_possible_wrap)]
let delta = point.start_line as i32 - prev_start_line as i32;
write_compressed_int(delta, &mut buffer);
}
if is_first {
write_compressed_uint(u32::from(point.start_col), &mut buffer);
} else {
let delta = i32::from(point.start_col) - i32::from(prev_start_col);
write_compressed_int(delta, &mut buffer);
}
let end_line_delta = point.end_line - point.start_line;
write_compressed_uint(end_line_delta, &mut buffer);
let end_col_delta = point.end_col - point.start_col;
write_compressed_uint(u32::from(end_col_delta), &mut buffer);
prev_il_offset = point.il_offset;
prev_start_line = point.start_line;
prev_start_col = point.start_col;
}
buffer
}
}
pub fn parse_sequence_points(blob: &[u8]) -> Result<SequencePoints> {
let mut parser = Parser::new(blob);
let mut points = Vec::new();
let mut il_offset = 0u32;
let mut start_line = 0u32;
let mut start_col = 0u16;
let mut first = true;
while parser.has_more_data() {
let il_offset_delta = parser.read_compressed_uint()?;
il_offset = if first {
il_offset_delta
} else {
il_offset + il_offset_delta
};
let start_line_delta = if first {
parser.read_compressed_uint()? } else {
#[allow(clippy::cast_sign_loss)]
{
parser.read_compressed_int()? as u32 }
};
start_line = if first {
start_line_delta
} else {
start_line.wrapping_add(start_line_delta)
};
let start_col_delta = if first {
#[allow(clippy::cast_possible_truncation)]
{
parser.read_compressed_uint()? as u16 }
} else {
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
{
parser.read_compressed_int()? as u16 }
};
start_col = if first {
start_col_delta
} else {
start_col.wrapping_add(start_col_delta)
};
let end_line_delta = parser.read_compressed_uint()?;
#[allow(clippy::cast_possible_truncation)]
let end_col_delta = parser.read_compressed_uint()? as u16;
let end_line = start_line + end_line_delta;
let end_col = start_col + end_col_delta;
let is_hidden = start_line == HIDDEN_SEQUENCE_POINT_LINE;
points.push(SequencePoint {
il_offset,
start_line,
start_col,
end_line,
end_col,
is_hidden,
});
first = false;
}
Ok(SequencePoints(points))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_empty_blob() {
let blob: &[u8] = &[];
let result = parse_sequence_points(blob);
assert!(result.is_ok());
assert!(result.unwrap().0.is_empty());
}
#[test]
fn parse_single_sequence_point() {
let blob: &[u8] = &[1, 10, 2, 0, 5];
let result = parse_sequence_points(blob).unwrap();
assert_eq!(result.0.len(), 1);
let sp = &result.0[0];
assert_eq!(sp.il_offset, 1);
assert_eq!(sp.start_line, 10);
assert_eq!(sp.start_col, 2);
assert_eq!(sp.end_line, 10);
assert_eq!(sp.end_col, 7);
assert!(!sp.is_hidden);
}
#[test]
fn parse_hidden_sequence_point() {
let blob: &[u8] = &[0, 0xC0, 0xFE, 0xEF, 0xEE, 0, 0, 0];
let result = parse_sequence_points(blob);
if let Ok(points) = result {
let sp = &points.0[0];
assert!(sp.is_hidden);
assert_eq!(sp.start_line, HIDDEN_SEQUENCE_POINT_LINE);
assert_eq!(sp.il_offset, 0);
assert_eq!(sp.start_col, 0);
assert_eq!(sp.end_line, HIDDEN_SEQUENCE_POINT_LINE);
assert_eq!(sp.end_col, 0);
} else {
panic!("Hidden sequence point parse failed: {result:?}");
}
}
#[test]
fn parse_multiple_sequence_points_with_deltas() {
let blob: &[u8] = &[1, 10, 2, 0, 5, 4, 2, 2, 0, 2];
let result = parse_sequence_points(blob).unwrap();
assert_eq!(result.0.len(), 2);
let sp0 = &result.0[0];
let sp1 = &result.0[1];
assert_eq!(sp0.il_offset, 1);
assert_eq!(sp0.start_line, 10);
assert_eq!(sp0.start_col, 2);
assert_eq!(sp0.end_line, 10);
assert_eq!(sp0.end_col, 7);
assert_eq!(sp1.il_offset, 5); assert_eq!(sp1.start_line, 11); assert_eq!(sp1.start_col, 3); assert_eq!(sp1.end_line, 11);
assert_eq!(sp1.end_col, 5);
}
#[test]
fn new_valid_single_line_sequence_point() {
let sp = SequencePoint::new(0, 10, 5, 10, 15);
assert!(sp.is_some());
let sp = sp.unwrap();
assert_eq!(sp.il_offset, 0);
assert_eq!(sp.start_line, 10);
assert_eq!(sp.start_col, 5);
assert_eq!(sp.end_line, 10);
assert_eq!(sp.end_col, 15);
assert!(!sp.is_hidden);
}
#[test]
fn new_valid_multi_line_sequence_point() {
let sp = SequencePoint::new(42, 10, 5, 15, 3);
assert!(sp.is_some());
let sp = sp.unwrap();
assert_eq!(sp.il_offset, 42);
assert_eq!(sp.start_line, 10);
assert_eq!(sp.start_col, 5);
assert_eq!(sp.end_line, 15);
assert_eq!(sp.end_col, 3);
assert!(!sp.is_hidden);
}
#[test]
fn new_invalid_end_line_before_start() {
let sp = SequencePoint::new(0, 10, 5, 5, 15);
assert!(sp.is_none(), "Should reject end_line < start_line");
}
#[test]
fn new_invalid_end_col_before_start_same_line() {
let sp = SequencePoint::new(0, 10, 15, 10, 5);
assert!(
sp.is_none(),
"Should reject end_col < start_col on same line"
);
}
#[test]
fn new_valid_end_col_equals_start_col() {
let sp = SequencePoint::new(0, 10, 5, 10, 5);
assert!(sp.is_some());
let sp = sp.unwrap();
assert_eq!(sp.start_col, sp.end_col);
}
#[test]
fn new_hidden_sequence_point() {
let sp = SequencePoint::new(
0,
HIDDEN_SEQUENCE_POINT_LINE,
0,
HIDDEN_SEQUENCE_POINT_LINE,
0,
);
assert!(sp.is_some());
let sp = sp.unwrap();
assert!(sp.is_hidden);
assert_eq!(sp.start_line, HIDDEN_SEQUENCE_POINT_LINE);
}
#[test]
fn hidden_constructor() {
let sp = SequencePoint::hidden(42);
assert!(sp.is_hidden);
assert_eq!(sp.il_offset, 42);
assert_eq!(sp.start_line, HIDDEN_SEQUENCE_POINT_LINE);
assert_eq!(sp.start_col, 0);
assert_eq!(sp.end_line, HIDDEN_SEQUENCE_POINT_LINE);
assert_eq!(sp.end_col, 0);
}
}