gsym-rs 0.1.5

Pure-Rust reader, writer, and Linux ELF/DWARF converter for LLVM GSYM
Documentation
use crate::endian::{Cursor, Encoder, Endian};
use crate::error::Error;
use crate::format::function::{self, EncodedCallSite, EncodedFunction, EncodedInlineNode};
use crate::format::{INFO_CALL_SITE, INFO_END, INFO_INLINE, INFO_LINE_TABLE, INFO_MERGED};
use crate::model::{AddressRange, LineEntry};

use super::ENDIANS;

const STRING_WIDTHS: [u8; 2] = [4, 8];

fn minimal_function(name: u64) -> EncodedFunction {
    EncodedFunction {
        range: AddressRange::new(0x1000, 0x1100),
        name,
        ..EncodedFunction::default()
    }
}

fn nested_inline(root_name: u64, child_name: u64) -> EncodedInlineNode {
    EncodedInlineNode {
        ranges: vec![AddressRange::new(0x1000, 0x1100)],
        name: root_name,
        call_file: 0,
        call_line: 0,
        children: vec![EncodedInlineNode {
            ranges: vec![AddressRange::new(0x1010, 0x1060)],
            name: child_name,
            call_file: 2,
            call_line: 22,
            children: vec![
                EncodedInlineNode {
                    ranges: vec![AddressRange::new(0x1012, 0x1015)],
                    name: child_name.saturating_add(1),
                    call_file: 3,
                    call_line: 33,
                    children: Vec::new(),
                },
                EncodedInlineNode {
                    ranges: vec![AddressRange::new(0x1057, 0x1058)],
                    name: child_name.saturating_add(2),
                    call_file: 4,
                    call_line: 44,
                    children: Vec::new(),
                },
            ],
        }],
    }
}

fn rich_function(width: u8) -> EncodedFunction {
    let large = if width == 8 { 0x1_0000_0001 } else { 1 };
    EncodedFunction {
        range: AddressRange::new(0x1000, 0x1200),
        name: large,
        lines: Some(vec![
            LineEntry {
                address: 0x1000,
                file: 1.into(),
                line: 10,
            },
            LineEntry {
                address: 0x1010,
                file: 1.into(),
                line: 11,
            },
            LineEntry {
                address: 0x1100,
                file: 2.into(),
                line: 1000,
            },
        ]),
        inline: Some(nested_inline(
            large.saturating_add(1),
            large.saturating_add(2),
        )),
        merged: vec![EncodedFunction {
            range: AddressRange::new(0x1000, 0x1200),
            name: large.saturating_add(10),
            ..EncodedFunction::default()
        }],
        call_sites: vec![EncodedCallSite {
            return_offset: 0x30,
            flags: 1,
            match_regex: vec![large.saturating_add(20), large.saturating_add(21)],
        }],
    }
}

#[test]
fn function_info_round_trips_every_optional_record() {
    for endian in ENDIANS {
        for width in STRING_WIDTHS {
            let expected = rich_function(width);
            let bytes = function::encode(&expected, endian, width).unwrap();
            let decoded =
                function::decode_exact(&bytes, endian, width, expected.range.start).unwrap();
            assert_eq!(decoded, expected);
        }
    }
}

#[test]
fn function_and_all_nested_records_preserve_large_v2_offsets() {
    let expected = rich_function(8);
    let bytes = function::encode(&expected, Endian::Little, 8).unwrap();
    let decoded = function::decode_exact(&bytes, Endian::Little, 8, 0x1000).unwrap();
    assert!(decoded.name > u64::from(u32::MAX));
    assert!(decoded.inline.unwrap().name > u64::from(u32::MAX));
    assert!(decoded.merged.first().unwrap().name > u64::from(u32::MAX));
    assert!(
        decoded
            .call_sites
            .first()
            .unwrap()
            .match_regex
            .iter()
            .all(|offset| *offset > u64::from(u32::MAX))
    );
}

#[test]
fn function_info_matches_reference_minimal_bytes() {
    for endian in ENDIANS {
        for width in STRING_WIDTHS {
            let bytes = function::encode(&minimal_function(1), endian, width).unwrap();
            assert_eq!(bytes.len(), 4 + usize::from(width) + 8);
            let mut cursor = Cursor::new(&bytes, endian);
            assert_eq!(cursor.read_u32().unwrap(), 0x100);
            assert_eq!(cursor.read_uint(width).unwrap(), 1);
            assert_eq!(cursor.read_u32().unwrap(), INFO_END);
            assert_eq!(cursor.read_u32().unwrap(), 0);
        }
    }
}

#[test]
fn function_info_reports_reference_decode_and_encode_errors() {
    for endian in ENDIANS {
        for width in STRING_WIDTHS {
            let mut output = Encoder::new(endian);
            assert!(function::decode(output.as_slice(), endian, width, 0x1000).is_err());
            output.write_u32(0x100);
            assert!(function::decode(output.as_slice(), endian, width, 0x1000).is_err());
            output.write_uint(0, width).unwrap();
            assert!(matches!(
                function::decode(output.as_slice(), endian, width, 0x1000),
                Err(Error::ZeroNameOffset)
            ));
            output.patch_uint(4, 1, width).unwrap();
            assert!(function::decode(output.as_slice(), endian, width, 0x1000).is_err());
            output.write_u32(7);
            output.write_u32(0);
            assert!(matches!(
                function::decode(output.as_slice(), endian, width, 0x1000),
                Err(Error::UnsupportedInfoType(7))
            ));

            let mut invalid = minimal_function(0);
            assert!(matches!(
                function::encode(&invalid, endian, width),
                Err(Error::ZeroNameOffset)
            ));
            invalid.name = 1;
            invalid.range.end = invalid.range.start + u64::from(u32::MAX) + 1;
            assert!(function::encode(&invalid, endian, width).is_err());
            assert!(function::encode(&minimal_function(1), endian, 3).is_err());
        }
    }
}

#[test]
fn function_info_rejects_truncated_payloads_unknown_types_and_bad_end_markers() {
    for endian in ENDIANS {
        for width in STRING_WIDTHS {
            let bytes = function::encode(&rich_function(width), endian, width).unwrap();
            for length in 0..bytes.len() {
                assert!(
                    function::decode_exact(bytes.split_at(length).0, endian, width, 0x1000)
                        .is_err()
                );
            }

            let mut trailing = function::encode(&minimal_function(1), endian, width).unwrap();
            trailing.push(0);
            assert!(function::decode_exact(&trailing, endian, width, 0x1000).is_err());

            let mut bad_end = Encoder::new(endian);
            bad_end.write_u32(0x100);
            bad_end.write_uint(1, width).unwrap();
            bad_end.write_u32(INFO_END);
            bad_end.write_u32(1);
            bad_end.write_u8(0);
            assert!(function::decode(bad_end.as_slice(), endian, width, 0x1000).is_err());

            let mut truncated_record = Encoder::new(endian);
            truncated_record.write_u32(0x100);
            truncated_record.write_uint(1, width).unwrap();
            truncated_record.write_u32(INFO_LINE_TABLE);
            truncated_record.write_u32(10);
            truncated_record.write_u8(0);
            assert!(function::decode(truncated_record.as_slice(), endian, width, 0x1000).is_err());
        }
    }
}

#[test]
fn function_info_rejects_duplicate_empty_collection_records() {
    for endian in ENDIANS {
        for info_type in [INFO_MERGED, INFO_CALL_SITE] {
            let mut bytes = Encoder::new(endian);
            bytes.write_u32(0x100);
            bytes.write_u32(1);
            for _ in 0..2 {
                bytes.write_u32(info_type);
                bytes.write_u32(4);
                bytes.write_u32(0);
            }
            bytes.write_u32(INFO_END);
            bytes.write_u32(0);
            assert!(matches!(
                function::decode_exact(bytes.as_slice(), endian, 4, 0x1000),
                Err(Error::InvalidFormat(_))
            ));
        }
    }
}

#[test]
fn inline_ranges_and_sibling_terminators_match_llvm() {
    for endian in ENDIANS {
        for width in STRING_WIDTHS {
            let mut function_value = minimal_function(1);
            function_value.inline =
                Some(nested_inline(0, if width == 8 { 0x1_0000_0001 } else { 2 }));
            let bytes = function::encode(&function_value, endian, width).unwrap();
            let decoded = function::decode_exact(&bytes, endian, width, 0x1000).unwrap();
            assert_eq!(decoded, function_value);
        }
    }
}

#[test]
fn inline_address_ranges_handle_zero_one_and_many() {
    for endian in ENDIANS {
        for width in STRING_WIDTHS {
            for ranges in [
                vec![AddressRange::new(0x1000, 0x1010)],
                vec![
                    AddressRange::new(0x1000, 0x1010),
                    AddressRange::new(0x1020, 0x1030),
                    AddressRange::new(0x1050, 0x1070),
                ],
            ] {
                let mut expected = minimal_function(1);
                expected.inline = Some(EncodedInlineNode {
                    ranges,
                    ..EncodedInlineNode::default()
                });
                let bytes = function::encode(&expected, endian, width).unwrap();
                assert_eq!(
                    function::decode_exact(&bytes, endian, width, 0x1000).unwrap(),
                    expected
                );
            }

            let mut empty = minimal_function(1);
            empty.inline = Some(EncodedInlineNode::default());
            assert!(function::encode(&empty, endian, width).is_err());
        }
    }
}

#[test]
fn inline_encoding_rejects_empty_uncontained_unsorted_and_truncated_trees() {
    for endian in ENDIANS {
        for width in STRING_WIDTHS {
            let mut function_value = minimal_function(1);
            function_value.inline = Some(EncodedInlineNode::default());
            assert!(function::encode(&function_value, endian, width).is_err());

            function_value.inline = Some(EncodedInlineNode {
                ranges: vec![AddressRange::new(0x1000, 0x1100)],
                name: 0,
                call_file: 0,
                call_line: 0,
                children: vec![EncodedInlineNode {
                    ranges: vec![AddressRange::new(0x1100, 0x1200)],
                    name: 2,
                    call_file: 1,
                    call_line: 1,
                    children: Vec::new(),
                }],
            });
            assert!(function::encode(&function_value, endian, width).is_err());

            function_value.inline = Some(EncodedInlineNode {
                ranges: vec![
                    AddressRange::new(0x1080, 0x1090),
                    AddressRange::new(0x1070, 0x1078),
                ],
                name: 0,
                call_file: 0,
                call_line: 0,
                children: Vec::new(),
            });
            assert!(function::encode(&function_value, endian, width).is_err());

            let valid = rich_function(width);
            let bytes = function::encode(&valid, endian, width).unwrap();
            for length in 0..bytes.len() {
                assert!(
                    function::decode_exact(bytes.split_at(length).0, endian, width, 0x1000)
                        .is_err()
                );
            }
        }
    }
}

#[test]
fn merged_and_callsite_collections_round_trip_and_reject_truncation() {
    for endian in ENDIANS {
        for width in STRING_WIDTHS {
            let expected = rich_function(width);
            let bytes = function::encode(&expected, endian, width).unwrap();
            let actual = function::decode_exact(&bytes, endian, width, 0x1000).unwrap();
            assert_eq!(actual.merged, expected.merged);
            assert_eq!(actual.call_sites, expected.call_sites);
            let call_site = actual.call_sites.first().unwrap();
            assert_eq!(call_site.return_offset, 0x30);
            assert_eq!(call_site.flags, 1);
            assert_eq!(call_site.match_regex.len(), 2);
        }
    }
}

#[test]
fn record_type_headers_are_endian_aware() {
    for endian in ENDIANS {
        let mut value = minimal_function(1);
        value.inline = Some(EncodedInlineNode {
            ranges: vec![value.range],
            ..EncodedInlineNode::default()
        });
        let bytes = function::encode(&value, endian, 4).unwrap();
        let mut cursor = Cursor::new(&bytes, endian);
        cursor.take(8).unwrap();
        assert_eq!(cursor.read_u32().unwrap(), INFO_INLINE);
        let payload_size = cursor.read_u32().unwrap();
        assert!(payload_size > 0);
    }
}