#[path = "scanner_header.rs"]
mod scanner_header;
use scanner_header::data_section_start;
#[path = "scanner_attributes.rs"]
mod scanner_attributes;
pub use scanner_attributes::{nth_attribute_is_present, StepListItems};
pub(crate) use scanner_attributes::argument_list_start;
#[path = "scanner_diagnostics.rs"]
mod scanner_diagnostics;
#[path = "scanner_record_end.rs"]
mod scanner_record_end;
pub struct EntityScanner<'a> {
bytes: &'a [u8],
position: usize,
skipped_oversized_id_starts: Vec<usize>,
malformed_record_starts: Vec<usize>,
}
impl<'a> EntityScanner<'a> {
pub fn new<T>(content: &'a T) -> Self
where
T: AsRef<[u8]> + ?Sized,
{
let bytes = content.as_ref();
Self {
bytes,
position: data_section_start(bytes),
skipped_oversized_id_starts: Vec::new(),
malformed_record_starts: Vec::new(),
}
}
pub fn new_at<T>(content: &'a T, position: usize) -> Self
where
T: AsRef<[u8]> + ?Sized,
{
let bytes = content.as_ref();
let clamped = position.min(bytes.len());
Self {
bytes,
position: clamped,
skipped_oversized_id_starts: Vec::new(),
malformed_record_starts: Vec::new(),
}
}
pub fn position(&self) -> usize {
self.position
}
fn mark_malformed(&mut self, at: usize) {
if self.malformed_record_starts.last() != Some(&at) {
self.malformed_record_starts.push(at);
}
}
#[inline]
pub fn next_entity(&mut self) -> Option<(u32, &'a str, usize, usize)> {
let bytes = self.bytes;
let len = bytes.len();
loop {
let (line_start, parsed_id, eq_pos) = loop {
let remaining = &bytes[self.position..];
let next = memchr::memchr2(b'#', b'/', remaining)?;
let candidate = self.position + next;
let candidate_byte = bytes[candidate];
if candidate_byte == b'/' {
if candidate + 1 < len && bytes[candidate + 1] == b'*' {
match super::lexical::skip_step_comment(bytes, candidate) {
Some(next_pos) => {
self.position = next_pos;
continue;
}
None => {
self.mark_malformed(candidate);
return None;
}
}
}
self.position = candidate + 1;
continue;
}
let after = candidate + 1;
let (digit_count, parsed_id) =
crate::express_id::parse_express_id_prefix(&bytes[after..]);
if digit_count == 0 {
self.position = after;
continue;
}
let digit_end = after + digit_count;
let probe = super::lexical::skip_step_trivia(bytes, digit_end).unwrap_or(len);
if probe < len && bytes[probe] == b'=' {
let after_eq = super::lexical::skip_step_trivia(bytes, probe + 1).unwrap_or(len);
if after_eq < len
&& matches!(bytes[after_eq], b'A'..=b'Z' | b'a'..=b'z' | b'!' | b'(' | 0x80..=0xFF)
{
break (candidate, parsed_id, probe);
}
}
self.position = digit_end;
};
let body_start = eq_pos + 1;
let line_content = &bytes[body_start..];
let end_offset = match self.find_entity_end(line_content) {
Some(o) => o,
None => {
self.mark_malformed(line_start);
self.position = match super::lexical::close_step_record(line_content) {
super::lexical::RecordClose::At(o) => body_start + o,
super::lexical::RecordClose::Unbalanced => line_start + 1,
super::lexical::RecordClose::Unreadable => return None,
};
continue;
}
};
let line_end = body_start + end_offset + 1;
let Some(id) = parsed_id else {
self.skipped_oversized_id_starts.push(line_start);
self.position = line_end;
continue;
};
let type_start = super::lexical::skip_step_trivia(&self.bytes[..line_end], eq_pos + 1)
.unwrap_or(line_end);
let mut type_end = type_start;
let mut type_bytes_or = 0u8;
while type_end < line_end {
let b = self.bytes[type_end];
if b == b'(' || super::lexical::is_step_space(b) {
break;
}
if b == b'/' && self.bytes.get(type_end + 1) == Some(&b'*') {
break;
}
type_bytes_or |= b;
type_end += 1;
}
let type_bytes = &self.bytes[type_start..type_end];
let type_name = if type_bytes_or.is_ascii() {
unsafe { std::str::from_utf8_unchecked(type_bytes) }
} else {
std::str::from_utf8(type_bytes).unwrap_or("UNKNOWN")
};
self.position = line_end;
return Some((id, type_name, line_start, line_end));
}
}
pub fn find_by_type(&mut self, target_type: &str) -> Vec<(u32, usize, usize)> {
let mut results = Vec::new();
while let Some((id, type_name, start, end)) = self.next_entity() {
if type_name.eq_ignore_ascii_case(target_type) {
results.push((id, start, end));
}
}
results
}
pub fn count_by_type(&mut self) -> rustc_hash::FxHashMap<String, usize> {
let mut counts = rustc_hash::FxHashMap::default();
while let Some((_, type_name, _, _)) = self.next_entity() {
*counts.entry(type_name.to_string()).or_insert(0) += 1;
}
counts
}
pub fn count(&mut self) -> usize {
let mut n = 0usize;
while self.next_entity().is_some() {
n += 1;
}
n
}
pub fn reset(&mut self) {
self.position = data_section_start(self.bytes);
self.skipped_oversized_id_starts.clear();
self.malformed_record_starts.clear();
}
}
pub fn entity_count<T>(content: &T) -> usize
where
T: AsRef<[u8]> + ?Sized,
{
EntityScanner::new(content).count()
}
#[cfg(test)]
#[path = "scanner_tests.rs"]
mod scanner_tests;