use crate::error::{Error, Result};
use crate::limits::Limits;
pub const MODEL_VERSION: u8 = 1;
pub const MAX_MODEL_RECORDS: u32 = 1 << 24;
pub const MAX_MODEL_SLIDES: u32 = 1 << 20;
pub const MAX_MODEL_RUNS: u32 = 1 << 24;
pub const MAX_MODEL_TEXT_UNITS: u32 = 1 << 30;
pub const REC_VER_CONTAINER: u8 = 0x0F;
pub const REC_DOCUMENT: u16 = 1000;
pub const REC_SLIDE_LIST_WITH_TEXT: u16 = 4080;
pub const REC_SLIDE_PERSIST_ATOM: u16 = 1011;
pub const REC_TEXT_HEADER_ATOM: u16 = 3999;
pub const REC_TEXT_CHARS_ATOM: u16 = 4000;
pub const REC_TEXT_BYTES_ATOM: u16 = 4008;
pub const REC_USER_EDIT_ATOM: u16 = 4085;
pub const SLIDE_LIST_SLIDES: u16 = 0;
pub const SLIDE_LIST_MASTER: u16 = 1;
pub const SLIDE_LIST_NOTES: u16 = 2;
pub const RUN_CHARS: u8 = 0;
pub const RUN_BYTES: u8 = 1;
pub const FIELD_RUN: u8 = 0;
pub const STREAM_POWERPOINT_DOCUMENT: &str = "PowerPoint Document";
pub const STREAM_CURRENT_USER: &str = "Current User";
pub const fn run_kind_name(kind: u8) -> &'static str {
match kind {
RUN_BYTES => "bytes",
_ => "chars",
}
}
pub const fn record_type_name(rec_type: u16) -> &'static str {
match rec_type {
REC_DOCUMENT => "document",
REC_SLIDE_LIST_WITH_TEXT => "slide-list-with-text",
REC_SLIDE_PERSIST_ATOM => "slide-persist-atom",
REC_TEXT_HEADER_ATOM => "text-header-atom",
REC_TEXT_CHARS_ATOM => "text-chars-atom",
REC_TEXT_BYTES_ATOM => "text-bytes-atom",
REC_USER_EDIT_ATOM => "user-edit-atom",
_ => "unknown",
}
}
pub const fn slide_list_name(instance: u16) -> &'static str {
match instance {
SLIDE_LIST_SLIDES => "slides",
SLIDE_LIST_MASTER => "master",
SLIDE_LIST_NOTES => "notes",
_ => "other",
}
}
pub const fn field_name(field: u8) -> &'static str {
let _ = field;
"run"
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PptRecord {
pub rec_ver: u8,
pub rec_instance: u16,
pub rec_type: u16,
pub span: (u64, u64),
pub stream_off: u64,
pub len: u32,
pub depth: u32,
}
impl PptRecord {
pub const fn is_container(&self) -> bool {
self.rec_ver == REC_VER_CONTAINER
}
pub fn payload_span(&self) -> (u64, u64) {
(self.span.0 + 8, self.span.1)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PptSlide {
pub persist_id_ref: u32,
pub c_texts: u32,
pub list_instance: u16,
pub sp_span: (u64, u64),
pub run_start: u32,
pub run_end: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PptRun {
pub slide: u32,
pub kind: u8,
pub rec_type: u16,
pub header_span: (u64, u64),
pub span: (u64, u64),
pub text: Vec<u16>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PptCurrentUser {
pub span: (u64, u64),
pub edit_offset: u32,
pub user_edit_span: (u64, u64),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PptModel {
pub doc_len: u64,
pub stream_span: (u64, u64),
pub records: Vec<PptRecord>,
pub slides: Vec<PptSlide>,
pub runs: Vec<PptRun>,
pub current_user: Option<PptCurrentUser>,
}
impl PptModel {
pub fn slide(&self, index: u32) -> Option<&PptSlide> {
self.slides.get(index as usize)
}
pub fn run(&self, index: u32) -> Option<&PptRun> {
self.runs.get(index as usize)
}
pub fn slide_runs(&self, slide: &PptSlide) -> &[PptRun] {
let s = slide.run_start as usize;
let e = slide.run_end as usize;
self.runs.get(s..e).unwrap_or(&[])
}
pub fn char_runs(&self) -> u64 {
self.runs.iter().filter(|r| r.kind == RUN_CHARS).count() as u64
}
pub fn byte_runs(&self) -> u64 {
self.runs.iter().filter(|r| r.kind == RUN_BYTES).count() as u64
}
pub fn text_units(&self) -> u64 {
self.runs.iter().map(|r| r.text.len() as u64).sum()
}
pub fn encode(&self) -> Vec<u8> {
let mut out: Vec<u8> = Vec::new();
out.extend_from_slice(b"PPTM");
out.push(MODEL_VERSION);
out.extend_from_slice(&self.doc_len.to_le_bytes());
w_span(&mut out, self.stream_span);
out.extend_from_slice(&(self.records.len() as u32).to_le_bytes());
for r in &self.records {
out.push(r.rec_ver);
out.extend_from_slice(&r.rec_instance.to_le_bytes());
out.extend_from_slice(&r.rec_type.to_le_bytes());
w_span(&mut out, r.span);
out.extend_from_slice(&r.stream_off.to_le_bytes());
out.extend_from_slice(&r.len.to_le_bytes());
out.extend_from_slice(&r.depth.to_le_bytes());
}
out.extend_from_slice(&(self.slides.len() as u32).to_le_bytes());
for s in &self.slides {
out.extend_from_slice(&s.persist_id_ref.to_le_bytes());
out.extend_from_slice(&s.c_texts.to_le_bytes());
out.extend_from_slice(&s.list_instance.to_le_bytes());
w_span(&mut out, s.sp_span);
out.extend_from_slice(&s.run_start.to_le_bytes());
out.extend_from_slice(&s.run_end.to_le_bytes());
}
out.extend_from_slice(&(self.runs.len() as u32).to_le_bytes());
for r in &self.runs {
out.extend_from_slice(&r.slide.to_le_bytes());
out.push(r.kind);
out.extend_from_slice(&r.rec_type.to_le_bytes());
w_span(&mut out, r.header_span);
w_span(&mut out, r.span);
w_units(&mut out, &r.text);
}
match &self.current_user {
None => out.push(0),
Some(cu) => {
out.push(1);
w_span(&mut out, cu.span);
out.extend_from_slice(&cu.edit_offset.to_le_bytes());
w_span(&mut out, cu.user_edit_span);
}
}
out
}
pub fn decode(bytes: &[u8]) -> Result<PptModel> {
let mut r = BinReader::new(bytes);
if r.bytes(4)? != b"PPTM" {
return Err(corrupt("bad model magic"));
}
if r.u8()? != MODEL_VERSION {
return Err(corrupt("unsupported model version"));
}
let doc_len = r.u64()?;
let stream_span = r.span()?;
check_span(stream_span, doc_len, "PowerPoint Document stream span")?;
let stream_len = stream_span.1 - stream_span.0;
let n_records = r.u32()?;
if n_records > MAX_MODEL_RECORDS {
return Err(corrupt("model record count is implausible"));
}
let mut records = Vec::new();
for _ in 0..n_records {
let rec_ver = r.u8()?;
let rec_instance = r.u16()?;
let rec_type = r.u16()?;
let span = r.span()?;
check_span(span, doc_len, "record span")?;
let stream_off = r.u64()?;
let len = r.u32()?;
let depth = r.u32()?;
if span.1 - span.0 != 8 + len as u64 {
return Err(corrupt("record span disagrees with its length"));
}
if stream_off + span.1 - span.0 > stream_len {
return Err(corrupt(
"record lies outside the PowerPoint Document stream",
));
}
records.push(PptRecord {
rec_ver,
rec_instance,
rec_type,
span,
stream_off,
len,
depth,
});
}
let n_slides = r.u32()?;
if n_slides > MAX_MODEL_SLIDES {
return Err(corrupt("model slide count is implausible"));
}
let mut slides = Vec::new();
for _ in 0..n_slides {
let persist_id_ref = r.u32()?;
let c_texts = r.u32()?;
let list_instance = r.u16()?;
let sp_span = r.span()?;
check_span(sp_span, doc_len, "SlidePersistAtom span")?;
let run_start = r.u32()?;
let run_end = r.u32()?;
slides.push(PptSlide {
persist_id_ref,
c_texts,
list_instance,
sp_span,
run_start,
run_end,
});
}
let n_runs = r.u32()?;
if n_runs > MAX_MODEL_RUNS {
return Err(corrupt("model run count is implausible"));
}
let mut runs = Vec::new();
let mut units_total: u64 = 0;
for _ in 0..n_runs {
let slide = r.u32()?;
let kind = r.u8()?;
let rec_type = r.u16()?;
let header_span = r.span()?;
check_span(header_span, doc_len, "run header span")?;
let span = r.span()?;
check_span(span, doc_len, "run span")?;
let text = r.units()?;
units_total = units_total.saturating_add(text.len() as u64);
if units_total > MAX_MODEL_TEXT_UNITS as u64 {
return Err(corrupt("model text is implausible"));
}
runs.push(PptRun {
slide,
kind,
rec_type,
header_span,
span,
text,
});
}
for s in &slides {
if s.run_start > s.run_end || s.run_end as usize > runs.len() {
return Err(corrupt("a slide run range is out of bounds"));
}
}
for run in &runs {
if run.slide as usize >= slides.len() {
return Err(corrupt("a run names an out-of-range slide"));
}
}
let current_user = if r.u8()? != 0 {
let span = r.span()?;
check_span(span, doc_len, "Current User stream span")?;
let edit_offset = r.u32()?;
let user_edit_span = r.span()?;
check_span(user_edit_span, doc_len, "UserEditAtom span")?;
Some(PptCurrentUser {
span,
edit_offset,
user_edit_span,
})
} else {
None
};
if !r.at_end() {
return Err(corrupt("model has trailing bytes"));
}
Ok(PptModel {
doc_len,
stream_span,
records,
slides,
runs,
current_user,
})
}
}
fn check_span(span: (u64, u64), bound: u64, what: &'static str) -> Result<()> {
if span.0 > span.1 || span.1 > bound {
return Err(corrupt(format!("{what} is outside its container")));
}
Ok(())
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PptMatch {
pub field: u8,
pub index: u32,
pub start: u64,
pub end: u64,
pub text: String,
}
pub fn find(model: &PptModel, pattern: &str, max_bytes: u64) -> Result<Vec<PptMatch>> {
if pattern.is_empty() {
return Ok(Vec::new());
}
let mut out: Vec<PptMatch> = Vec::new();
let mut estimated: u64 = 0;
for (i, r) in model.runs.iter().enumerate() {
let text = String::from_utf16_lossy(&r.text);
if text.contains(pattern) {
estimated = estimated.saturating_add(64 + text.len() as u64);
if estimated > max_bytes {
return Err(Error::resource_limit(format!(
"ppt find exceeded the {max_bytes}-byte budget"
)));
}
out.push(PptMatch {
field: FIELD_RUN,
index: i as u32,
start: r.span.0,
end: r.span.1,
text,
});
}
}
Ok(out)
}
pub fn canonical_text(model: &PptModel, _source: &[u8], max_bytes: u64) -> Result<String> {
let mut out = String::new();
for (i, slide) in model.slides.iter().enumerate() {
if i > 0 {
out.push('\n');
}
for (k, run) in model.slide_runs(slide).iter().enumerate() {
if k > 0 {
out.push('\n');
}
out.push_str(&String::from_utf16_lossy(&run.text));
}
if out.len() as u64 > max_bytes {
return Err(Error::resource_limit(format!(
"ppt canonical text exceeded the {max_bytes}-byte budget"
)));
}
}
Ok(out)
}
pub fn detect(source: &[u8], limits: Limits) -> bool {
if source.len() as u64 > limits.max_ppt_document_bytes {
return false;
}
if !crate::adapter::cfbf::detect(source, limits) {
return false;
}
let Ok(model) = crate::adapter::cfbf::parse(source, limits) else {
return false;
};
let Some(entry) = find_named(&model, STREAM_POWERPOINT_DOCUMENT) else {
return false;
};
let Ok(stream) = crate::adapter::cfbf::stream_bytes(source, entry) else {
return false;
};
if stream.len() < 8 {
return false;
}
let w = u16::from_le_bytes([stream[0], stream[1]]);
let rec_ver = (w & 0x000F) as u8;
let rec_type = u16::from_le_bytes([stream[2], stream[3]]);
rec_ver == REC_VER_CONTAINER && rec_type == REC_DOCUMENT
}
pub fn parse(source: &[u8], limits: Limits) -> Result<PptModel> {
let len = source.len() as u64;
if len > limits.max_ppt_document_bytes {
return Err(Error::resource_limit(format!(
"ppt source is {len} bytes, above the {}-byte document cap",
limits.max_ppt_document_bytes
)));
}
let cfbf = crate::adapter::cfbf::parse(source, limits)?;
let entry = find_named(&cfbf, STREAM_POWERPOINT_DOCUMENT)
.ok_or_else(|| invalid("the compound file has no PowerPoint Document stream"))?;
let stream = crate::adapter::cfbf::stream_bytes(source, entry)?;
if stream.len() as u64 > limits.max_ppt_document_bytes {
return Err(Error::resource_limit(format!(
"the PowerPoint Document stream is {} bytes, above the {}-byte document cap",
stream.len(),
limits.max_ppt_document_bytes
)));
}
let base = entry.data_span.0;
let stream_span = (base, base + stream.len() as u64);
let records = scan_records(stream, base, limits)?;
let first = records
.first()
.ok_or_else(|| invalid("the PowerPoint Document stream is empty"))?;
if first.rec_ver != REC_VER_CONTAINER || first.rec_type != REC_DOCUMENT {
return Err(invalid(
"the stream does not begin with a Document record (recVer 0xF, recType 1000)",
));
}
let (slides, runs) = parse_slides(stream, &records, limits)?;
let text_units: u64 = runs.iter().map(|r| r.text.len() as u64).sum();
if text_units > limits.max_ppt_text_bytes {
return Err(Error::resource_limit(format!(
"ppt derived text is above the {}-byte text cap",
limits.max_ppt_text_bytes
)));
}
let current_user = parse_current_user(source, &cfbf)?;
Ok(PptModel {
doc_len: len,
stream_span,
records,
slides,
runs,
current_user,
})
}
pub fn build_ppt_model(source: &[u8], limits: Limits) -> Result<Vec<u8>> {
Ok(parse(source, limits)?.encode())
}
fn scan_records(stream: &[u8], base: u64, limits: Limits) -> Result<Vec<PptRecord>> {
let mut out: Vec<PptRecord> = Vec::new();
let mut off = 0usize;
let mut stack: Vec<usize> = Vec::new();
while off < stream.len() {
while stack.last().is_some_and(|&e| off >= e) {
stack.pop();
}
if off >= stream.len() {
break;
}
if stream.len() - off < 8 {
return Err(invalid("a record header is truncated"));
}
let w = u16::from_le_bytes([stream[off], stream[off + 1]]);
let rec_ver = (w & 0x000F) as u8;
let rec_instance = (w >> 4) & 0x0FFF;
let rec_type = u16::from_le_bytes([stream[off + 2], stream[off + 3]]);
let rec_len = u32::from_le_bytes([
stream[off + 4],
stream[off + 5],
stream[off + 6],
stream[off + 7],
]);
let rec_end = off
.checked_add(8)
.and_then(|o| o.checked_add(rec_len as usize))
.ok_or_else(|| invalid("a record length overflows"))?;
if rec_end > stream.len() {
return Err(invalid("a record extends past the stream"));
}
let parent_end = stack.last().copied().unwrap_or(stream.len());
if rec_end > parent_end {
return Err(invalid("a record extends past its enclosing container"));
}
let depth = stack.len() as u32;
if depth > limits.max_ppt_depth {
return Err(Error::resource_limit(format!(
"the record nesting depth exceeds {}",
limits.max_ppt_depth
)));
}
if out.len() as u64 >= limits.max_ppt_records {
return Err(Error::resource_limit(format!(
"the PowerPoint Document stream has more than {} records",
limits.max_ppt_records
)));
}
out.push(PptRecord {
rec_ver,
rec_instance,
rec_type,
span: (base + off as u64, base + rec_end as u64),
stream_off: off as u64,
len: rec_len,
depth,
});
if rec_ver == REC_VER_CONTAINER {
stack.push(rec_end);
off += 8;
} else {
off = rec_end;
}
}
Ok(out)
}
fn parse_slides(
stream: &[u8],
records: &[PptRecord],
limits: Limits,
) -> Result<(Vec<PptSlide>, Vec<PptRun>)> {
let mut slides: Vec<PptSlide> = Vec::new();
let mut runs: Vec<PptRun> = Vec::new();
for (i, r) in records.iter().enumerate() {
if r.rec_type != REC_SLIDE_LIST_WITH_TEXT || r.rec_ver != REC_VER_CONTAINER {
continue;
}
if r.rec_instance != SLIDE_LIST_SLIDES {
continue;
}
let container_end = r.stream_off + 8 + r.len as u64;
let child_depth = r.depth + 1;
let mut cur: Option<usize> = None;
let mut header_span: (u64, u64) = (0, 0);
for c in &records[i + 1..] {
if c.stream_off >= container_end {
break;
}
if c.depth != child_depth {
continue;
}
match c.rec_type {
REC_SLIDE_PERSIST_ATOM => {
let pl = payload(stream, c);
if pl.len() < 8 {
return Err(invalid("a SlidePersistAtom is truncated"));
}
let persist_id_ref = u32::from_le_bytes([pl[0], pl[1], pl[2], pl[3]]);
let c_texts = u32::from_le_bytes([pl[4], pl[5], pl[6], pl[7]]);
if slides.len() as u64 >= limits.max_ppt_slides {
return Err(Error::resource_limit(format!(
"the presentation has more than {} slides",
limits.max_ppt_slides
)));
}
cur = Some(slides.len());
slides.push(PptSlide {
persist_id_ref,
c_texts,
list_instance: r.rec_instance,
sp_span: c.span,
run_start: runs.len() as u32,
run_end: runs.len() as u32,
});
}
REC_TEXT_HEADER_ATOM => header_span = c.span,
REC_TEXT_CHARS_ATOM | REC_TEXT_BYTES_ATOM => {
let Some(si) = cur else {
return Err(invalid(
"a text atom appears before any SlidePersistAtom in a slide list",
));
};
let pl = payload(stream, c);
let (kind, text) = if c.rec_type == REC_TEXT_CHARS_ATOM {
if !pl.len().is_multiple_of(2) {
return Err(invalid(
"a TextCharsAtom payload length is not a multiple of two",
));
}
let text: Vec<u16> = pl
.as_chunks::<2>()
.0
.iter()
.map(|c| u16::from_le_bytes(*c))
.collect();
(RUN_CHARS, text)
} else {
(RUN_BYTES, pl.iter().map(|&b| cp1252(b)).collect())
};
if runs.len() as u64 >= limits.max_ppt_runs {
return Err(Error::resource_limit(format!(
"the presentation has more than {} text runs",
limits.max_ppt_runs
)));
}
runs.push(PptRun {
slide: si as u32,
kind,
rec_type: c.rec_type,
header_span,
span: c.span,
text,
});
slides[si].run_end = runs.len() as u32;
header_span = (0, 0);
}
_ => {}
}
}
}
Ok((slides, runs))
}
fn parse_current_user(
source: &[u8],
cfbf: &crate::adapter::cfbf::CfbfModel,
) -> Result<Option<PptCurrentUser>> {
let Some(entry) = find_named(cfbf, STREAM_CURRENT_USER) else {
return Ok(None);
};
let s = crate::adapter::cfbf::stream_bytes(source, entry)?;
if s.len() < 4 {
return Err(invalid("the Current User stream is too short"));
}
let edit_offset = u32::from_le_bytes([s[0], s[1], s[2], s[3]]) as u64;
let edit_end = edit_offset
.checked_add(8)
.ok_or_else(|| invalid("the Current User UserEditAtom offset overflows"))?;
if edit_end > s.len() as u64 {
return Err(invalid(
"the Current User UserEditAtom offset lies outside the stream",
));
}
let o = edit_offset as usize;
let rec_type = u16::from_le_bytes([s[o + 2], s[o + 3]]);
if rec_type != REC_USER_EDIT_ATOM {
return Err(invalid(
"the Current User stream does not point at a UserEditAtom record",
));
}
let base = entry.data_span.0;
Ok(Some(PptCurrentUser {
span: (base, base + s.len() as u64),
edit_offset: edit_offset as u32,
user_edit_span: (base + edit_offset, base + s.len() as u64),
}))
}
fn payload<'a>(stream: &'a [u8], r: &PptRecord) -> &'a [u8] {
let s = r.stream_off as usize + 8;
let e = s + r.len as usize;
stream.get(s..e).unwrap_or(&[])
}
fn cp1252(b: u8) -> u16 {
const HIGH: [u16; 32] = [
0x20AC, 0xFFFD, 0x201A, 0x0192, 0x201E, 0x2026, 0x2020, 0x2021, 0x02C6, 0x2030, 0x0160, 0x2039, 0x0152, 0xFFFD, 0x017D, 0xFFFD, 0xFFFD, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0x02DC, 0x2122, 0x0161, 0x203A, 0x0153, 0xFFFD, 0x017E, 0x0178, ];
match b {
0x80..=0x9F => HIGH[(b - 0x80) as usize],
_ => b as u16,
}
}
fn find_named<'a>(
model: &'a crate::adapter::cfbf::CfbfModel,
name: &str,
) -> Option<&'a crate::adapter::cfbf::CfbfEntry> {
model.entries.iter().find(|e| {
e.obj_type == crate::adapter::cfbf::OBJ_STREAM
&& crate::adapter::cfbf::entry_name_text(e) == name
})
}
fn invalid(message: impl Into<String>) -> Error {
Error::invalid_ppt_structure(format!("ppt: {}", message.into()))
}
fn corrupt(message: impl Into<String>) -> Error {
Error::invalid_ppt_structure(format!("ppt model: {}", message.into()))
}
fn w_span(out: &mut Vec<u8>, s: (u64, u64)) {
out.extend_from_slice(&s.0.to_le_bytes());
out.extend_from_slice(&s.1.to_le_bytes());
}
fn w_units(out: &mut Vec<u8>, units: &[u16]) {
out.extend_from_slice(&(units.len() as u32).to_le_bytes());
for u in units {
out.extend_from_slice(&u.to_le_bytes());
}
}
struct BinReader<'a> {
b: &'a [u8],
at: usize,
}
impl<'a> BinReader<'a> {
fn new(b: &'a [u8]) -> Self {
BinReader { b, at: 0 }
}
fn bytes(&mut self, n: usize) -> Result<&'a [u8]> {
let end = self
.at
.checked_add(n)
.ok_or_else(|| corrupt("length overflow"))?;
let out = self
.b
.get(self.at..end)
.ok_or_else(|| corrupt("unexpected end of model"))?;
self.at = end;
Ok(out)
}
fn u8(&mut self) -> Result<u8> {
Ok(self.bytes(1)?[0])
}
fn u16(&mut self) -> Result<u16> {
let b = self.bytes(2)?;
Ok(u16::from_le_bytes([b[0], b[1]]))
}
fn u32(&mut self) -> Result<u32> {
let b = self.bytes(4)?;
Ok(u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
}
fn u64(&mut self) -> Result<u64> {
let b = self.bytes(8)?;
Ok(u64::from_le_bytes([
b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7],
]))
}
fn span(&mut self) -> Result<(u64, u64)> {
Ok((self.u64()?, self.u64()?))
}
fn units(&mut self) -> Result<Vec<u16>> {
let n = self.u32()?;
if n as u64 > MAX_MODEL_TEXT_UNITS as u64 {
return Err(corrupt("text length is implausible"));
}
let bytes = self.bytes(n as usize * 2)?;
Ok(bytes
.as_chunks::<2>()
.0
.iter()
.map(|c| u16::from_le_bytes(*c))
.collect())
}
fn at_end(&self) -> bool {
self.at == self.b.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn type_and_kind_names_are_stable() {
assert_eq!(record_type_name(REC_DOCUMENT), "document");
assert_eq!(
record_type_name(REC_SLIDE_LIST_WITH_TEXT),
"slide-list-with-text"
);
assert_eq!(record_type_name(REC_TEXT_CHARS_ATOM), "text-chars-atom");
assert_eq!(record_type_name(REC_TEXT_BYTES_ATOM), "text-bytes-atom");
assert_eq!(run_kind_name(RUN_CHARS), "chars");
assert_eq!(run_kind_name(RUN_BYTES), "bytes");
assert_eq!(slide_list_name(SLIDE_LIST_SLIDES), "slides");
assert_eq!(slide_list_name(SLIDE_LIST_MASTER), "master");
assert_eq!(slide_list_name(7), "other");
}
#[test]
fn cp1252_maps_the_high_range() {
assert_eq!(cp1252(0x41), 0x0041);
assert_eq!(cp1252(0x80), 0x20AC); assert_eq!(cp1252(0x99), 0x2122); assert_eq!(cp1252(0x81), 0xFFFD); }
#[test]
fn prose_is_not_a_ppt() {
assert!(!detect(
b"The quick brown fox jumps over the lazy dog.\nPlain prose.\n",
Limits::DEFAULT
));
}
#[test]
fn random_bytes_never_panic() {
let mut seed: u64 = 0x2620_0003_5050_5456;
for _ in 0..1024 {
let mut buf = [0u8; 1024];
for b in buf.iter_mut() {
seed = seed
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
*b = (seed >> 33) as u8;
}
let _ = detect(&buf, Limits::DEFAULT);
let _ = parse(&buf, Limits::STRICT);
}
let mut buf = vec![0u8; 4096];
buf[..8].copy_from_slice(&crate::adapter::cfbf::SIGNATURE);
for _ in 0..256 {
for b in buf[8..].iter_mut() {
seed = seed
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
*b = (seed >> 33) as u8;
}
let _ = detect(&buf, Limits::STRICT);
let _ = parse(&buf, Limits::STRICT);
}
}
}