use crate::error::PdfError;
use crate::filters;
use crate::lexer::{Lexer, Token, parse_dict_body};
use crate::objects::PdfDict;
#[derive(Debug, Clone, Copy)]
pub enum XrefEntry {
InFile { offset: usize, generation: u16 },
InStream {
stream_obj_num: u32,
index_within: u16,
},
Free,
}
#[derive(Clone)]
pub struct XrefTable {
entries: Vec<Option<XrefEntry>>,
pub trailer: PdfDict,
}
impl XrefTable {
#[cfg(test)]
pub(crate) fn empty() -> Self {
Self {
entries: Vec::new(),
trailer: PdfDict::new(),
}
}
pub fn get(&self, obj_num: u32) -> Option<&XrefEntry> {
self.entries.get(obj_num as usize).and_then(|e| e.as_ref())
}
pub fn entries(&self) -> &[Option<XrefEntry>] {
&self.entries
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
pub fn parse_xref(data: &[u8]) -> Result<XrefTable, PdfError> {
let startxref = match find_startxref(data) {
Ok(v) => v,
Err(PdfError::NoStartXref) => return rebuild_xref_from_scan(data),
Err(e) => return Err(e),
};
let header_offset = data.windows(5).position(|w| w == b"%PDF-").unwrap_or(0);
let pdf_headers: Vec<usize> = find_all_pdf_headers(data);
let mut sections: Vec<(Vec<(u32, XrefEntry)>, PdfDict, usize)> = Vec::new();
let mut offset = startxref + header_offset;
let mut visited = std::collections::HashSet::new();
let mut xref_failed = false;
if offset < data.len() && parse_xref_section(data, offset).is_err() && pdf_headers.len() > 1 {
for &h in pdf_headers.iter().skip(1).rev() {
let try_offset = startxref + h;
if try_offset < data.len() {
if let Ok((mut entries, trailer)) = parse_xref_section(data, try_offset) {
shift_xref_entries(&mut entries, h);
visited.insert(try_offset);
let prev = trailer.get_int(b"Prev").map(|v| v as usize + h);
sections.push((entries, trailer, try_offset));
if let Some(p) = prev {
offset = p;
} else {
xref_failed = false;
}
break;
}
}
}
}
loop {
if !visited.insert(offset) {
break; }
match parse_xref_section(data, offset) {
Ok((entries, trailer)) => {
let prev = trailer.get_int(b"Prev").map(|v| v as usize + header_offset);
sections.push((entries, trailer, offset));
match prev {
Some(p) => offset = p,
None => break,
}
}
Err(_) => {
xref_failed = true;
break;
}
}
}
if xref_failed && sections.is_empty() {
discover_orphaned_xref_sections(
data,
&mut sections,
&mut visited,
header_offset,
&pdf_headers,
);
if sections.is_empty() {
return rebuild_xref_from_scan(data);
}
}
discover_orphaned_xref_sections(
data,
&mut sections,
&mut visited,
header_offset,
&pdf_headers,
);
sections.sort_by_key(|(_, _, file_offset)| *file_offset);
let mut combined_entries: Vec<Option<XrefEntry>> = Vec::new();
let mut final_trailer = PdfDict::new();
for (entries, trailer, _) in sections {
for (num, entry) in entries {
let idx = num as usize;
if idx >= combined_entries.len() {
combined_entries.resize(idx + 1, None);
}
combined_entries[idx] = Some(entry);
}
for (key, val) in trailer.into_entries() {
final_trailer.insert(key, val);
}
}
if final_trailer.get(b"Root").is_none() {
return rebuild_xref_from_scan(data);
}
if header_offset > 0 {
for entry in combined_entries.iter_mut().flatten() {
if let XrefEntry::InFile { offset, .. } = entry {
*offset += header_offset;
}
}
}
supplement_xref_from_scan(data, &mut combined_entries);
Ok(XrefTable {
entries: combined_entries,
trailer: final_trailer,
})
}
fn discover_orphaned_xref_sections(
data: &[u8],
sections: &mut Vec<(Vec<(u32, XrefEntry)>, PdfDict, usize)>,
visited: &mut std::collections::HashSet<usize>,
header_offset: usize,
pdf_headers: &[usize],
) {
let mut candidates = Vec::new();
let mut pos = 0;
while pos + 7 < data.len() {
if &data[pos..pos + 7] == b"trailer" {
let end = (pos + 500).min(data.len());
if let Some(prev_off) = find_int_after_key(&data[pos..end], b"/Prev") {
candidates.push(prev_off + header_offset);
}
}
if pos + 9 < data.len() && &data[pos..pos + 9] == b"startxref" {
let end = (pos + 50).min(data.len());
if let Some(sx_off) = find_int_after_key(&data[pos..end], b"startxref") {
if sx_off > 0 {
candidates.push(sx_off + header_offset);
for &h in pdf_headers.iter().skip(1) {
let adjusted = sx_off + h;
if adjusted < data.len() {
candidates.push(adjusted);
}
}
}
}
}
if pos + 4 <= data.len() && &data[pos..pos + 4] == b"xref" {
if pos == 0 || data[pos - 1] != b't' {
candidates.push(pos);
}
}
pos += 1;
}
for candidate in candidates {
let mut offset = candidate;
loop {
if !visited.insert(offset) {
break;
}
match parse_xref_section(data, offset) {
Ok((mut entries, trailer)) => {
let section_header = owning_pdf_header(offset, pdf_headers);
if section_header > 0 {
shift_xref_entries(&mut entries, section_header);
}
let prev = trailer.get_int(b"Prev").map(|v| v as usize + header_offset);
sections.push((entries, trailer, offset));
match prev {
Some(p) => offset = p,
None => break,
}
}
Err(_) => break,
}
}
}
}
fn supplement_xref_from_scan(data: &[u8], entries: &mut Vec<Option<XrefEntry>>) {
let mut pos = 0;
while pos < data.len() {
if pos > 0 && data[pos - 1] != b'\n' && data[pos - 1] != b'\r' {
while pos < data.len() && data[pos] != b'\n' && data[pos] != b'\r' {
pos += 1;
}
if pos < data.len() {
if data[pos] == b'\r' && pos + 1 < data.len() && data[pos + 1] == b'\n' {
pos += 2;
} else {
pos += 1;
}
}
continue;
}
if pos < data.len()
&& data[pos].is_ascii_digit()
&& let Some((obj_num, generation, obj_offset)) = try_parse_obj_header(data, pos)
{
let idx = obj_num as usize;
if idx < 100_000 {
let dominated_by_existing = entries.get(idx).is_some_and(|e| {
matches!(
e,
Some(XrefEntry::InFile { .. } | XrefEntry::InStream { .. })
)
});
if !dominated_by_existing {
if idx >= entries.len() {
entries.resize(idx + 1, None);
}
entries[idx] = Some(XrefEntry::InFile {
offset: obj_offset,
generation,
});
}
}
}
while pos < data.len() && data[pos] != b'\n' && data[pos] != b'\r' {
pos += 1;
}
if pos < data.len() {
if data[pos] == b'\r' && pos + 1 < data.len() && data[pos + 1] == b'\n' {
pos += 2;
} else {
pos += 1;
}
}
}
}
fn find_int_after_key(data: &[u8], key: &[u8]) -> Option<usize> {
let s = std::str::from_utf8(data).ok()?;
let key_str = std::str::from_utf8(key).ok()?;
let idx = s.find(key_str)?;
let after = &s[idx + key_str.len()..];
let after = after.trim_start();
let end = after
.find(|c: char| !c.is_ascii_digit())
.unwrap_or(after.len());
if end == 0 {
return None;
}
after[..end].parse().ok()
}
fn rebuild_xref_from_scan(data: &[u8]) -> Result<XrefTable, PdfError> {
let mut entries: Vec<Option<XrefEntry>> = Vec::new();
let mut pos = 0;
while pos < data.len() {
if pos > 0 && data[pos - 1] != b'\n' && data[pos - 1] != b'\r' {
while pos < data.len() && data[pos] != b'\n' && data[pos] != b'\r' {
pos += 1;
}
if pos < data.len() {
if data[pos] == b'\r' && pos + 1 < data.len() && data[pos + 1] == b'\n' {
pos += 2;
} else {
pos += 1;
}
}
continue;
}
if pos < data.len()
&& data[pos].is_ascii_digit()
&& let Some((obj_num, generation, obj_offset)) = try_parse_obj_header(data, pos)
{
let idx = obj_num as usize;
if idx < 100_000 {
if idx >= entries.len() {
entries.resize(idx + 1, None);
}
entries[idx] = Some(XrefEntry::InFile {
offset: obj_offset,
generation,
});
}
}
while pos < data.len() && data[pos] != b'\n' && data[pos] != b'\r' {
pos += 1;
}
if pos < data.len() {
if data[pos] == b'\r' && pos + 1 < data.len() && data[pos + 1] == b'\n' {
pos += 2;
} else {
pos += 1;
}
}
}
for entry in entries.iter().flatten() {
if let XrefEntry::InFile { offset, .. } = entry {
let search_end = (*offset + 512).min(data.len());
let slice = &data[*offset..search_end];
if slice.windows(10).any(|w| w == b"/Type/XRef")
|| slice.windows(11).any(|w| w == b"/Type /XRef")
{
if let Ok((xref_entries, xref_trailer)) = parse_xref_stream(data, *offset) {
let mut merged = entries;
for (num, xentry) in xref_entries {
let idx = num as usize;
if idx < 100_000 {
if idx >= merged.len() {
merged.resize(idx + 1, None);
}
if merged[idx].is_none() {
merged[idx] = Some(xentry);
}
}
}
return Ok(XrefTable {
entries: merged,
trailer: xref_trailer,
});
}
}
}
}
let trailer = find_trailer_dict(data).unwrap_or_default();
let trailer = if trailer.get(b"Root").is_none() {
let mut t = trailer;
if let Some(root_num) = find_catalog_obj(data, &entries) {
t.insert(b"Root".to_vec(), crate::objects::PdfObj::Ref(root_num, 0));
}
t
} else {
trailer
};
Ok(XrefTable { entries, trailer })
}
fn try_parse_obj_header(data: &[u8], pos: usize) -> Option<(u32, u16, usize)> {
let mut p = pos;
let num_start = p;
while p < data.len() && data[p].is_ascii_digit() {
p += 1;
}
if p == num_start || p >= data.len() {
return None;
}
let obj_num: u32 = std::str::from_utf8(&data[num_start..p])
.ok()?
.parse()
.ok()?;
while p < data.len() && data[p] == b' ' {
p += 1;
}
let gen_start = p;
while p < data.len() && data[p].is_ascii_digit() {
p += 1;
}
if p == gen_start || p >= data.len() {
return None;
}
let generation: u16 = std::str::from_utf8(&data[gen_start..p])
.ok()?
.parse()
.ok()?;
while p < data.len() && data[p] == b' ' {
p += 1;
}
if p + 3 > data.len() || &data[p..p + 3] != b"obj" {
return None;
}
let after_obj = p + 3;
if after_obj < data.len()
&& !is_whitespace(data[after_obj])
&& !matches!(data[after_obj], b'<' | b'[' | b'(' | b'/')
{
return None;
}
Some((obj_num, generation, pos))
}
fn find_trailer_dict(data: &[u8]) -> Option<PdfDict> {
let needle = b"trailer";
let mut pos = data.len().saturating_sub(needle.len());
while pos > 0 {
if &data[pos..pos + needle.len()] == needle {
let dict_start = pos + needle.len();
let mut lexer = Lexer::at(data, dict_start);
if let Ok(Token::DictBegin) = lexer.next_token()
&& let Ok(dict) = parse_dict_body(&mut lexer)
{
return Some(dict);
}
}
pos -= 1;
}
None
}
fn find_catalog_obj(data: &[u8], entries: &[Option<XrefEntry>]) -> Option<u32> {
for (num, entry) in entries.iter().enumerate() {
if let Some(XrefEntry::InFile { offset, .. }) = entry {
let search_end = (*offset + 512).min(data.len());
let slice = &data[*offset..search_end];
if let Some(idx) = slice.windows(8).position(|w| w == b"/Catalog") {
if idx > 5 && slice[..idx].windows(5).any(|w| w == b"/Type") {
return Some(num as u32);
}
}
if slice.windows(5).any(|w| w == b"/Root")
&& slice.windows(10).any(|w| w == b"/Type/XRef")
{
let obj_start = slice
.windows(3)
.position(|w| w == b"obj")
.map(|p| p + 3)
.unwrap_or(0);
let mut lexer = Lexer::at(slice, obj_start);
if let Ok(Token::DictBegin) = lexer.next_token()
&& let Ok(dict) = parse_dict_body(&mut lexer)
{
if let Some((root_num, _)) = dict.get_ref(b"Root") {
return Some(root_num);
}
}
}
}
}
None
}
fn parse_xref_section(
data: &[u8],
offset: usize,
) -> Result<(Vec<(u32, XrefEntry)>, PdfDict), PdfError> {
let mut pos = offset;
while pos < data.len() && is_whitespace(data[pos]) {
pos += 1;
}
if pos + 4 <= data.len() && &data[pos..pos + 4] == b"xref" {
parse_classic_xref(data, pos)
} else {
let scan_start = offset.saturating_sub(20);
let found = (scan_start..offset)
.rev()
.find(|&off| off + 4 <= data.len() && &data[off..off + 4] == b"xref");
if let Some(xref_pos) = found {
parse_classic_xref(data, xref_pos)
} else {
match parse_xref_stream(data, offset) {
Ok(result) => Ok(result),
Err(_) => {
let scan_start = offset.saturating_sub(256);
let scan_end = offset.min(data.len());
for search in (scan_start..scan_end).rev() {
if data[search].is_ascii_digit() {
if let Some((_, _, obj_offset)) = try_parse_obj_header(data, search) {
if obj_offset == search {
if let Ok(result) = parse_xref_stream(data, search) {
return Ok(result);
}
}
}
}
}
parse_xref_stream(data, offset) }
}
}
}
}
fn parse_classic_xref(
data: &[u8],
offset: usize,
) -> Result<(Vec<(u32, XrefEntry)>, PdfDict), PdfError> {
let mut pos = offset + 4;
while pos < data.len() && is_whitespace(data[pos]) {
pos += 1;
}
let mut entries = Vec::new();
let mut has_trailer = false;
loop {
if pos + 7 <= data.len() && &data[pos..pos + 7] == b"trailer" {
pos += 7;
has_trailer = true;
break;
}
let Ok((first_obj, new_pos)) = parse_int_at(data, pos) else {
break;
};
pos = new_pos;
while pos < data.len() && (data[pos] == b' ' || data[pos] == b'\t') {
pos += 1;
}
let Ok((count, new_pos)) = parse_int_at(data, pos) else {
break;
};
pos = new_pos;
while pos < data.len() && is_whitespace(data[pos]) {
pos += 1;
}
let mut entry_error = false;
for i in 0..count as u32 {
if pos + 18 > data.len() {
break;
}
let Ok(off_str) = std::str::from_utf8(&data[pos..pos + 10]) else {
entry_error = true;
break;
};
let Ok(off) = off_str.trim().parse::<usize>() else {
entry_error = true;
break;
};
let Ok(gen_str) = std::str::from_utf8(&data[pos + 11..pos + 16]) else {
entry_error = true;
break;
};
let Ok(gen_val) = gen_str.trim().parse::<u32>() else {
entry_error = true;
break;
};
let generation = gen_val.min(65535) as u16;
let type_byte = data[pos + 17];
let obj_num = first_obj as u32 + i;
let entry = match type_byte {
b'n' => XrefEntry::InFile {
offset: off,
generation,
},
b'f' => XrefEntry::Free,
_ => XrefEntry::Free,
};
entries.push((obj_num, entry));
pos += 18;
while pos < data.len()
&& (data[pos] == b' ' || data[pos] == b'\r' || data[pos] == b'\n')
{
pos += 1;
}
}
if entry_error {
break;
}
while pos < data.len() && is_whitespace(data[pos]) {
pos += 1;
}
}
let trailer = if has_trailer {
let mut lexer = Lexer::at(data, pos);
lexer.set_pos(pos);
let tok = lexer.next_token()?;
match tok {
Token::DictBegin => parse_dict_body(&mut lexer)?,
_ => return Err(PdfError::MalformedTrailer),
}
} else if entries.is_empty() {
return Err(PdfError::MalformedXref(offset));
} else {
PdfDict::new()
};
Ok((entries, trailer))
}
fn parse_xref_stream(
data: &[u8],
offset: usize,
) -> Result<(Vec<(u32, XrefEntry)>, PdfDict), PdfError> {
let mut lexer = Lexer::at(data, offset);
let _obj_num = match lexer.next_token()? {
Token::Int(n) => n,
t => {
return Err(PdfError::UnexpectedToken {
expected: "object number".into(),
got: format!("{t:?}"),
});
}
};
match lexer.next_token()? {
Token::Int(_) => {}
t => {
return Err(PdfError::UnexpectedToken {
expected: "generation number".into(),
got: format!("{t:?}"),
});
}
}
match lexer.next_token()? {
Token::Keyword(ref kw) if kw == b"obj" => {}
t => {
return Err(PdfError::UnexpectedToken {
expected: "obj".into(),
got: format!("{t:?}"),
});
}
}
match lexer.next_token()? {
Token::DictBegin => {}
t => {
return Err(PdfError::UnexpectedToken {
expected: "<<".into(),
got: format!("{t:?}"),
});
}
}
let dict = parse_dict_body(&mut lexer)?;
let tok = lexer.next_token()?;
if !matches!(tok, Token::Keyword(ref kw) if kw == b"stream") {
return Err(PdfError::UnexpectedToken {
expected: "stream".into(),
got: format!("{tok:?}"),
});
}
let mut data_start = lexer.pos();
if data_start < data.len() && data[data_start] == b'\r' {
data_start += 1;
}
if data_start < data.len() && data[data_start] == b'\n' {
data_start += 1;
}
let length = match dict.get_int(b"Length") {
Some(n) => n as usize,
None => {
let needle = b"endstream";
let search_end = data.len().saturating_sub(needle.len());
let mut pos = data_start;
let mut found = None;
while pos <= search_end {
if &data[pos..pos + needle.len()] == needle {
let mut end = pos;
while end > data_start && matches!(data[end - 1], b' ' | b'\r' | b'\n') {
end -= 1;
}
found = Some(end - data_start);
break;
}
pos += 1;
}
found.ok_or(PdfError::StreamMissingLength)?
}
};
let raw_data = &data[data_start..std::cmp::min(data_start + length, data.len())];
let (filter_list, parms) = filters::parse_filters(&dict, None)?;
let stream_data = if filter_list.is_empty() {
raw_data.to_vec()
} else {
filters::decode_stream(raw_data, &filter_list, &parms, None)?
};
let w = dict.get_array(b"W").ok_or(PdfError::MissingKey("W"))?;
if w.len() != 3 {
return Err(PdfError::Other(
"xref stream /W must have 3 elements".into(),
));
}
let w1 = w[0].as_int().unwrap_or(0) as usize;
let w2 = w[1].as_int().unwrap_or(0) as usize;
let w3 = w[2].as_int().unwrap_or(0) as usize;
let entry_size = w1 + w2 + w3;
if entry_size == 0 {
return Err(PdfError::Other("xref stream entry size is 0".into()));
}
let size = dict.get_int(b"Size").ok_or(PdfError::MissingKey("Size"))? as u32;
let index_pairs: Vec<(u32, u32)> = if let Some(index_arr) = dict.get_array(b"Index") {
index_arr
.chunks(2)
.filter_map(|pair| {
if pair.len() == 2 {
Some((pair[0].as_int()? as u32, pair[1].as_int()? as u32))
} else {
None
}
})
.collect()
} else {
vec![(0, size)]
};
let mut entries = Vec::new();
let mut stream_pos = 0;
for (first_obj, count) in &index_pairs {
for i in 0..*count {
if stream_pos + entry_size > stream_data.len() {
break;
}
let field1 = read_field(&stream_data[stream_pos..], w1);
let field2 = read_field(&stream_data[stream_pos + w1..], w2);
let field3 = read_field(&stream_data[stream_pos + w1 + w2..], w3);
stream_pos += entry_size;
let entry_type = if w1 == 0 { 1 } else { field1 };
let obj_num = first_obj + i;
let entry = match entry_type {
0 => XrefEntry::Free,
1 => XrefEntry::InFile {
offset: field2 as usize,
generation: field3 as u16,
},
2 => XrefEntry::InStream {
stream_obj_num: field2 as u32,
index_within: field3 as u16,
},
_ => XrefEntry::Free, };
entries.push((obj_num, entry));
}
}
Ok((entries, dict))
}
fn read_field(data: &[u8], width: usize) -> u64 {
let mut val: u64 = 0;
for i in 0..width {
if i < data.len() {
val = (val << 8) | data[i] as u64;
}
}
val
}
fn find_all_pdf_headers(data: &[u8]) -> Vec<usize> {
let mut headers = Vec::new();
let mut pos = 0;
while pos + 5 <= data.len() {
if &data[pos..pos + 5] == b"%PDF-" {
headers.push(pos);
pos += 5;
} else {
pos += 1;
}
}
headers
}
fn owning_pdf_header(pos: usize, pdf_headers: &[usize]) -> usize {
match pdf_headers.iter().rposition(|&h| h <= pos) {
Some(idx) if idx > 0 => pdf_headers[idx],
_ => 0,
}
}
fn shift_xref_entries(entries: &mut [(u32, XrefEntry)], base: usize) {
for (_, entry) in entries.iter_mut() {
if let XrefEntry::InFile { offset, .. } = entry {
*offset += base;
}
}
}
fn find_startxref(data: &[u8]) -> Result<usize, PdfError> {
let needle = b"startxref";
let eof_marker = b"%%EOF";
let mut eof_pos = None;
for i in (0..data.len().saturating_sub(eof_marker.len())).rev() {
if &data[i..i + eof_marker.len()] == eof_marker {
eof_pos = Some(i);
break;
}
}
if let Some(eof) = eof_pos {
let search_start = eof.saturating_sub(1024);
let region = &data[search_start..eof];
let mut found = None;
for i in 0..region.len().saturating_sub(needle.len()) {
if ®ion[i..i + needle.len()] == needle {
found = Some(search_start + i);
}
}
if let Some(pos) = found {
let mut p = pos + needle.len();
while p < data.len() && is_whitespace(data[p]) {
p += 1;
}
let (offset, _) = parse_int_at(data, p)?;
return Ok(offset as usize);
}
}
let search_start = data.len().saturating_sub(1024);
let tail = &data[search_start..];
let mut found = None;
for i in 0..tail.len().saturating_sub(needle.len()) {
if &tail[i..i + needle.len()] == needle {
found = Some(search_start + i);
}
}
if let Some(pos) = found {
let mut p = pos + needle.len();
while p < data.len() && is_whitespace(data[p]) {
p += 1;
}
let (offset, _) = parse_int_at(data, p)?;
return Ok(offset as usize);
}
let xref_kw = b"xref";
let mut last_xref = None;
for i in (0..data.len().saturating_sub(xref_kw.len())).rev() {
if &data[i..i + xref_kw.len()] == xref_kw
&& (i == 0 || is_whitespace(data[i - 1]) || data[i - 1] == b'\n')
{
last_xref = Some(i);
break;
}
}
last_xref.ok_or(PdfError::NoStartXref)
}
fn parse_int_at(data: &[u8], pos: usize) -> Result<(i64, usize), PdfError> {
let mut p = pos;
while p < data.len() && is_whitespace(data[p]) {
p += 1;
}
let start = p;
if p < data.len() && (data[p] == b'+' || data[p] == b'-') {
p += 1;
}
while p < data.len() && data[p].is_ascii_digit() {
p += 1;
}
if p == start {
return Err(PdfError::Other(format!("expected integer at offset {pos}")));
}
let s = std::str::from_utf8(&data[start..p])
.map_err(|_| PdfError::Other(format!("invalid integer at offset {pos}")))?;
let n: i64 = s
.parse()
.map_err(|_| PdfError::Other(format!("invalid integer '{s}' at offset {pos}")))?;
Ok((n, p))
}
fn is_whitespace(b: u8) -> bool {
matches!(b, b' ' | b'\t' | b'\r' | b'\n' | 0x0C | 0x00)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn find_startxref_simple() {
let data = b"%PDF-1.4\nstartxref\n1234\n%%EOF\n";
let offset = find_startxref(data).unwrap();
assert_eq!(offset, 1234);
}
#[test]
fn find_startxref_trailing_garbage() {
let mut data = b"%PDF-1.4\nstartxref\n5678\n%%EOF\n".to_vec();
data.extend_from_slice(&[0xFFu8; 2000]);
let offset = find_startxref(&data).unwrap();
assert_eq!(offset, 5678);
}
#[test]
fn parse_int_at_basic() {
let (val, pos) = parse_int_at(b" 42 rest", 0).unwrap();
assert_eq!(val, 42);
assert_eq!(pos, 4);
}
#[test]
fn read_field_sizes() {
assert_eq!(read_field(&[0x01], 1), 1);
assert_eq!(read_field(&[0x01, 0x00], 2), 256);
assert_eq!(read_field(&[0x00, 0x01, 0x00], 3), 256);
}
}