use std::io::Read;
use crate::{
file::parser::Parser,
metadata::customdebuginformation::types::{CustomDebugInfo, CustomDebugKind},
Result,
};
pub struct CustomDebugParser<'a> {
parser: Parser<'a>,
kind: CustomDebugKind,
}
impl<'a> CustomDebugParser<'a> {
#[must_use]
pub fn new(data: &'a [u8], kind: CustomDebugKind) -> Self {
CustomDebugParser {
parser: Parser::new(data),
kind,
}
}
pub fn parse_debug_info(&mut self) -> Result<CustomDebugInfo> {
match self.kind {
CustomDebugKind::SourceLink => {
let document = self.read_utf8_string()?;
Ok(CustomDebugInfo::SourceLink { document })
}
CustomDebugKind::EmbeddedSource => {
self.parse_embedded_source()
}
CustomDebugKind::CompilationMetadata => {
let metadata = self.read_utf8_string()?;
Ok(CustomDebugInfo::CompilationMetadata { metadata })
}
CustomDebugKind::CompilationOptions => {
let options = self.read_utf8_string()?;
Ok(CustomDebugInfo::CompilationOptions { options })
}
CustomDebugKind::Unknown(_) => {
let remaining_data = self.read_remaining_bytes();
Ok(CustomDebugInfo::Unknown {
kind: self.kind,
data: remaining_data,
})
}
}
}
fn parse_embedded_source(&mut self) -> Result<CustomDebugInfo> {
if self.parser.len() < 4 {
return Err(malformed_error!(
"EmbeddedSource blob too small: {} bytes (minimum 4 required)",
self.parser.len()
));
}
let format = self.parser.read_le::<i32>()?;
let remaining = self.read_remaining_bytes();
match format.cmp(&0) {
std::cmp::Ordering::Equal => {
let content = String::from_utf8(remaining).map_err(|e| {
malformed_error!("EmbeddedSource contains invalid UTF-8: {}", e.utf8_error())
})?;
Ok(CustomDebugInfo::EmbeddedSource {
filename: String::new(), content,
was_compressed: false,
})
}
std::cmp::Ordering::Greater => {
#[allow(clippy::cast_sign_loss)] let decompressed_size = format as usize;
let content = Self::decompress_deflate(&remaining, decompressed_size)?;
Ok(CustomDebugInfo::EmbeddedSource {
filename: String::new(), content,
was_compressed: true,
})
}
std::cmp::Ordering::Less => Err(malformed_error!(
"EmbeddedSource has invalid format indicator: {} (expected >= 0)",
format
)),
}
}
fn decompress_deflate(compressed: &[u8], expected_size: usize) -> Result<String> {
let mut decoder = flate2::read::DeflateDecoder::new(compressed);
let mut decompressed = Vec::with_capacity(expected_size);
decoder
.read_to_end(&mut decompressed)
.map_err(|e| malformed_error!("Failed to decompress EmbeddedSource: {}", e))?;
if decompressed.len() != expected_size {
return Err(malformed_error!(
"EmbeddedSource decompressed size mismatch: expected {}, got {}",
expected_size,
decompressed.len()
));
}
String::from_utf8(decompressed).map_err(|e| {
malformed_error!(
"EmbeddedSource decompressed content is not valid UTF-8: {}",
e.utf8_error()
)
})
}
fn read_utf8_string(&mut self) -> Result<String> {
let remaining = self.read_remaining_bytes();
if remaining.is_empty() {
return Ok(String::new());
}
String::from_utf8(remaining).map_err(|e| {
malformed_error!(
"Custom debug information contains invalid UTF-8 at byte {}: {}",
e.utf8_error().valid_up_to(),
e.utf8_error()
)
})
}
fn read_remaining_bytes(&mut self) -> Vec<u8> {
let pos = self.parser.pos();
let len = self.parser.len();
if pos >= len {
return Vec::new();
}
self.parser.data()[pos..len].to_vec()
}
}
pub fn parse_custom_debug_blob(data: &[u8], kind: CustomDebugKind) -> Result<CustomDebugInfo> {
if data.is_empty() {
return Ok(CustomDebugInfo::Unknown {
kind,
data: Vec::new(),
});
}
let mut parser = CustomDebugParser::new(data, kind);
parser.parse_debug_info()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_empty_blob() {
let kind = CustomDebugKind::SourceLink;
let result = parse_custom_debug_blob(&[], kind).unwrap();
assert!(matches!(result, CustomDebugInfo::Unknown { .. }));
}
#[test]
fn test_custom_debug_parser_new() {
let kind = CustomDebugKind::SourceLink;
let data = b"test data";
let parser = CustomDebugParser::new(data, kind);
assert_eq!(parser.parser.len(), 9);
}
#[test]
fn test_parse_source_link() {
let kind = CustomDebugKind::SourceLink;
let data = b"{\"documents\":{}}";
let result = parse_custom_debug_blob(data, kind).unwrap();
match result {
CustomDebugInfo::SourceLink { document } => {
assert_eq!(document, "{\"documents\":{}}");
}
_ => panic!("Expected SourceLink variant"),
}
}
#[test]
fn test_parse_unknown_kind() {
let unknown_guid = [
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D,
0x0E, 0x0F,
];
let kind = CustomDebugKind::Unknown(unknown_guid);
let data = b"raw data";
let result = parse_custom_debug_blob(data, kind).unwrap();
match result {
CustomDebugInfo::Unknown {
kind: parsed_kind,
data: parsed_data,
} => {
assert_eq!(parsed_kind, kind);
assert_eq!(parsed_data, b"raw data");
}
_ => panic!("Expected Unknown variant"),
}
}
#[test]
fn test_parse_embedded_source_uncompressed() {
let kind = CustomDebugKind::EmbeddedSource;
let mut data = Vec::new();
data.extend_from_slice(&0i32.to_le_bytes()); data.extend_from_slice(b"// Hello, world!\nclass Test {}");
let result = parse_custom_debug_blob(&data, kind).unwrap();
match result {
CustomDebugInfo::EmbeddedSource {
filename,
content,
was_compressed,
} => {
assert!(
filename.is_empty(),
"Filename should be empty (set by caller)"
);
assert_eq!(content, "// Hello, world!\nclass Test {}");
assert!(!was_compressed);
}
_ => panic!("Expected EmbeddedSource variant"),
}
}
#[test]
fn test_parse_embedded_source_too_small() {
let kind = CustomDebugKind::EmbeddedSource;
let data = [0x00, 0x00, 0x00];
let result = parse_custom_debug_blob(&data, kind);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("too small"));
}
#[test]
fn test_parse_embedded_source_negative_format() {
let kind = CustomDebugKind::EmbeddedSource;
let mut data = Vec::new();
data.extend_from_slice(&(-1i32).to_le_bytes());
data.extend_from_slice(b"content");
let result = parse_custom_debug_blob(&data, kind);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("invalid format indicator"));
}
#[test]
fn test_parse_invalid_utf8_returns_error() {
let kind = CustomDebugKind::SourceLink;
let data = [0xFF, 0xFE, 0x00, 0x01];
let result = parse_custom_debug_blob(&data, kind);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("invalid UTF-8"));
}
#[test]
fn test_parse_compilation_metadata() {
let kind = CustomDebugKind::CompilationMetadata;
let data = b"compiler: csc 4.0";
let result = parse_custom_debug_blob(data, kind).unwrap();
match result {
CustomDebugInfo::CompilationMetadata { metadata } => {
assert_eq!(metadata, "compiler: csc 4.0");
}
_ => panic!("Expected CompilationMetadata variant"),
}
}
#[test]
fn test_parse_compilation_options() {
let kind = CustomDebugKind::CompilationOptions;
let data = b"/optimize+ /debug:full";
let result = parse_custom_debug_blob(data, kind).unwrap();
match result {
CustomDebugInfo::CompilationOptions { options } => {
assert_eq!(options, "/optimize+ /debug:full");
}
_ => panic!("Expected CompilationOptions variant"),
}
}
}