diff --git a/resolve-facts/rs/Cargo.lock b/resolve-facts/rs/Cargo.lock index f95a5feb..fb266494 100644 --- a/resolve-facts/rs/Cargo.lock +++ b/resolve-facts/rs/Cargo.lock @@ -22,13 +22,130 @@ dependencies = [ "syn", ] +[[package]] +name = "cc" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + [[package]] name = "facts-rs" version = "0.1.0" dependencies = [ "bytemuck", + "object", + "zstd", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom", + "libc", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "object" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e5a6c098c7a3b6547378093f5cc30bc54fd361ce711e05293a5cc589562739b" +dependencies = [ + "crc32fast", + "hashbrown", + "indexmap", + "memchr", +] + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + [[package]] name = "proc-macro2" version = "1.0.107" @@ -47,6 +164,18 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + [[package]] name = "syn" version = "3.0.3" @@ -63,3 +192,31 @@ name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/resolve-facts/rs/Cargo.toml b/resolve-facts/rs/Cargo.toml index 725335a9..0cfdb0b7 100644 --- a/resolve-facts/rs/Cargo.toml +++ b/resolve-facts/rs/Cargo.toml @@ -8,3 +8,8 @@ crate-type = ["rlib", "staticlib", "cdylib"] [dependencies] bytemuck = { version = "1", features = ["derive"] } +object = { version = "0.39.1", default-features = false, features = ["read_core", "elf", "std"] } +zstd = "0.13.3" + +[dev-dependencies] +object = { version = "0.39.1", default-features = false, features = ["write"] } diff --git a/resolve-facts/rs/cbindgen.toml b/resolve-facts/rs/cbindgen.toml index babadc1a..e027f9d1 100644 --- a/resolve-facts/rs/cbindgen.toml +++ b/resolve-facts/rs/cbindgen.toml @@ -18,4 +18,8 @@ crates = ["facts-rs"] include = [ "Node", "Edge", + "FactsPath", + "FactsReadResult", + "FactsModuleCursor", + "FactsModuleView", ] diff --git a/resolve-facts/rs/src/ffi.rs b/resolve-facts/rs/src/ffi.rs new file mode 100644 index 00000000..cbeebe69 --- /dev/null +++ b/resolve-facts/rs/src/ffi.rs @@ -0,0 +1,149 @@ +use std::path::*; + +use crate::schema::*; +use crate::utils::*; +use crate::writer::*; + +#[repr(C)] +#[derive(Clone, Copy, Debug, Default)] +pub struct FactsPath { + pub data: *const u8, + pub len: usize, +} + +pub struct FactsReadError(String); + +#[repr(C)] +#[derive(Debug)] +pub struct FactsReadResult { + pub facts: *mut FactsBuf, + pub error: *mut FactsReadError, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Default)] +pub struct FactsModuleCursor { + pub byte_offset: usize, + pub module_index: u32, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug)] +pub struct FactsModuleView { + pub module_index: u32, + pub nodes: *const Node, + pub node_count: usize, + pub edges: *const Edge, + pub edge_count: usize, + pub string_pool: *const u8, + pub string_pool_len: usize, +} + +impl FactsReadResult { + fn success(facts: FactsBuf) -> Self { + Self { + facts: Box::into_raw(Box::new(facts)), + error: std::ptr::null_mut(), + } + } + + fn error(message: impl Into) -> Self { + Self { + facts: std::ptr::null_mut(), + error: Box::into_raw(Box::new(FactsReadError(message.into()))), + } + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn facts_read_files(paths: *const FactsPath, len: usize) -> FactsReadResult { + let paths = if len == 0 { + &[] + } else if paths.is_null() { + return FactsReadResult::error("null facts path array"); + } else { + unsafe { std::slice::from_raw_parts(paths, len) } + }; + + let mut owned = Vec::with_capacity(paths.len()); + for path in paths { + let Some(path) = (unsafe { as_str(path.data, path.len) }) else { + return FactsReadResult::error("facts paths must be valid UTF-8"); + }; + owned.push(PathBuf::from(path)); + } + + match FactsBuf::read_files(&owned) { + Ok(facts) => FactsReadResult::success(facts), + Err(error) => FactsReadResult::error(error.to_string()), + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn facts_read_error_len(error: *const FactsReadError) -> usize { + unsafe { error.as_ref() }.map_or(0, |error| error.0.len()) +} + +#[unsafe(no_mangle)] +pub extern "C" fn facts_read_error_data(error: *const FactsReadError) -> *const u8 { + unsafe { error.as_ref() }.map_or(std::ptr::null(), |error| error.0.as_ptr()) +} + +#[unsafe(no_mangle)] +pub extern "C" fn facts_read_error_free(error: *mut FactsReadError) { + if !error.is_null() { + unsafe { + drop(Box::from_raw(error)); + } + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn facts_module_next( + facts: *const FactsBuf, + cursor: *mut FactsModuleCursor, + output: *mut FactsModuleView, +) -> bool { + let Some(facts) = (unsafe { facts.as_ref() }) else { + return false; + }; + let Some(cursor) = (unsafe { cursor.as_mut() }) else { + return false; + }; + let Some(output) = (unsafe { output.as_mut() }) else { + return false; + }; + + let bytes = facts.as_bytes(); + if cursor.byte_offset == bytes.len() { + return false; + } + let Some(remaining) = bytes.get(cursor.byte_offset..) else { + return false; + }; + let Ok((module, _)) = ModuleRef::from_prefix(remaining) else { + return false; + }; + let Some(next_offset) = cursor.byte_offset.checked_add(module.as_bytes().len()) else { + return false; + }; + let Some(next_index) = cursor.module_index.checked_add(1) else { + return false; + }; + + let nodes = module.nodes(); + let edges = module.edges(); + let string_pool = module.string_pool(); + *output = FactsModuleView { + module_index: cursor.module_index, + nodes: nodes.as_ptr(), + node_count: nodes.len(), + edges: edges.as_ptr(), + edge_count: edges.len(), + string_pool: string_pool.as_ptr(), + string_pool_len: string_pool.len(), + }; + cursor.byte_offset = next_offset; + cursor.module_index = next_index; + true +} diff --git a/resolve-facts/rs/src/lib.rs b/resolve-facts/rs/src/lib.rs index 66c73479..6488d863 100644 --- a/resolve-facts/rs/src/lib.rs +++ b/resolve-facts/rs/src/lib.rs @@ -2,11 +2,14 @@ compile_error!("the facts format currently requires a little-endian target"); mod builder; +mod ffi; mod interner; +mod reader; mod schema; mod utils; mod writer; pub use builder::{FactsBuilder, ModuleHandle}; +pub use reader::*; pub use schema::*; pub use writer::FactsBuf; diff --git a/resolve-facts/rs/src/reader.rs b/resolve-facts/rs/src/reader.rs new file mode 100644 index 00000000..94aba14f --- /dev/null +++ b/resolve-facts/rs/src/reader.rs @@ -0,0 +1,576 @@ +use std::fs::read; +use std::io::Read; +use std::mem::*; +use std::path::{Path, PathBuf}; + +use object::{Object, ObjectSection}; + +use crate::schema::*; +use crate::writer::*; + +const FACTS_SECTION: &str = ".facts"; +const ZSTD_MAGIC: u32 = 0xfd2f_b528; +const ZSTD_SKIPPABLE_MAGIC: u32 = 0x184d_2a50; +const KNOWN_PROPERTIES: u32 = P_IDX + | P_NAME + | P_OPCODE + | P_LINKAGE + | P_CALL_TYPE + | P_SOURCE_LOC + | P_SOURCE_FILE + | P_FUNCTION_TYPE + | P_ADDRESS_TAKEN; +const KNOWN_EDGE_KINDS: u32 = (1 << EdgeKind::Calls as u8) + | (1 << EdgeKind::Contains as u8) + | (1 << EdgeKind::DataFlowTo as u8) + | (1 << EdgeKind::References as u8) + | (1 << EdgeKind::EntryPoint as u8) + | (1 << EdgeKind::ControlFlowTo as u8); + +#[derive(Debug)] +pub enum ReadError { + Io { + path: PathBuf, + source: std::io::Error, + }, + Object { + path: PathBuf, + source: object::Error, + }, + MissingFactsSection { + path: PathBuf, + }, + Decompression { + path: PathBuf, + source: std::io::Error, + }, + UnalignedPayload { + path: PathBuf, + byte_len: usize, + }, + InvalidFacts { + path: PathBuf, + source: InvalidFacts, + }, +} + +impl std::fmt::Display for ReadError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Io { path, source } => write!(f, "could not read {}: {source}", path.display()), + Self::Object { path, source } => { + write!(f, "could not read ELF {}: {source}", path.display()) + } + Self::MissingFactsSection { path } => { + write!(f, "ELF {} has no {FACTS_SECTION} section", path.display()) + } + Self::Decompression { path, source } => { + write!( + f, + "could not decompress facts from {}: {source}", + path.display() + ) + } + Self::UnalignedPayload { path, byte_len } => write!( + f, + "facts from {} contain {byte_len} bytes, which is not divisible by 4", + path.display() + ), + Self::InvalidFacts { path, source } => { + write!(f, "invalid facts in {}: {source}", path.display()) + } + } + } +} + +impl std::error::Error for ReadError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::Io { source, .. } => Some(source), + Self::Object { source, .. } => Some(source), + Self::Decompression { source, .. } => Some(source), + Self::InvalidFacts { source, .. } => Some(source), + Self::MissingFactsSection { .. } | Self::UnalignedPayload { .. } => None, + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum InvalidFacts { + View { + module: usize, + byte_offset: usize, + source: ViewError, + }, + UnknownProperties { + module: usize, + node: NodeID, + bits: u32, + }, + InvalidNodeType { + module: usize, + node: NodeID, + value: u8, + }, + MissingModuleNode { + module: usize, + }, + InvalidModuleNode { + module: usize, + value: u8, + }, + InvalidLinkage { + module: usize, + node: NodeID, + value: u8, + }, + InvalidCallType { + module: usize, + node: NodeID, + value: u8, + }, + InvalidStringOffset { + module: usize, + node: NodeID, + property: &'static str, + offset: u32, + }, + InvalidUtf8 { + module: usize, + node: NodeID, + property: &'static str, + offset: u32, + }, + InvalidEdgeEndpoint { + module: usize, + edge: usize, + endpoint: NodeID, + node_count: u32, + }, + UnknownEdgeKinds { + module: usize, + edge: usize, + bits: u32, + }, + UnsortedEdges { + module: usize, + edge: usize, + }, +} + +impl std::fmt::Display for InvalidFacts { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::View { + module, + byte_offset, + source, + } => write!( + f, + "module {module} at byte {byte_offset} cannot be viewed: {source}" + ), + Self::UnknownProperties { module, node, bits } => write!( + f, + "module {module}, node {node} has unknown property bits {bits:#x}" + ), + Self::InvalidNodeType { + module, + node, + value, + } => write!( + f, + "module {module}, node {node} has invalid node type {value}" + ), + Self::MissingModuleNode { module } => { + write!(f, "module {module} has no module node at index 0") + } + Self::InvalidModuleNode { module, value } => write!( + f, + "module {module}, node 0 has node type {value}, not Module" + ), + Self::InvalidLinkage { + module, + node, + value, + } => write!( + f, + "module {module}, node {node} has invalid linkage {value}" + ), + Self::InvalidCallType { + module, + node, + value, + } => write!( + f, + "module {module}, node {node} has invalid call type {value}" + ), + Self::InvalidStringOffset { + module, + node, + property, + offset, + } => write!( + f, + "module {module}, node {node} has an invalid {property} string offset {offset}" + ), + Self::InvalidUtf8 { + module, + node, + property, + offset, + } => write!( + f, + "module {module}, node {node} has non-UTF-8 {property} at string offset {offset}" + ), + Self::InvalidEdgeEndpoint { + module, + edge, + endpoint, + node_count, + } => write!( + f, + "module {module}, edge {edge} refers to node {endpoint}, but the module has {node_count} nodes" + ), + Self::UnknownEdgeKinds { module, edge, bits } => write!( + f, + "module {module}, edge {edge} has unknown kind bits {bits:#x}" + ), + Self::UnsortedEdges { module, edge } => write!( + f, + "module {module}, edge {edge} is not strictly ordered after its predecessor" + ), + } + } +} + +impl std::error::Error for InvalidFacts { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::View { source, .. } => Some(source), + _ => None, + } + } +} + +impl FactsBuf { + pub fn read_file(path: impl AsRef) -> Result { + Self::read_files([path]) + } + + pub fn read_files(paths: I) -> Result + where + I: IntoIterator, + P: AsRef, + { + let mut words = Vec::new(); + for path in paths { + read_input(path.as_ref(), &mut words)?; + } + Ok(Self::from_words(words)) + } +} + +fn read_input(path: &Path, words: &mut Vec) -> Result<(), ReadError> { + let input = read(path).map_err(|source| ReadError::Io { + path: path.to_owned(), + source, + })?; + + let payload = if input.starts_with(b"\x7fELF") { + let object = object::File::parse(input.as_slice()).map_err(|source| ReadError::Object { + path: path.to_owned(), + source, + })?; + let section = object.section_by_name(FACTS_SECTION).ok_or_else(|| { + ReadError::MissingFactsSection { + path: path.to_owned(), + } + })?; + section.data().map_err(|source| ReadError::Object { + path: path.to_owned(), + source, + })? + } else { + input.as_slice() + }; + + let start = words.len(); + if is_zstd(payload) { + let decoder = zstd::stream::read::Decoder::new(payload).map_err(|source| { + ReadError::Decompression { + path: path.to_owned(), + source, + } + })?; + append_reader(decoder, words).map_err(|error| match error { + AppendError::Read(source) => ReadError::Decompression { + path: path.to_owned(), + source, + }, + AppendError::Unaligned(byte_len) => ReadError::UnalignedPayload { + path: path.to_owned(), + byte_len, + }, + })?; + } else { + append_bytes(payload, words).map_err(|byte_len| ReadError::UnalignedPayload { + path: path.to_owned(), + byte_len, + })?; + } + + let bytes = words_as_bytes(&words[start..]); + validate(bytes).map_err(|source| ReadError::InvalidFacts { + path: path.to_owned(), + source, + }) +} + +fn is_zstd(bytes: &[u8]) -> bool { + let Some(magic) = bytes + .get(..size_of::()) + .and_then(|bytes| bytes.try_into().ok()) + .map(u32::from_le_bytes) + else { + return false; + }; + + magic == ZSTD_MAGIC || magic & 0xffff_fff0 == ZSTD_SKIPPABLE_MAGIC +} + +fn append_bytes(bytes: &[u8], words: &mut Vec) -> Result<(), usize> { + if !bytes.len().is_multiple_of(size_of::()) { + return Err(bytes.len()); + } + + words.reserve(bytes.len() / size_of::()); + for chunk in bytes.chunks_exact(size_of::()) { + words.push(u32::from_le_bytes(chunk.try_into().unwrap())); + } + Ok(()) +} + +enum AppendError { + Read(std::io::Error), + Unaligned(usize), +} + +fn append_reader(mut reader: impl Read, words: &mut Vec) -> Result<(), AppendError> { + let mut buffer = [0; 64 * 1024]; + let mut pending = [0; size_of::()]; + let mut pending_len = 0; + let mut byte_len = 0; + + loop { + let read = reader.read(&mut buffer).map_err(AppendError::Read)?; + if read == 0 { + break; + } + byte_len += read; + + let mut bytes = &buffer[..read]; + if pending_len != 0 { + let needed = size_of::() - pending_len; + let copied = needed.min(bytes.len()); + pending[pending_len..pending_len + copied].copy_from_slice(&bytes[..copied]); + pending_len += copied; + bytes = &bytes[copied..]; + if pending_len != size_of::() { + continue; + } + words.push(u32::from_le_bytes(pending)); + } + + let mut chunks = bytes.chunks_exact(size_of::()); + words.extend( + chunks + .by_ref() + .map(|chunk| u32::from_le_bytes(chunk.try_into().unwrap())), + ); + let remainder = chunks.remainder(); + pending[..remainder.len()].copy_from_slice(remainder); + pending_len = remainder.len(); + } + + if pending_len == 0 { + Ok(()) + } else { + Err(AppendError::Unaligned(byte_len)) + } +} + +fn words_as_bytes(words: &[u32]) -> &[u8] { + bytemuck::cast_slice(words) +} + +fn validate(bytes: &[u8]) -> Result<(), InvalidFacts> { + let mut remaining = bytes; + let mut byte_offset = 0; + let mut module_index = 0; + + while !remaining.is_empty() { + let (module, rest) = + ModuleRef::from_prefix(remaining).map_err(|source| InvalidFacts::View { + module: module_index, + byte_offset, + source, + })?; + validate_module(module_index, module)?; + byte_offset += module.as_bytes().len(); + module_index += 1; + remaining = rest; + } + + Ok(()) +} + +fn validate_module(module_index: usize, module: ModuleRef<'_>) -> Result<(), InvalidFacts> { + let Some(module_node) = module.nodes().first() else { + return Err(InvalidFacts::MissingModuleNode { + module: module_index, + }); + }; + if module_node.node_type_raw() != NodeType::Module as u8 { + return Err(InvalidFacts::InvalidModuleNode { + module: module_index, + value: module_node.node_type_raw(), + }); + } + + for (node_index, node) in module.nodes().iter().enumerate() { + let node_index = node_index as NodeID; + let unknown = node.meta & PRESENT_MASK & !KNOWN_PROPERTIES; + if unknown != 0 { + return Err(InvalidFacts::UnknownProperties { + module: module_index, + node: node_index, + bits: unknown, + }); + } + if node.node_type_raw() > NodeType::GlobalVariable as u8 { + return Err(InvalidFacts::InvalidNodeType { + module: module_index, + node: node_index, + value: node.node_type_raw(), + }); + } + if let Some(value) = node.linkage_raw() + && value > Linkage::ExternalLinkage as u8 + { + return Err(InvalidFacts::InvalidLinkage { + module: module_index, + node: node_index, + value, + }); + } + if let Some(value) = node.call_type_raw() + && value > CallType::Indirect as u8 + { + return Err(InvalidFacts::InvalidCallType { + module: module_index, + node: node_index, + value, + }); + } + + validate_node_string( + module_index, + node_index, + node, + module, + P_NAME, + "name", + node.name, + )?; + validate_node_string( + module_index, + node_index, + node, + module, + P_OPCODE, + "opcode", + node.opcode, + )?; + validate_node_string( + module_index, + node_index, + node, + module, + P_SOURCE_FILE, + "source file", + node.source_file, + )?; + validate_node_string( + module_index, + node_index, + node, + module, + P_FUNCTION_TYPE, + "function type", + node.function_type, + )?; + } + + let node_count = module.header().node_count; + let mut previous = None; + for (edge_index, edge) in module.edges().iter().enumerate() { + for endpoint in [edge.src, edge.dst] { + if endpoint >= node_count { + return Err(InvalidFacts::InvalidEdgeEndpoint { + module: module_index, + edge: edge_index, + endpoint, + node_count, + }); + } + } + let unknown = edge.kinds & !KNOWN_EDGE_KINDS; + if unknown != 0 { + return Err(InvalidFacts::UnknownEdgeKinds { + module: module_index, + edge: edge_index, + bits: unknown, + }); + } + let current = (edge.src, edge.dst); + if previous.is_some_and(|previous| previous >= current) { + return Err(InvalidFacts::UnsortedEdges { + module: module_index, + edge: edge_index, + }); + } + previous = Some(current); + } + + Ok(()) +} + +fn validate_node_string( + module_index: usize, + node_index: NodeID, + node: &Node, + module: ModuleRef<'_>, + present: u32, + property: &'static str, + id: Interned, +) -> Result<(), InvalidFacts> { + if !node.has(present) { + return Ok(()); + } + + let bytes = module + .string_bytes(id) + .ok_or(InvalidFacts::InvalidStringOffset { + module: module_index, + node: node_index, + property, + offset: id.0, + })?; + std::str::from_utf8(bytes).map_err(|_| InvalidFacts::InvalidUtf8 { + module: module_index, + node: node_index, + property, + offset: id.0, + })?; + Ok(()) +}