use std::io;
use crate::char_stream::{CharStream, PositionSummary, TextInterval};
use crate::int_stream::{EOF, IntStream, UNKNOWN_SOURCE_NAME};
#[derive(Clone, Debug)]
pub struct ByteStream<B = Vec<u8>> {
bytes: B,
cursor: usize,
source_name: String,
}
impl ByteStream<Vec<u8>> {
pub fn from_reader(mut reader: impl io::Read) -> io::Result<Self> {
let mut bytes = Vec::new();
reader.read_to_end(&mut bytes)?;
Ok(Self::new(bytes))
}
}
impl<B: AsRef<[u8]>> ByteStream<B> {
pub fn new(bytes: B) -> Self {
Self::with_source_name(bytes, UNKNOWN_SOURCE_NAME)
}
pub fn with_source_name(bytes: B, source_name: impl Into<String>) -> Self {
Self {
bytes,
cursor: 0,
source_name: source_name.into(),
}
}
#[must_use]
pub fn bytes(&self) -> &[u8] {
self.bytes.as_ref()
}
#[must_use]
pub fn is_eof(&self) -> bool {
self.cursor >= self.bytes.as_ref().len()
}
}
impl<B: AsRef<[u8]>> IntStream for ByteStream<B> {
fn consume(&mut self) {
if !self.is_eof() {
self.cursor += 1;
}
}
fn la(&mut self, offset: isize) -> i32 {
if offset == 0 {
return 0;
}
let absolute = if offset > 0 {
self.cursor.checked_add((offset - 1).cast_unsigned())
} else {
offset
.checked_neg()
.and_then(|distance| usize::try_from(distance).ok())
.and_then(|distance| self.cursor.checked_sub(distance))
};
absolute.map_or(EOF, |index| self.symbol_at(index).unwrap_or(EOF))
}
fn index(&self) -> usize {
self.cursor
}
fn seek(&mut self, index: usize) {
self.cursor = index.min(self.bytes.as_ref().len());
}
fn size(&self) -> usize {
self.bytes.as_ref().len()
}
fn source_name(&self) -> &str {
&self.source_name
}
}
impl<B: AsRef<[u8]>> CharStream for ByteStream<B> {
fn text(&self, interval: TextInterval) -> String {
let bytes = self.bytes.as_ref();
let len = bytes.len();
if interval.is_empty() || len == 0 {
return String::new();
}
let start = interval.start.min(len);
let stop = interval.stop.min(len - 1);
if start > stop {
return String::new();
}
use std::fmt::Write as _;
bytes[start..=stop].iter().fold(
String::with_capacity((stop - start + 1) * 2),
|mut acc, byte| {
let _ = write!(acc, "{byte:02x}");
acc
},
)
}
fn symbol_at(&self, index: usize) -> Option<i32> {
Some(
self.bytes
.as_ref()
.get(index)
.map_or(EOF, |&byte| i32::from(byte)),
)
}
fn position_summary(&self, start: usize, end: usize) -> Option<PositionSummary> {
let bytes = self.bytes.as_ref();
let len = bytes.len();
if start > end {
return None;
}
let start = start.min(len);
let end = end.min(len);
let mut summary = PositionSummary::default();
for &byte in &bytes[start..end] {
if byte == b'\n' {
summary.line_breaks += 1;
summary.trailing_columns = 0;
} else {
summary.trailing_columns += 1;
}
}
Some(summary)
}
fn byte_interval(&self, interval: TextInterval) -> Option<(usize, usize)> {
let len = self.bytes.as_ref().len();
if interval.is_empty() || len == 0 {
return None;
}
let start = interval.start.min(len);
let stop = interval.stop.min(len - 1);
(start <= stop).then_some((start, stop + 1))
}
}
#[cfg(test)]
#[allow(clippy::disallowed_methods)] mod tests {
use super::*;
#[test]
fn lookahead_reads_bytes_including_high_bytes() {
let mut stream = ByteStream::new(vec![0x00, 0x7F, 0x80, 0xFF]);
assert_eq!(stream.la(0), 0, "la(0) is the ANTLR sentinel, not EOF");
assert_eq!(stream.la(1), 0x00);
assert_eq!(stream.la(2), 0x7F);
assert_eq!(stream.la(3), 0x80, "high byte is 128, not sign-extended");
assert_eq!(stream.la(4), 0xFF);
assert_eq!(stream.la(5), EOF);
stream.consume();
assert_eq!(stream.index(), 1);
assert_eq!(stream.la(-1), 0x00);
assert_eq!(stream.la(isize::MIN), EOF, "no panic on extreme offset");
}
#[test]
fn consume_stops_at_eof_and_seek_clamps() {
let mut stream = ByteStream::new(vec![0x01, 0x02]);
assert_eq!(stream.size(), 2);
stream.consume();
stream.consume();
stream.consume(); assert_eq!(stream.index(), 2);
assert!(stream.is_eof());
stream.seek(99);
assert_eq!(stream.index(), 2, "seek clamps to size");
stream.seek(1);
assert_eq!(stream.la(1), 0x02);
}
#[test]
fn text_is_lowercase_hex_and_byte_interval_is_exact() {
let stream = ByteStream::new(vec![0xDE, 0xAD, 0xBE, 0xEF]);
assert_eq!(stream.text(TextInterval::new(0, 3)), "deadbeef");
assert_eq!(stream.text(TextInterval::new(1, 2)), "adbe");
assert_eq!(stream.text(TextInterval::empty()), "");
assert_eq!(stream.byte_interval(TextInterval::new(1, 2)), Some((1, 3)));
assert_eq!(stream.byte_interval(TextInterval::empty()), None);
assert_eq!(stream.symbol_at(0), Some(0xDE));
assert_eq!(stream.symbol_at(4), Some(EOF));
}
#[test]
fn text_and_byte_interval_clamp_usize_max_without_overflow() {
let stream = ByteStream::new(vec![0xDE, 0xAD]);
assert_eq!(stream.text(TextInterval::new(0, usize::MAX)), "dead");
assert_eq!(
stream.byte_interval(TextInterval::new(0, usize::MAX)),
Some((0, 2)),
);
assert_eq!(stream.text(TextInterval::new(5, usize::MAX)), "");
}
#[test]
fn position_summary_scans_raw_bytes_not_hex() {
let stream = ByteStream::new(vec![0x41, 0x0A, 0x42, 0x43]);
assert_eq!(
stream.position_summary(0, 4),
Some(PositionSummary {
line_breaks: 1,
trailing_columns: 2,
}),
);
assert_eq!(
stream.position_summary(2, 4),
Some(PositionSummary {
line_breaks: 0,
trailing_columns: 2,
}),
);
assert_eq!(stream.position_summary(4, 2), None);
}
#[test]
fn borrows_bytes_zero_copy() {
let buffer: [u8; 4] = [0xCA, 0xFE, 0xBA, 0xBE];
let mut stream = ByteStream::new(&buffer[..]);
assert_eq!(stream.la(1), 0xCA);
assert_eq!(stream.size(), 4);
assert_eq!(buffer[0], 0xCA);
}
#[test]
fn from_reader_drains_any_read() {
let source = io::Cursor::new(vec![0x4D, 0x54, 0x68, 0x64]); let mut stream = ByteStream::from_reader(source).expect("cursor read is infallible");
assert_eq!(stream.size(), 4);
assert_eq!(stream.la(1), 0x4D);
assert_eq!(stream.text(TextInterval::new(0, 3)), "4d546864");
}
}