mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-13 22:12:12 +00:00
Add craft-pst: Outlook .pst reader from MailCraft (#930)
The [MS-PST] reader of MailCraft crates/pst at a44a450, moved as it is: the node database, heap, property and table contexts, folders, messages, recipients, attachments, named properties, recurrence blobs and list streams, with its limits, salvage and tests. Left in MailCraft for now: the compressed-RTF and RTF text reading and the code-page name table, which are not part of the .pst format. Co-authored-by: Martin Ball <98639962+benandez@users.noreply.github.com>
This commit is contained in:
1 parent
e43e8663ae
commit
dbed0a83ea
34 files changed
+7294
No files matched your search
Generated
+7
@@ -1362,6 +1362,13 @@ dependencies = [
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "craft-pst"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"proptest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "craft-quit"
|
||||
version = "0.1.0"
|
||||
|
||||
@@ -75,6 +75,7 @@ craft-print = { path = "crates/print" }
|
||||
craft-proof = { path = "crates/proof" }
|
||||
craft-raster-codecs = { path = "crates/raster-codecs" }
|
||||
craft-psd = { path = "crates/psd" }
|
||||
craft-pst = { path = "crates/pst" }
|
||||
craft-quit = { path = "crates/quit" }
|
||||
craft-rtf = { path = "crates/rtf" }
|
||||
craft-raw = { path = "crates/raw" }
|
||||
|
||||
@@ -215,6 +215,12 @@ craft-<name> = { git = "https://github.com/storytold/craft-libs", rev = "<commit
|
||||
carry-over locales by default, the set `CRAFT_PROOF_EMBED` names otherwise (`en` for English
|
||||
only); other languages come from installed Language packs.
|
||||
|
||||
- [`craft-pst`](crates/pst/README.md): Outlook personal folders files (`.pst`, [MS-PST]) read over any
|
||||
`Read + Seek`, from MailCraft: the node database, property and table contexts, folders, messages,
|
||||
recipients, attachments, attached messages and named properties, ANSI and Unicode, encoded or not,
|
||||
with a password. Salvages damaged files. Bounded and panic-free on hostile input. What an item
|
||||
means stays in each app.
|
||||
|
||||
- [`craft-quit`](crates/quit/src/lib.rs): native macOS quit, egui close handling and an optional
|
||||
app menu. Unsaved-document decisions and save prompts stay in each app.
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "craft-pst"
|
||||
description = "Outlook personal folders files (.pst, [MS-PST]): a reader over any Read + Seek of the node database, property and table contexts, folders, messages, recipients, attachments and named properties."
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[features]
|
||||
# The test writer (`test_writer`): synthetic .pst files for the tests of code that reads them.
|
||||
# A dev-dependency feature only.
|
||||
test-writer = []
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
|
||||
[dev-dependencies]
|
||||
proptest = { workspace = true }
|
||||
@@ -0,0 +1,70 @@
|
||||
# craft-pst
|
||||
|
||||
A reader of Outlook personal folders files (`.pst`), written from Microsoft's published
|
||||
specification, [MS-PST], and nothing else. No dependencies, no `unsafe`.
|
||||
|
||||
- `Pst::open(reader)` / `Pst::open_with(reader, Limits)` read over any `Read + Seek`. The file is
|
||||
never held in memory: a page or a block is read when it is needed.
|
||||
- ANSI and Unicode files, with data blocks unencoded, permuted or cyclic-encoded. A file with a
|
||||
password answers only after `Pst::unlock`.
|
||||
- The layers are the specification's: the node database (B-trees of nodes and blocks, data
|
||||
trees, subnode trees), the heap on a node, its B-tree, property contexts and table contexts,
|
||||
and the messaging layer: `store`, `walk_folders`, `items`, `associated_items`, `message`
|
||||
(properties, recipients, attachments, attached messages), `attachment_reader`,
|
||||
`read_property` for large values, and `named_properties`.
|
||||
- `recur` reads the appointment recurrence pattern and time zone blobs of [MS-OXOCAL];
|
||||
`list_stream` reads the entry stream of a distribution list.
|
||||
- A damaged file is salvaged: what cannot be read is left out and named (`Pst::damage`,
|
||||
`Message::left_out`).
|
||||
|
||||
```rust
|
||||
use craft_pst::Pst;
|
||||
|
||||
let mut pst = Pst::open(std::fs::File::open(path)?)?;
|
||||
for entry in pst.walk_folders()?.folders {
|
||||
for item in pst.items(entry.folder.id)?.items {
|
||||
let message = pst.message(&craft_pst::MessageRef::item(item.id))?;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Hostile input
|
||||
|
||||
Nothing panics on any bytes. Every offset and length is checked against the file, every CRC,
|
||||
signature and block id against what it guards; trees are bounded in depth and read each part
|
||||
once. The caps of `Limits` are checked before anything is read or allocated; past a cap a part
|
||||
is left out and named (`Error::Cap`).
|
||||
|
||||
| Limit | Default |
|
||||
|---|---|
|
||||
| `max_file_size` | 100 GiB |
|
||||
| `max_items` | 2,000,000 |
|
||||
| `max_folders` / `max_folder_depth` | 10,000 / 64 |
|
||||
| `max_item_bytes` | 256 MiB |
|
||||
| `max_attachment_bytes` | 150 MiB |
|
||||
| `max_recipients` / `max_attachments` | 1,000 / 1,000 |
|
||||
| `max_embedded_depth` / `max_embedded_messages` | 8 / 10,000 |
|
||||
| `max_btree_pages` | 4,000,000 |
|
||||
| `max_subnodes` | 200,000 |
|
||||
| `max_inline_value` | 4 MiB (larger values are `PropValue::Large`, read on their own) |
|
||||
| `max_listing_bytes` | 1 GiB per folder walk or item listing |
|
||||
| `max_summary_value` | 64 KiB |
|
||||
| `read_multiple` / `read_floor` | all reads together: 64 times the file size plus 256 MiB |
|
||||
|
||||
## What is not here
|
||||
|
||||
- Writing. The `test-writer` feature has a writer of small synthetic files, for tests only.
|
||||
- Offline folders files with 4 KB pages (version 36) and files protected by Windows Information
|
||||
Protection (version 37): `Error::UnsupportedVersion`.
|
||||
- Text in a code page: `PropValue::String8` carries the bytes, and the caller decodes them in
|
||||
the code page the message names.
|
||||
- Compressed RTF bodies (`PidTagRtfCompressed`, [MS-OXRTFCP]): the property's bytes are
|
||||
returned as they are.
|
||||
- What an item means (mail, contact, appointment): that mapping is the caller's.
|
||||
|
||||
## Origin
|
||||
|
||||
MailCraft `crates/pst` at
|
||||
[a44a450](https://github.com/UCS-Care-Group/mailcraft/commit/a44a45013d1c6396229c40ab60172d1882b368d5),
|
||||
by Martin Ball and the MailCraft contributors (MIT OR Apache-2.0), with its tests. Left behind:
|
||||
the RTF reading (`rtf.rs`) and the code-page name table, which are not part of the .pst format.
|
||||
@@ -0,0 +1,10 @@
|
||||
# Seeds for failure cases proptest has generated in the past. It is
|
||||
# automatically read and these particular cases re-run before any
|
||||
# novel cases are generated.
|
||||
#
|
||||
# It is recommended to check this file in to source control so that
|
||||
# everyone who runs the test benefits from these saved cases.
|
||||
cc 8293943e3b757e40e0fe3af376fa196920a1ed194392c20aa2aca37408fb4cbb # shrinks to which = 0, cut = 0
|
||||
cc 9c84f9ada31defc2f9a4fe42d97bccf19053ee5e5dc431a235fabbc49b77f959 # shrinks to which = 0, changes = [(0, 0)]
|
||||
cc 7d8fc4ba0f55c701c63582ad7ca2f1ad74aecbb6284caf9a59d5ad36982ad1d3 # shrinks to which = 0, changes = [(0, 0)]
|
||||
cc c641f6f7aa79ee9da2d0d73b8b6faad35ebd2f16d89fd1846bc9f14878411da6 # shrinks to tail = [0, 94, 66, 177, 32, 232, 54, 103, 58, 14, 68, 179, 59, 223, 244, 19, 222, 225, 10, 74, 31, 155, 228, 4, 143, 112, 235, 183, 227, 192, 4, 164, 16, 69, 156, 149, 182, 161, 226, 179, 247, 148, 232, 236, 75, 53, 209, 12, 7, 86, 236, 176, 216, 5, 125, 167, 109, 136, 99, 162, 17, 112], which = 2
|
||||
@@ -0,0 +1,86 @@
|
||||
//! The B-tree-on-heap ([MS-PST] 2.3.2). The tree is at most [`MAX_LEVELS`] index levels deep,
|
||||
//! every heap item of it is read at most once, the heap's items share no bytes
|
||||
//! ([`Heap::new`] checks its page maps) and the items read are charged against the heap's size,
|
||||
//! so a hostile tree reads no more than its heap holds; and it gives at most `max_records`
|
||||
//! records, refused past them while they are gathered.
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::bytes::{slice, u8_at, u32_at};
|
||||
use crate::error::{Error, Result};
|
||||
use crate::heap::{CLIENT_BTH, Heap};
|
||||
|
||||
pub(crate) const MAX_LEVELS: u8 = 8;
|
||||
|
||||
pub(crate) struct Bth<'h> {
|
||||
pub key_size: usize,
|
||||
pub data_size: usize,
|
||||
/// Leaf records, `key_size + data_size` bytes each, in the order of the tree.
|
||||
pub records: Vec<&'h [u8]>,
|
||||
}
|
||||
|
||||
pub(crate) fn read(heap: &Heap, header_hid: u32, max_records: usize, cap: Error) -> Result<Bth<'_>> {
|
||||
let h = heap.item(header_hid)?;
|
||||
if u8_at(h, 0)? != CLIENT_BTH {
|
||||
return Err(Error::Corrupt("a B-tree-on-heap's signature"));
|
||||
}
|
||||
let key_size = usize::from(u8_at(h, 1)?);
|
||||
let data_size = usize::from(u8_at(h, 2)?);
|
||||
let levels = u8_at(h, 3)?;
|
||||
let root = u32_at(h, 4)?;
|
||||
if !matches!(key_size, 2 | 4 | 8 | 16) || data_size == 0 || data_size > 32 {
|
||||
return Err(Error::Corrupt("a B-tree-on-heap's record sizes"));
|
||||
}
|
||||
if levels > MAX_LEVELS {
|
||||
return Err(Error::Corrupt("a B-tree-on-heap too deep"));
|
||||
}
|
||||
let mut records = Vec::new();
|
||||
if root == 0 {
|
||||
return Ok(Bth { key_size, data_size, records });
|
||||
}
|
||||
let mut seen = HashSet::new();
|
||||
let mut left = heap.size();
|
||||
let mut stack = vec![(root, levels)];
|
||||
while let Some((hid, level)) = stack.pop() {
|
||||
if !seen.insert(hid) {
|
||||
return Err(Error::Cycle("a B-tree-on-heap"));
|
||||
}
|
||||
let item = heap.item(hid)?;
|
||||
left = left.checked_sub(item.len()).ok_or(Error::Corrupt("a B-tree-on-heap larger than its heap"))?;
|
||||
if level == 0 {
|
||||
let size = key_size.saturating_add(data_size);
|
||||
if !item.len().is_multiple_of(size) {
|
||||
return Err(Error::Corrupt("a B-tree-on-heap's records"));
|
||||
}
|
||||
if item.len().checked_div(size).unwrap_or(0) > max_records.saturating_sub(records.len()) {
|
||||
return Err(cap);
|
||||
}
|
||||
// A BTH's keys are unique and in order: each must be greater than the one before
|
||||
// (as a little-endian number), so no record is named twice.
|
||||
for r in item.chunks_exact(size) {
|
||||
let key = r.get(..key_size).unwrap_or(&[]);
|
||||
if let Some(prev) = records.last().and_then(|p: &&[u8]| p.get(..key_size))
|
||||
&& key.iter().rev().cmp(prev.iter().rev()) != std::cmp::Ordering::Greater
|
||||
{
|
||||
return Err(Error::Corrupt("a B-tree-on-heap's keys out of order"));
|
||||
}
|
||||
records.push(r);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let size = key_size.saturating_add(4);
|
||||
if !item.len().is_multiple_of(size) {
|
||||
return Err(Error::Corrupt("a B-tree-on-heap's index"));
|
||||
}
|
||||
let below = level.saturating_sub(1);
|
||||
let n = item.len().checked_div(size).unwrap_or(0);
|
||||
let mut children = Vec::with_capacity(n);
|
||||
for i in 0..n {
|
||||
let r = slice(item, i.saturating_mul(size), size)?;
|
||||
children.push((u32_at(r, key_size)?, below));
|
||||
}
|
||||
// Last-first, so that the leaves come out in key order.
|
||||
stack.extend(children.into_iter().rev());
|
||||
}
|
||||
Ok(Bth { key_size, data_size, records })
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
//! Little-endian reads out of a byte slice that fail instead of panicking.
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
|
||||
const SHORT: Error = Error::Corrupt("a structure runs past its end");
|
||||
|
||||
pub(crate) fn slice(b: &[u8], at: usize, len: usize) -> Result<&[u8]> {
|
||||
let end = at.checked_add(len).ok_or(SHORT)?;
|
||||
b.get(at..end).ok_or(SHORT)
|
||||
}
|
||||
|
||||
pub(crate) fn u8_at(b: &[u8], at: usize) -> Result<u8> {
|
||||
b.get(at).copied().ok_or(SHORT)
|
||||
}
|
||||
|
||||
pub(crate) fn u16_at(b: &[u8], at: usize) -> Result<u16> {
|
||||
let s = slice(b, at, 2)?;
|
||||
Ok(u16::from_le_bytes([s.first().copied().unwrap_or(0), s.get(1).copied().unwrap_or(0)]))
|
||||
}
|
||||
|
||||
pub(crate) fn u32_at(b: &[u8], at: usize) -> Result<u32> {
|
||||
let s = slice(b, at, 4)?;
|
||||
let mut a = [0u8; 4];
|
||||
a.copy_from_slice(s);
|
||||
Ok(u32::from_le_bytes(a))
|
||||
}
|
||||
|
||||
pub(crate) fn u64_at(b: &[u8], at: usize) -> Result<u64> {
|
||||
let s = slice(b, at, 8)?;
|
||||
let mut a = [0u8; 8];
|
||||
a.copy_from_slice(s);
|
||||
Ok(u64::from_le_bytes(a))
|
||||
}
|
||||
|
||||
pub(crate) fn array16(b: &[u8], at: usize) -> Result<[u8; 16]> {
|
||||
let s = slice(b, at, 16)?;
|
||||
let mut a = [0u8; 16];
|
||||
a.copy_from_slice(s);
|
||||
Ok(a)
|
||||
}
|
||||
|
||||
/// UTF-16LE text, lossy; an odd last byte and trailing NULs are dropped.
|
||||
pub(crate) fn utf16(b: &[u8]) -> String {
|
||||
let units: Vec<u16> = b.as_chunks::<2>().0.iter().map(|c| u16::from_le_bytes(*c)).collect();
|
||||
let mut s = String::from_utf16_lossy(&units);
|
||||
while s.ends_with('\0') {
|
||||
s.pop();
|
||||
}
|
||||
s
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
//! The CRC of [MS-PST] 5.3: the reflected CRC-32 polynomial (its table starts `0x00000000,
|
||||
//! 0x77073096, 0xEE0E612C`), seeded with zero and with no final inversion. It guards the header,
|
||||
//! every page and every block, and is what the file keeps of its password.
|
||||
|
||||
const TABLE: [u32; 256] = table();
|
||||
|
||||
#[allow(clippy::indexing_slicing, clippy::arithmetic_side_effects)] // a constant, every index below 256
|
||||
const fn table() -> [u32; 256] {
|
||||
let mut t = [0u32; 256];
|
||||
let mut i = 0;
|
||||
while i < 256 {
|
||||
let mut c = i as u32;
|
||||
let mut k = 0;
|
||||
while k < 8 {
|
||||
c = if c & 1 != 0 { 0xEDB8_8320 ^ (c >> 1) } else { c >> 1 };
|
||||
k += 1;
|
||||
}
|
||||
t[i] = c;
|
||||
i += 1;
|
||||
}
|
||||
t
|
||||
}
|
||||
|
||||
/// The CRC of `data`.
|
||||
pub fn crc32(data: &[u8]) -> u32 {
|
||||
let mut crc = 0u32;
|
||||
for &b in data {
|
||||
let at = ((crc ^ u32::from(b)) & 0xFF) as usize;
|
||||
crc = TABLE.get(at).copied().unwrap_or(0) ^ (crc >> 8);
|
||||
}
|
||||
crc
|
||||
}
|
||||
|
||||
/// The first entries of the table as [MS-PST] 5.3 prints them (a test checks the rest of the
|
||||
/// algorithm against the spec's sample header).
|
||||
#[cfg(test)]
|
||||
pub(crate) fn table_head() -> [u32; 4] {
|
||||
[TABLE[0], TABLE[1], TABLE[2], TABLE[255]]
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
//! The two keyless encodings of data blocks, [MS-PST] 5.1 (`NDB_CRYPT_PERMUTE`) and 5.2
|
||||
//! (`NDB_CRYPT_CYCLIC`). They hide nothing: they keep text from being read straight off the disk.
|
||||
//! The three tables are the specification's (`mpbbCrypt`, in three parts of 256); `I` undoes `R`.
|
||||
|
||||
/// How a file's data blocks are encoded (`bCryptMethod`).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Encryption {
|
||||
None,
|
||||
Permute,
|
||||
Cyclic,
|
||||
}
|
||||
|
||||
/// Decodes `data` in place, as it was stored in the block with id `bid`.
|
||||
pub(crate) fn decode(method: Encryption, data: &mut [u8], bid: u64) {
|
||||
match method {
|
||||
Encryption::None => {}
|
||||
Encryption::Permute => {
|
||||
for b in data.iter_mut() {
|
||||
*b = at(&I, *b);
|
||||
}
|
||||
}
|
||||
Encryption::Cyclic => cyclic(data, bid as u32),
|
||||
}
|
||||
}
|
||||
|
||||
/// Encodes `data` in place (the test writer's side).
|
||||
#[cfg(any(test, feature = "test-writer"))]
|
||||
pub(crate) fn encode(method: Encryption, data: &mut [u8], bid: u64) {
|
||||
match method {
|
||||
Encryption::None => {}
|
||||
Encryption::Permute => {
|
||||
for b in data.iter_mut() {
|
||||
*b = at(&R, *b);
|
||||
}
|
||||
}
|
||||
Encryption::Cyclic => cyclic(data, bid as u32),
|
||||
}
|
||||
}
|
||||
|
||||
/// [MS-PST] 5.2: symmetric, keyed by the low 32 bits of the block id.
|
||||
fn cyclic(data: &mut [u8], key: u32) {
|
||||
let mut w = (key ^ (key >> 16)) as u16;
|
||||
for b in data.iter_mut() {
|
||||
let lo = w as u8;
|
||||
let hi = (w >> 8) as u8;
|
||||
let mut x = b.wrapping_add(lo);
|
||||
x = at(&R, x);
|
||||
x = x.wrapping_add(hi);
|
||||
x = at(&S, x);
|
||||
x = x.wrapping_sub(hi);
|
||||
x = at(&I, x);
|
||||
x = x.wrapping_sub(lo);
|
||||
*b = x;
|
||||
w = w.wrapping_add(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn at(table: &[u8; 256], b: u8) -> u8 {
|
||||
table.get(usize::from(b)).copied().unwrap_or(b)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn tables() -> (&'static [u8; 256], &'static [u8; 256], &'static [u8; 256]) {
|
||||
(&R, &S, &I)
|
||||
}
|
||||
|
||||
const R: [u8; 256] = [
|
||||
65, 54, 19, 98, 168, 33, 110, 187, 244, 22, 204, 4, 127, 100, 232, 93, 30, 242, 203, 42, 116, 197, 94, 53, 210, 149, 71, 158, 150, 45, 154, 136,
|
||||
76, 125, 132, 63, 219, 172, 49, 182, 72, 95, 246, 196, 216, 57, 139, 231, 35, 59, 56, 142, 200, 193, 223, 37, 177, 32, 165, 70, 96, 78, 156, 251,
|
||||
170, 211, 86, 81, 69, 124, 85, 0, 7, 201, 43, 157, 133, 155, 9, 160, 143, 173, 179, 15, 99, 171, 137, 75, 215, 167, 21, 90, 113, 102, 66, 191,
|
||||
38, 74, 107, 152, 250, 234, 119, 83, 178, 112, 5, 44, 253, 89, 58, 134, 126, 206, 6, 235, 130, 120, 87, 199, 141, 67, 175, 180, 28, 212, 91, 205,
|
||||
226, 233, 39, 79, 195, 8, 114, 128, 207, 176, 239, 245, 40, 109, 190, 48, 77, 52, 146, 213, 14, 60, 34, 50, 229, 228, 249, 159, 194, 209, 10,
|
||||
129, 18, 225, 238, 145, 131, 118, 227, 151, 230, 97, 138, 23, 121, 164, 183, 220, 144, 122, 92, 140, 2, 166, 202, 105, 222, 80, 26, 17, 147, 185,
|
||||
82, 135, 88, 252, 237, 29, 55, 73, 27, 106, 224, 41, 51, 153, 189, 108, 217, 148, 243, 64, 84, 111, 240, 198, 115, 184, 214, 62, 101, 24, 68, 31,
|
||||
221, 103, 16, 241, 12, 25, 236, 174, 3, 161, 20, 123, 169, 11, 255, 248, 163, 192, 162, 1, 247, 46, 188, 36, 104, 117, 13, 254, 186, 47, 181,
|
||||
208, 218, 61,
|
||||
];
|
||||
const S: [u8; 256] = [
|
||||
20, 83, 15, 86, 179, 200, 122, 156, 235, 101, 72, 23, 22, 21, 159, 2, 204, 84, 124, 131, 0, 13, 12, 11, 162, 98, 168, 118, 219, 217, 237, 199,
|
||||
197, 164, 220, 172, 133, 116, 214, 208, 167, 155, 174, 154, 150, 113, 102, 195, 99, 153, 184, 221, 115, 146, 142, 132, 125, 165, 94, 209, 93,
|
||||
147, 177, 87, 81, 80, 128, 137, 82, 148, 79, 78, 10, 107, 188, 141, 127, 110, 71, 70, 65, 64, 68, 1, 17, 203, 3, 63, 247, 244, 225, 169, 143, 60,
|
||||
58, 249, 251, 240, 25, 48, 130, 9, 46, 201, 157, 160, 134, 73, 238, 111, 77, 109, 196, 45, 129, 52, 37, 135, 27, 136, 170, 252, 6, 161, 18, 56,
|
||||
253, 76, 66, 114, 100, 19, 55, 36, 106, 117, 119, 67, 255, 230, 180, 75, 54, 92, 228, 216, 53, 61, 69, 185, 44, 236, 183, 49, 43, 41, 7, 104,
|
||||
163, 14, 105, 123, 24, 158, 33, 57, 190, 40, 26, 91, 120, 245, 35, 202, 42, 176, 175, 62, 254, 4, 140, 231, 229, 152, 50, 149, 211, 246, 74, 232,
|
||||
166, 234, 233, 243, 213, 47, 112, 32, 242, 31, 5, 103, 173, 85, 16, 206, 205, 227, 39, 59, 218, 186, 215, 194, 38, 212, 145, 29, 210, 28, 34, 51,
|
||||
248, 250, 241, 90, 239, 207, 144, 182, 139, 181, 189, 192, 191, 8, 151, 30, 108, 226, 97, 224, 198, 193, 89, 171, 187, 88, 222, 95, 223, 96, 121,
|
||||
126, 178, 138,
|
||||
];
|
||||
const I: [u8; 256] = [
|
||||
71, 241, 180, 230, 11, 106, 114, 72, 133, 78, 158, 235, 226, 248, 148, 83, 224, 187, 160, 2, 232, 90, 9, 171, 219, 227, 186, 198, 124, 195, 16,
|
||||
221, 57, 5, 150, 48, 245, 55, 96, 130, 140, 201, 19, 74, 107, 29, 243, 251, 143, 38, 151, 202, 145, 23, 1, 196, 50, 45, 110, 49, 149, 255, 217,
|
||||
35, 209, 0, 94, 121, 220, 68, 59, 26, 40, 197, 97, 87, 32, 144, 61, 131, 185, 67, 190, 103, 210, 70, 66, 118, 192, 109, 91, 126, 178, 15, 22, 41,
|
||||
60, 169, 3, 84, 13, 218, 93, 223, 246, 183, 199, 98, 205, 141, 6, 211, 105, 92, 134, 214, 20, 247, 165, 102, 117, 172, 177, 233, 69, 33, 112, 12,
|
||||
135, 159, 116, 164, 34, 76, 111, 191, 31, 86, 170, 46, 179, 120, 51, 80, 176, 163, 146, 188, 207, 25, 28, 167, 99, 203, 30, 77, 62, 75, 27, 155,
|
||||
79, 231, 240, 238, 173, 58, 181, 89, 4, 234, 64, 85, 37, 81, 229, 122, 137, 56, 104, 82, 123, 252, 39, 174, 215, 189, 250, 7, 244, 204, 142, 95,
|
||||
239, 53, 156, 132, 43, 21, 213, 119, 52, 73, 182, 18, 10, 127, 113, 136, 253, 157, 24, 65, 125, 147, 216, 88, 44, 206, 254, 36, 175, 222, 184,
|
||||
54, 200, 161, 128, 166, 153, 152, 168, 47, 14, 129, 101, 115, 228, 194, 162, 138, 212, 225, 17, 208, 8, 139, 42, 242, 237, 154, 100, 63, 193,
|
||||
108, 249, 236,
|
||||
];
|
||||
@@ -0,0 +1,206 @@
|
||||
//! What can go wrong reading a .pst file, and the caps that keep a hostile one bounded.
|
||||
|
||||
use std::fmt;
|
||||
|
||||
/// A failure to read (part of) a .pst file. Nothing in this crate panics: everything a file can
|
||||
/// get wrong ends here.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
pub enum Error {
|
||||
/// The reader under the file failed.
|
||||
Io(std::io::ErrorKind),
|
||||
/// Not a .pst file (its first bytes are not `!BDN` … `SM`).
|
||||
NotPst,
|
||||
/// A .pst file of a version this reader doesn't know (`wVer`).
|
||||
UnsupportedVersion(u16),
|
||||
/// Data encoded in a way this reader can't undo (`bCryptMethod`, for example Windows
|
||||
/// Information Protection).
|
||||
UnsupportedEncryption(u8),
|
||||
/// A checksum, signature or block id that doesn't match what it guards.
|
||||
BadChecksum(&'static str),
|
||||
/// A structure that makes no sense.
|
||||
Corrupt(&'static str),
|
||||
/// An offset or length that points past the end of the file.
|
||||
OutOfBounds,
|
||||
/// A tree that leads back to where it was (or reads the same part twice).
|
||||
Cycle(&'static str),
|
||||
/// Something the file should have and hasn't.
|
||||
NotFound(&'static str),
|
||||
/// Past one of the caps in [`Limits`]: left out, not read.
|
||||
Cap(Cap),
|
||||
/// The file has a password and it has not been given ([`crate::Pst::unlock`]).
|
||||
PasswordRequired,
|
||||
/// The password given is not the file's.
|
||||
WrongPassword,
|
||||
/// The operation does not apply to this object (a search folder's items, for example).
|
||||
Unsupported(&'static str),
|
||||
}
|
||||
|
||||
/// Which cap was reached.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[non_exhaustive]
|
||||
pub enum Cap {
|
||||
FileSize,
|
||||
Items,
|
||||
Folders,
|
||||
FolderDepth,
|
||||
ItemSize,
|
||||
AttachmentSize,
|
||||
Recipients,
|
||||
Attachments,
|
||||
EmbeddedDepth,
|
||||
BTreePages,
|
||||
Subnodes,
|
||||
DataBlocks,
|
||||
NamedProperties,
|
||||
/// [`Limits::max_listing_bytes`]: what one folder walk or one listing may read.
|
||||
ListingSize,
|
||||
/// [`Limits::max_embedded_messages`]: attached messages read under one item.
|
||||
EmbeddedMessages,
|
||||
/// [`Limits::read_multiple`]: more read from the file, in all, than its size allows for.
|
||||
TotalRead,
|
||||
}
|
||||
|
||||
impl fmt::Display for Cap {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let what = match self {
|
||||
Cap::FileSize => "the file is larger than the cap",
|
||||
Cap::Items => "more items than the cap",
|
||||
Cap::Folders => "more folders than the cap",
|
||||
Cap::FolderDepth => "folders nested deeper than the cap",
|
||||
Cap::ItemSize => "an item larger than the cap",
|
||||
Cap::AttachmentSize => "an attachment larger than the cap",
|
||||
Cap::Recipients => "more recipients than the cap",
|
||||
Cap::Attachments => "more attachments than the cap",
|
||||
Cap::EmbeddedDepth => "messages attached deeper than the cap",
|
||||
Cap::BTreePages => "a tree with more pages than the cap",
|
||||
Cap::Subnodes => "an item with more parts than the cap",
|
||||
Cap::DataBlocks => "data spread over more blocks than its size needs",
|
||||
Cap::NamedProperties => "a named property's name, or all of them, longer than the cap",
|
||||
Cap::ListingSize => "a folder list or a list of items larger than the cap",
|
||||
Cap::EmbeddedMessages => "more attached messages under one item than the cap",
|
||||
Cap::TotalRead => "more read from the file than its size allows for",
|
||||
};
|
||||
f.write_str(what)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Error::Io(kind) => write!(f, "the file could not be read ({kind})"),
|
||||
Error::NotPst => f.write_str("not a .pst file"),
|
||||
Error::UnsupportedVersion(36) => f.write_str("an offline folders file (.ost) with 4 KB pages, not supported (version 36)"),
|
||||
Error::UnsupportedVersion(37) => f.write_str("a file protected by Windows Information Protection, not supported (version 37)"),
|
||||
Error::UnsupportedVersion(v) => write!(f, "a .pst file of a version not supported ({v})"),
|
||||
Error::UnsupportedEncryption(m) => write!(f, "the file's data is encoded in a way not supported ({m:#04x})"),
|
||||
Error::BadChecksum(what) => write!(f, "damaged file: {what} doesn't match its checksum"),
|
||||
Error::Corrupt(what) => write!(f, "damaged file: {what}"),
|
||||
Error::OutOfBounds => f.write_str("damaged file: a part points past its end"),
|
||||
Error::Cycle(what) => write!(f, "damaged file: {what} leads back to itself"),
|
||||
Error::NotFound(what) => write!(f, "damaged file: {what} is missing"),
|
||||
Error::Cap(cap) => write!(f, "left out: {cap}"),
|
||||
Error::PasswordRequired => f.write_str("the file has a password"),
|
||||
Error::WrongPassword => f.write_str("the password is not the file's"),
|
||||
Error::Unsupported(what) => write!(f, "not supported: {what}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {}
|
||||
|
||||
impl From<std::io::Error> for Error {
|
||||
fn from(e: std::io::Error) -> Self {
|
||||
match e.kind() {
|
||||
std::io::ErrorKind::UnexpectedEof => Error::OutOfBounds,
|
||||
kind => Error::Io(kind),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// A part of the file's own structure that could not be read: a page of the node tree (the
|
||||
/// folders and items under it can't be found) or of the block tree (the data of the nodes that
|
||||
/// use its blocks can't be found). The page's own count of what it held can't be trusted, so
|
||||
/// what was lost with it is not known; each folder or item that needed it is named in the
|
||||
/// `left_out` of the walk, list or message that met it.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Damage {
|
||||
/// Where the page is in the file.
|
||||
pub offset: u64,
|
||||
/// "a page of the node tree" or "a page of the block tree".
|
||||
pub what: &'static str,
|
||||
pub reason: Error,
|
||||
/// 1; or, for the last entry once [`MAX_DAMAGE`] pages have been named, how many pages
|
||||
/// from it on could not be read (at least: past 65,536 they are no longer counted).
|
||||
pub pages: u64,
|
||||
}
|
||||
|
||||
/// Damaged pages named one by one; past them they are counted in one entry.
|
||||
pub const MAX_DAMAGE: usize = 1000;
|
||||
|
||||
/// The caps, checked before anything is read or allocated. [`Limits::default`] is the import's
|
||||
/// (the file at most 100 GB, 2,000,000 items, 10,000 folders nested at most 64 deep, 256 MB an
|
||||
/// item, 150 MB an attachment, 1,000 recipients and 1,000 attachments an item, attached
|
||||
/// messages at most 8 deep, 10,000 of them under one item; a folder walk or a listing at most
|
||||
/// 1 GB; at most 64 times the file's size plus 256 MB read in all); lower ones serve tests.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Limits {
|
||||
pub max_file_size: u64,
|
||||
/// Items listed across the file's folders ([`crate::Pst::items`]).
|
||||
pub max_items: usize,
|
||||
pub max_folders: usize,
|
||||
pub max_folder_depth: usize,
|
||||
/// One node's data (a message's properties, a table), and all of an item's property values
|
||||
/// read at once.
|
||||
pub max_item_bytes: u64,
|
||||
pub max_attachment_bytes: u64,
|
||||
pub max_recipients: usize,
|
||||
pub max_attachments: usize,
|
||||
pub max_embedded_depth: usize,
|
||||
/// Pages a walk of the whole node tree may visit.
|
||||
pub max_btree_pages: usize,
|
||||
/// Entries in one node's subnode tree.
|
||||
pub max_subnodes: usize,
|
||||
/// A property value larger than this is not read with the rest ([`crate::PropValue::Large`])
|
||||
/// and is read on its own ([`crate::Pst::read_property`]).
|
||||
pub max_inline_value: u64,
|
||||
/// All that one [`crate::Pst::walk_folders`] or one listing of a folder's items may read
|
||||
/// and keep (tables and values together).
|
||||
pub max_listing_bytes: u64,
|
||||
/// A value larger than this in a folder walk or an item list is [`crate::PropValue::Large`].
|
||||
pub max_summary_value: u64,
|
||||
/// Attached messages read under one item, at every depth together.
|
||||
pub max_embedded_messages: usize,
|
||||
/// All that may be read from the file while it is open: this many times its size, plus
|
||||
/// [`Limits::read_floor`], so that the work stays in proportion to the file. It is
|
||||
/// counted for each [`crate::Pst`] from its opening: an import that reads a file more than
|
||||
/// once (again after a cancel, say) opens it again.
|
||||
pub read_multiple: u64,
|
||||
pub read_floor: u64,
|
||||
}
|
||||
|
||||
impl Default for Limits {
|
||||
fn default() -> Self {
|
||||
Limits {
|
||||
max_file_size: 100 * 1024 * 1024 * 1024,
|
||||
max_items: 2_000_000,
|
||||
max_folders: 10_000,
|
||||
max_folder_depth: 64,
|
||||
max_item_bytes: 256 * 1024 * 1024,
|
||||
max_attachment_bytes: 150 * 1024 * 1024,
|
||||
max_recipients: 1_000,
|
||||
max_attachments: 1_000,
|
||||
max_embedded_depth: 8,
|
||||
max_btree_pages: 4_000_000,
|
||||
max_subnodes: 200_000,
|
||||
max_inline_value: 4 * 1024 * 1024,
|
||||
max_listing_bytes: 1024 * 1024 * 1024,
|
||||
max_summary_value: 64 * 1024,
|
||||
max_embedded_messages: 10_000,
|
||||
read_multiple: 64,
|
||||
read_floor: 256 * 1024 * 1024,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
//! The HEADER at offset 0 ([MS-PST] 2.2.2.6) and the ROOT in it (2.2.2.5): which format, how
|
||||
//! data is encoded, and where the two B-trees start. Both CRCs are checked.
|
||||
|
||||
use crate::bytes::{u8_at, u16_at, u32_at, u64_at};
|
||||
use crate::crc::crc32;
|
||||
use crate::crypt::Encryption;
|
||||
use crate::error::{Error, Result};
|
||||
|
||||
/// ANSI (Outlook 97–2002: 32-bit block ids and offsets) or Unicode (64-bit).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Format {
|
||||
Ansi,
|
||||
Unicode,
|
||||
}
|
||||
|
||||
impl Format {
|
||||
/// Bytes in a block id or a file offset.
|
||||
pub(crate) fn id_size(self) -> usize {
|
||||
match self {
|
||||
Format::Ansi => 4,
|
||||
Format::Unicode => 8,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn read_id(self, b: &[u8], at: usize) -> Result<u64> {
|
||||
match self {
|
||||
Format::Ansi => u32_at(b, at).map(u64::from),
|
||||
Format::Unicode => u64_at(b, at),
|
||||
}
|
||||
}
|
||||
|
||||
/// A block reference (`BREF`): id, then offset.
|
||||
pub(crate) fn read_bref(self, b: &[u8], at: usize) -> Result<Bref> {
|
||||
let bid = self.read_id(b, at)?;
|
||||
let ib = self.read_id(b, at.checked_add(self.id_size()).ok_or(Error::OutOfBounds)?)?;
|
||||
Ok(Bref { bid, ib })
|
||||
}
|
||||
|
||||
/// Bytes of a page trailer, a block trailer (the same sizes).
|
||||
pub(crate) fn trailer_size(self) -> usize {
|
||||
match self {
|
||||
Format::Ansi => 12,
|
||||
Format::Unicode => 16,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) struct Bref {
|
||||
pub bid: u64,
|
||||
pub ib: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct Header {
|
||||
pub format: Format,
|
||||
pub version: u16,
|
||||
pub encryption: Encryption,
|
||||
pub file_eof: u64,
|
||||
pub nbt: Bref,
|
||||
pub bbt: Bref,
|
||||
}
|
||||
|
||||
pub(crate) const HEADER_READ: usize = 564;
|
||||
|
||||
impl Header {
|
||||
pub(crate) fn parse(b: &[u8]) -> Result<Header> {
|
||||
if b.get(0..4) != Some(b"!BDN") || b.get(8..10) != Some(b"SM") {
|
||||
return Err(Error::NotPst);
|
||||
}
|
||||
let version = u16_at(b, 10)?;
|
||||
let format = match version {
|
||||
14 | 15 => Format::Ansi,
|
||||
// 36 is an offline folders file with 4 KB pages, 37 one protected by Windows
|
||||
// Information Protection: neither is read.
|
||||
23..=35 => Format::Unicode,
|
||||
v => return Err(Error::UnsupportedVersion(v)),
|
||||
};
|
||||
// dwCRCPartial: the 471 bytes from wMagicClient.
|
||||
let partial = u32_at(b, 4)?;
|
||||
if crc32(b.get(8..479).ok_or(Error::NotPst)?) != partial {
|
||||
return Err(Error::BadChecksum("the file's header"));
|
||||
}
|
||||
// ROOT (at 180, or 164 in ANSI): dwReserved, ibFileEof, ibAMapLast, cbAMapFree,
|
||||
// cbPMapFree, BREFNBT, BREFBBT; then the sentinel and bCryptMethod.
|
||||
let (eof_at, nbt_at, bbt_at, sentinel, crypt) = match format {
|
||||
Format::Unicode => (184, 216, 232, 512, 513),
|
||||
Format::Ansi => (168, 184, 192, 460, 461),
|
||||
};
|
||||
if format == Format::Unicode {
|
||||
// dwCRCFull: the 516 bytes from wMagicClient to bidNextB.
|
||||
let full = u32_at(b, 524)?;
|
||||
if crc32(b.get(8..524).ok_or(Error::NotPst)?) != full {
|
||||
return Err(Error::BadChecksum("the file's header"));
|
||||
}
|
||||
}
|
||||
if u8_at(b, sentinel)? != 0x80 {
|
||||
return Err(Error::Corrupt("the header's sentinel"));
|
||||
}
|
||||
let encryption = match u8_at(b, crypt)? {
|
||||
0 => Encryption::None,
|
||||
1 => Encryption::Permute,
|
||||
2 => Encryption::Cyclic,
|
||||
other => return Err(Error::UnsupportedEncryption(other)),
|
||||
};
|
||||
let file_eof = format.read_id(b, eof_at)?;
|
||||
let nbt = format.read_bref(b, nbt_at)?;
|
||||
let bbt = format.read_bref(b, bbt_at)?;
|
||||
Ok(Header { format, version, encryption, file_eof, nbt, bbt })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
//! The heap-on-node ([MS-PST] 2.3.1): a node's data blocks, each with its page map, and the
|
||||
//! items allocated in them, found by their HID.
|
||||
|
||||
use crate::bytes::{slice, u8_at, u16_at, u32_at};
|
||||
use crate::error::{Error, Result};
|
||||
|
||||
const HN_SIGNATURE: u8 = 0xEC;
|
||||
pub(crate) const CLIENT_TC: u8 = 0x7C;
|
||||
pub(crate) const CLIENT_BTH: u8 = 0xB5;
|
||||
pub(crate) const CLIENT_PC: u8 = 0xBC;
|
||||
|
||||
pub(crate) struct Heap {
|
||||
blocks: Vec<Vec<u8>>,
|
||||
pub client: u8,
|
||||
pub user_root: u32,
|
||||
}
|
||||
|
||||
/// Whether an HNID names a heap item (else it names a subnode).
|
||||
pub(crate) fn is_hid(hnid: u32) -> bool {
|
||||
hnid & 0x1F == 0
|
||||
}
|
||||
|
||||
impl Heap {
|
||||
pub(crate) fn new(blocks: Vec<Vec<u8>>) -> Result<Heap> {
|
||||
let first = blocks.first().ok_or(Error::Corrupt("an empty heap"))?;
|
||||
if u8_at(first, 2)? != HN_SIGNATURE {
|
||||
return Err(Error::Corrupt("a heap's signature"));
|
||||
}
|
||||
let client = u8_at(first, 3)?;
|
||||
let user_root = u32_at(first, 4)?;
|
||||
if blocks.len() > 1 << 16 {
|
||||
return Err(Error::Corrupt("a heap of too many blocks"));
|
||||
}
|
||||
// Every page map's offsets run forward and end before the map, so that no two items
|
||||
// share a byte and the items of a heap together are no larger than it.
|
||||
for block in &blocks {
|
||||
let map = usize::from(u16_at(block, 0)?);
|
||||
let count = usize::from(u16_at(block, map)?);
|
||||
let mut last = 0usize;
|
||||
for i in 0..=count {
|
||||
let off = i.checked_mul(2).and_then(|o| o.checked_add(map)).and_then(|o| o.checked_add(4)).ok_or(Error::OutOfBounds)?;
|
||||
let at = usize::from(u16_at(block, off)?);
|
||||
if at < last || at > map {
|
||||
return Err(Error::Corrupt("a heap's page map"));
|
||||
}
|
||||
last = at;
|
||||
}
|
||||
}
|
||||
Ok(Heap { blocks, client, user_root })
|
||||
}
|
||||
|
||||
/// The bytes of all its blocks.
|
||||
pub(crate) fn size(&self) -> usize {
|
||||
self.blocks.iter().map(Vec::len).sum()
|
||||
}
|
||||
|
||||
/// The item `hid`; an HID of zero is an empty item.
|
||||
pub(crate) fn item(&self, hid: u32) -> Result<&[u8]> {
|
||||
if hid == 0 {
|
||||
return Ok(&[]);
|
||||
}
|
||||
if !is_hid(hid) {
|
||||
return Err(Error::Corrupt("a heap id of the wrong type"));
|
||||
}
|
||||
let index = usize::from(((hid >> 5) & 0x7FF) as u16);
|
||||
let block = self.blocks.get((hid >> 16) as usize).ok_or(Error::Corrupt("a heap id past the heap"))?;
|
||||
let map = usize::from(u16_at(block, 0)?);
|
||||
let count = usize::from(u16_at(block, map)?);
|
||||
if index == 0 || index > count {
|
||||
return Err(Error::Corrupt("a heap id past the page map"));
|
||||
}
|
||||
let at = |i: usize| -> Result<usize> {
|
||||
let off = i.checked_mul(2).and_then(|o| o.checked_add(map)).and_then(|o| o.checked_add(4)).ok_or(Error::OutOfBounds)?;
|
||||
Ok(usize::from(u16_at(block, off)?))
|
||||
};
|
||||
let start = at(index.saturating_sub(1))?;
|
||||
let end = at(index)?;
|
||||
if start > end || end > map {
|
||||
return Err(Error::Corrupt("a heap item outside its block"));
|
||||
}
|
||||
slice(block, start, end.saturating_sub(start))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
//! A reader of Outlook personal folders files (`.pst`), written from Microsoft's published
|
||||
//! specification, [MS-PST], and nothing else.
|
||||
//!
|
||||
//! [`Pst::open`] reads over any `Read + Seek` and never holds the file in memory: a page or a
|
||||
//! block is read when it is needed. ANSI and Unicode files are read, with data blocks encoded
|
||||
//! either way the format allows (or not at all). A file with a password is read only once
|
||||
//! [`Pst::unlock`] has been given it.
|
||||
//!
|
||||
//! The layers are the specification's: the node database (`ndb`: B-trees of nodes and blocks,
|
||||
//! data trees, subnode trees), lists, tables and properties (`ltp`: the heap on a node, its
|
||||
//! B-tree, property and table contexts), and the messaging layer ([`Pst`]: the store, folders,
|
||||
//! items, recipients, attachments, attached messages, named properties).
|
||||
//!
|
||||
//! A .pst file is hostile input. Nothing here panics on any bytes: every offset and length is
|
||||
//! checked against the file, every CRC, signature and block id against what it guards, trees
|
||||
//! are bounded in depth and read each part once, and the caps of [`Limits`] are checked before
|
||||
//! anything is read or allocated. Past a cap, a part is left out and named ([`Error::Cap`]).
|
||||
#![cfg_attr(not(test), deny(clippy::indexing_slicing, clippy::arithmetic_side_effects))]
|
||||
|
||||
mod bth;
|
||||
mod bytes;
|
||||
mod crc;
|
||||
mod crypt;
|
||||
mod error;
|
||||
mod header;
|
||||
mod heap;
|
||||
mod list_stream;
|
||||
mod ltp;
|
||||
mod named;
|
||||
mod ndb;
|
||||
mod prop;
|
||||
mod pst;
|
||||
pub mod recur;
|
||||
mod source;
|
||||
/// The test writer: synthetic .pst files from invented folders and messages, for this crate's
|
||||
/// tests and, through the `test-writer` feature (a dev-dependency only, never in a build that
|
||||
/// ships), for the tests of the import that reads them.
|
||||
#[cfg(any(test, feature = "test-writer"))]
|
||||
#[doc(hidden)]
|
||||
#[path = "tests/writer.rs"]
|
||||
#[allow(
|
||||
clippy::unwrap_used,
|
||||
clippy::expect_used,
|
||||
clippy::panic,
|
||||
clippy::indexing_slicing,
|
||||
clippy::arithmetic_side_effects,
|
||||
clippy::new_without_default,
|
||||
clippy::too_many_arguments,
|
||||
dead_code
|
||||
)]
|
||||
pub mod test_writer;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
pub use crc::crc32;
|
||||
pub use crypt::Encryption;
|
||||
pub use error::{Cap, Damage, Error, Limits, MAX_DAMAGE, Result};
|
||||
pub use header::Format;
|
||||
pub use list_stream::{ListMember, ListStream, MAX_LIST_ENTRY, list_stream};
|
||||
pub use named::{NamedMap, NamedProperty, PS_MAPI, PS_PUBLIC_STRINGS, PropName};
|
||||
pub use prop::{PropValue, Properties, Property, types};
|
||||
pub use pst::{
|
||||
Attachment, AttachmentContent, AttachmentReader, AttachmentRef, Folder, FolderEntry, FolderKind, FolderTree, ItemList, ItemSummary, LeftOut,
|
||||
LeftOutPart, Message, MessageRef, Nid, NodeInfo, Part, Pst, Recipient, RecipientKind, Store, node_types,
|
||||
};
|
||||
pub use recur::{Recur, RecurException, appointment_recurrence, time_zone_key};
|
||||
@@ -0,0 +1,106 @@
|
||||
//! The stream a personal distribution list keeps its members in once they pass about 15,000
|
||||
//! bytes (PidLidDistributionListStream, [MS-OXOCNTC] 2.2.2.2.4): a header, then one
|
||||
//! DistListMemberInfo (2.2.2.2.4.1) for each member, each an entry id and a one-off entry id,
|
||||
//! then two terminators.
|
||||
//!
|
||||
//! The bytes are hostile: the count of members is checked against the bytes there are, every
|
||||
//! length against what is left, nothing is allocated by what the stream says, and at most
|
||||
//! `max_members` members are kept.
|
||||
|
||||
use crate::bytes::{slice, u16_at, u32_at};
|
||||
use crate::error::{Error, Result};
|
||||
|
||||
/// The header: StreamVersion, Reserved, BuildVersion, DistListStreamFlags, CountOfEntries,
|
||||
/// TotalEntryIDSize, TotalOneOffSize, TotalExtraSize.
|
||||
const HEADER: usize = 28;
|
||||
/// The least a member takes: its three sizes.
|
||||
const LEAST_MEMBER: usize = 12;
|
||||
/// Longest an entry id of a member may be (one-off and wrapped entry ids are small).
|
||||
pub const MAX_LIST_ENTRY: usize = 16 * 1024;
|
||||
|
||||
/// One member of the stream: its entry id (a one-off entry id or a WrappedEntryId) and its
|
||||
/// one-off entry id (empty when it has none).
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ListMember<'a> {
|
||||
pub entry_id: &'a [u8],
|
||||
pub one_off: &'a [u8],
|
||||
}
|
||||
|
||||
/// The members a stream holds, as far as they could be read.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ListStream<'a> {
|
||||
/// The members read, at most the cap given.
|
||||
pub members: Vec<ListMember<'a>>,
|
||||
/// Members read past the cap, counted and not kept.
|
||||
pub more: usize,
|
||||
/// Members the stream says it has and that couldn't be read: it ends, or a length runs past
|
||||
/// it, before them.
|
||||
pub damaged: usize,
|
||||
/// Whether the totals of the header disagree with the members read, or the members carry
|
||||
/// extra information, which they are not to.
|
||||
pub totals_differ: bool,
|
||||
}
|
||||
|
||||
const SHORT: Error = Error::Corrupt("a distribution list's stream is shorter than its header");
|
||||
const VERSION: Error = Error::Corrupt("a distribution list's stream of a version this reader doesn't know");
|
||||
const COUNT: Error = Error::Corrupt("a distribution list's stream counts more members than it has room for");
|
||||
|
||||
/// The members of a distribution list's stream. Fails when the header can't be believed (too
|
||||
/// short, another version, more members than the bytes could hold); a member that runs past the
|
||||
/// end stops the reading there, and the members before it are kept.
|
||||
pub fn list_stream(b: &[u8], max_members: usize) -> Result<ListStream<'_>> {
|
||||
if b.len() < HEADER {
|
||||
return Err(SHORT);
|
||||
}
|
||||
if u16_at(b, 0)? != 1 {
|
||||
return Err(VERSION);
|
||||
}
|
||||
let count = usize::try_from(u32_at(b, 12)?).map_err(|_| COUNT)?;
|
||||
let total_ids = u32_at(b, 16)?;
|
||||
let total_one_offs = u32_at(b, 20)?;
|
||||
let total_extra = u32_at(b, 24)?;
|
||||
let room = b.len().saturating_sub(HEADER) / LEAST_MEMBER;
|
||||
if count > room {
|
||||
return Err(COUNT);
|
||||
}
|
||||
let mut out = ListStream { members: Vec::new(), more: 0, damaged: 0, totals_differ: false };
|
||||
let (mut ids, mut one_offs, mut extra) = (0u64, 0u64, 0u64);
|
||||
let mut at = HEADER;
|
||||
for read in 0..count {
|
||||
let Some((member, extra_len, next)) = member(b, at) else {
|
||||
out.damaged = count.saturating_sub(read);
|
||||
break;
|
||||
};
|
||||
at = next;
|
||||
ids = ids.saturating_add(member.entry_id.len() as u64);
|
||||
one_offs = one_offs.saturating_add(member.one_off.len() as u64);
|
||||
extra = extra.saturating_add(extra_len as u64);
|
||||
if out.members.len() < max_members {
|
||||
out.members.push(member);
|
||||
} else {
|
||||
out.more = out.more.saturating_add(1);
|
||||
}
|
||||
}
|
||||
out.totals_differ = out.damaged == 0 && (ids != u64::from(total_ids) || one_offs != u64::from(total_one_offs) || extra != 0 || total_extra != 0);
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// The member at `at`, how long its extra information is, and where the next begins, when it
|
||||
/// fits in `b`. ExtraMemberInfoSize is to be 0; what it names is stepped over when it fits.
|
||||
fn member(b: &[u8], at: usize) -> Option<(ListMember<'_>, usize, usize)> {
|
||||
let (entry_id, at) = sized(b, at)?;
|
||||
let (one_off, at) = sized(b, at)?;
|
||||
let (extra, at) = sized(b, at)?;
|
||||
Some((ListMember { entry_id, one_off }, extra.len(), at))
|
||||
}
|
||||
|
||||
/// The bytes a 4-byte size at `at` names, at most [`MAX_LIST_ENTRY`], and where they end.
|
||||
fn sized(b: &[u8], at: usize) -> Option<(&[u8], usize)> {
|
||||
let len = usize::try_from(u32_at(b, at).ok()?).ok()?;
|
||||
if len > MAX_LIST_ENTRY {
|
||||
return None;
|
||||
}
|
||||
let start = at.checked_add(4)?;
|
||||
let bytes = slice(b, start, len).ok()?;
|
||||
Some((bytes, start.checked_add(len)?))
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
//! Lists, tables and properties ([MS-PST] 2.3): a node's heap with its subnodes, the property
|
||||
//! context (2.3.3) and the table context (2.3.4), whose row matrix may be spread over blocks and
|
||||
//! is read a block at a time.
|
||||
|
||||
use std::io::{Read, Seek};
|
||||
|
||||
use crate::bth;
|
||||
use crate::bytes::{slice, u8_at, u16_at, u32_at};
|
||||
use crate::error::{Cap, Error, Result};
|
||||
use crate::header::Format;
|
||||
use crate::heap::{CLIENT_PC, CLIENT_TC, Heap, is_hid};
|
||||
use crate::ndb::{Ndb, Subnodes, is_internal};
|
||||
use crate::prop::{self, PropValue, Properties, Property, fixed_size};
|
||||
|
||||
/// A node's data as a heap, and its subnodes (where values too large for the heap are kept).
|
||||
pub(crate) struct NodeData {
|
||||
pub heap: Heap,
|
||||
pub subnodes: Subnodes,
|
||||
}
|
||||
|
||||
/// How much more one call may read and keep: every node it loads, every table block and
|
||||
/// every value, charged as read (so a value named twice costs twice). Past it, `cap`.
|
||||
pub(crate) struct Budget {
|
||||
pub left: u64,
|
||||
/// A value larger than this is not read but left as [`PropValue::Large`].
|
||||
pub inline: u64,
|
||||
pub cap: Cap,
|
||||
}
|
||||
|
||||
impl Budget {
|
||||
pub(crate) fn new(left: u64, inline: u64, cap: Cap) -> Budget {
|
||||
Budget { left, inline, cap }
|
||||
}
|
||||
|
||||
pub(crate) fn take(&mut self, n: u64) -> Result<()> {
|
||||
self.left = self.left.checked_sub(n).ok_or(Error::Cap(self.cap))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Charges what is kept of a value or a structure: refused past what is left.
|
||||
pub(crate) fn keep(&mut self, n: usize) -> Result<()> {
|
||||
self.take(n as u64)
|
||||
}
|
||||
|
||||
/// Charges what is kept of a note of something left out: never refused (each is one per
|
||||
/// part already charged, so they are bounded), but what is left goes down.
|
||||
pub(crate) fn note(&mut self, n: usize) {
|
||||
self.left = self.left.saturating_sub(n as u64);
|
||||
}
|
||||
}
|
||||
|
||||
impl NodeData {
|
||||
/// A node's heap and subnodes, charged to `budget`.
|
||||
pub(crate) fn load<R: Read + Seek>(ndb: &mut Ndb<R>, data: u64, sub: u64, budget: &mut Budget) -> Result<NodeData> {
|
||||
let max = ndb.limits.max_item_bytes.min(budget.left);
|
||||
let pieces = ndb.read_pieces(data, max, budget.cap)?;
|
||||
budget.take(pieces.iter().map(|p| p.len() as u64).sum())?;
|
||||
let heap = Heap::new(pieces)?;
|
||||
let subnodes = ndb.subnodes(sub)?;
|
||||
Ok(NodeData { heap, subnodes })
|
||||
}
|
||||
|
||||
/// The size of the value an HNID names, without reading it.
|
||||
pub(crate) fn hnid_len<R: Read + Seek>(&self, ndb: &mut Ndb<R>, hnid: u32) -> Result<u64> {
|
||||
if is_hid(hnid) {
|
||||
return Ok(self.heap.item(hnid)?.len() as u64);
|
||||
}
|
||||
let entry = self.subnodes.get(&hnid).ok_or(Error::NotFound("a property's subnode"))?;
|
||||
ndb.data_size(entry.data)
|
||||
}
|
||||
|
||||
/// The subnode an HNID names, when it names one.
|
||||
pub(crate) fn subnode(&self, hnid: u32) -> Option<crate::ndb::SubEntry> {
|
||||
if is_hid(hnid) { None } else { self.subnodes.get(&hnid).copied() }
|
||||
}
|
||||
|
||||
/// The value an HNID names, refused past `max` bytes.
|
||||
pub(crate) fn hnid_bytes<R: Read + Seek>(&self, ndb: &mut Ndb<R>, hnid: u32, max: u64, cap: Cap) -> Result<Vec<u8>> {
|
||||
if is_hid(hnid) {
|
||||
let item = self.heap.item(hnid)?;
|
||||
if item.len() as u64 > max {
|
||||
return Err(Error::Cap(cap));
|
||||
}
|
||||
return Ok(item.to_vec());
|
||||
}
|
||||
let entry = self.subnodes.get(&hnid).ok_or(Error::NotFound("a property's subnode"))?;
|
||||
ndb.read_data(entry.data, max, cap)
|
||||
}
|
||||
|
||||
/// A value stored apart: read when small enough, else [`PropValue::Large`].
|
||||
fn apart<R: Read + Seek>(&self, ndb: &mut Ndb<R>, ty: u16, hnid: u32, budget: &mut Budget) -> Result<PropValue> {
|
||||
let inline = budget.inline;
|
||||
let size = self.hnid_len(ndb, hnid)?;
|
||||
if size > inline {
|
||||
return Ok(PropValue::Large { size });
|
||||
}
|
||||
budget.take(size)?;
|
||||
let b = self.hnid_bytes(ndb, hnid, inline, budget.cap)?;
|
||||
// What making the value holds must fit in what is left, before it is made.
|
||||
if prop::building_cost(ty, &b) as u64 > budget.left {
|
||||
return Err(Error::Cap(budget.cap));
|
||||
}
|
||||
prop::stored(ty, &b)
|
||||
}
|
||||
}
|
||||
|
||||
/// A value that failed to read becomes [`PropValue::Damaged`] with its reason, so that one
|
||||
/// damaged property doesn't cost the rest; a cap, or the reader under the file failing, is
|
||||
/// passed on.
|
||||
fn salvage(ty: u16, r: Result<PropValue>) -> Result<PropValue> {
|
||||
match r {
|
||||
Ok(v) => Ok(v),
|
||||
Err(e @ (Error::Cap(_) | Error::Io(_))) => Err(e),
|
||||
Err(reason) => Ok(PropValue::Damaged { ty, reason }),
|
||||
}
|
||||
}
|
||||
|
||||
/// A property context's raw records: id, type and the 4-byte value or HNID.
|
||||
pub(crate) fn pc_records(node: &NodeData) -> Result<Vec<(u16, u16, u32)>> {
|
||||
if node.heap.client != CLIENT_PC {
|
||||
return Err(Error::Corrupt("a property context's signature"));
|
||||
}
|
||||
// Property ids are 16 bits: more records than that are repeats.
|
||||
let tree = bth::read(&node.heap, node.heap.user_root, 1 << 16, Error::Corrupt("a property context with more records than ids"))?;
|
||||
if tree.key_size != 2 || tree.data_size != 6 {
|
||||
return Err(Error::Corrupt("a property context's record sizes"));
|
||||
}
|
||||
tree.records.iter().map(|r| Ok((u16_at(r, 0)?, u16_at(r, 2)?, u32_at(r, 4)?))).collect()
|
||||
}
|
||||
|
||||
/// One property context value from its record.
|
||||
pub(crate) fn pc_value<R: Read + Seek>(ndb: &mut Ndb<R>, node: &NodeData, ty: u16, raw: u32, budget: &mut Budget) -> Result<PropValue> {
|
||||
let r = match fixed_size(ty) {
|
||||
Some(n) if n <= 4 => prop::fixed(ty, &raw.to_le_bytes()),
|
||||
Some(_) => node.heap.item(raw).and_then(|b| prop::fixed(ty, b)),
|
||||
None if is_known_variable(ty) => node.apart(ndb, ty, raw, budget),
|
||||
None => Ok(PropValue::Unknown { ty, data: raw.to_le_bytes().to_vec() }),
|
||||
};
|
||||
salvage(ty, r)
|
||||
}
|
||||
|
||||
fn is_known_variable(ty: u16) -> bool {
|
||||
use crate::prop::types::{BINARY, MULTIPLE, OBJECT, RESTRICTION, RULE_ACTION, SERVER_ID, STRING, STRING8};
|
||||
ty & MULTIPLE != 0 || matches!(ty, STRING | STRING8 | BINARY | OBJECT | SERVER_ID | RESTRICTION | RULE_ACTION)
|
||||
}
|
||||
|
||||
/// All of a property context's properties; those in `defer` that are stored apart are left as
|
||||
/// [`PropValue::Large`] (an attachment's data, read on its own).
|
||||
pub(crate) fn pc_properties<R: Read + Seek>(ndb: &mut Ndb<R>, node: &NodeData, budget: &mut Budget, defer: &[u16]) -> Result<Properties> {
|
||||
let mut out = Vec::new();
|
||||
for (id, ty, raw) in pc_records(node)? {
|
||||
let value = if defer.contains(&id) && fixed_size(ty).is_none() && is_known_variable(ty) && ty != crate::prop::types::OBJECT {
|
||||
salvage(ty, node.hnid_len(ndb, raw).map(|size| PropValue::Large { size }))?
|
||||
} else {
|
||||
pc_value(ndb, node, ty, raw, budget)?
|
||||
};
|
||||
let p = Property { id, ty, value };
|
||||
budget.keep(p.kept() as usize)?;
|
||||
out.push(p);
|
||||
}
|
||||
out.sort_by_key(|p| p.id);
|
||||
Ok(Properties(out))
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct Column {
|
||||
id: u16,
|
||||
ty: u16,
|
||||
offset: usize,
|
||||
size: usize,
|
||||
bit: usize,
|
||||
}
|
||||
|
||||
enum Matrix {
|
||||
None,
|
||||
Heap(Vec<u8>),
|
||||
Blocks { ids: Vec<u64>, per_block: usize, cached: Option<(usize, Vec<u8>)> },
|
||||
}
|
||||
|
||||
/// A table context: its columns, its rows (`(row id, row index)` from the row index), and the
|
||||
/// row matrix, read a block at a time.
|
||||
pub(crate) struct Table {
|
||||
node: NodeData,
|
||||
cols: Vec<Column>,
|
||||
row_size: usize,
|
||||
ceb_start: usize,
|
||||
pub rows: Vec<(u32, u32)>,
|
||||
matrix: Matrix,
|
||||
}
|
||||
|
||||
impl Table {
|
||||
/// A table of at most `max_rows` rows; more are refused (`cap`) as the row index is read.
|
||||
pub(crate) fn open<R: Read + Seek>(ndb: &mut Ndb<R>, node: NodeData, max_rows: usize, cap: Cap) -> Result<Table> {
|
||||
let format = ndb.header.format;
|
||||
if node.heap.client != CLIENT_TC {
|
||||
return Err(Error::Corrupt("a table context's signature"));
|
||||
}
|
||||
let info = node.heap.item(node.heap.user_root)?;
|
||||
if u8_at(info, 0)? != CLIENT_TC {
|
||||
return Err(Error::Corrupt("a table context's header"));
|
||||
}
|
||||
let ncols = usize::from(u8_at(info, 1)?);
|
||||
let rgib = [u16_at(info, 2)?, u16_at(info, 4)?, u16_at(info, 6)?, u16_at(info, 8)?].map(usize::from);
|
||||
let [b4, b2, b1, bm] = rgib;
|
||||
if !(b4 <= b2 && b2 <= b1 && b1 <= bm) || bm < 4 {
|
||||
return Err(Error::Corrupt("a table context's row layout"));
|
||||
}
|
||||
let row_index_hid = u32_at(info, 10)?;
|
||||
let rows_hnid = u32_at(info, 14)?;
|
||||
let mut cols = Vec::with_capacity(ncols);
|
||||
for i in 0..ncols {
|
||||
let c = slice(info, i.saturating_mul(8).saturating_add(22), 8)?;
|
||||
let tag = u32_at(c, 0)?;
|
||||
let col = Column {
|
||||
id: (tag >> 16) as u16,
|
||||
ty: (tag & 0xFFFF) as u16,
|
||||
offset: usize::from(u16_at(c, 4)?),
|
||||
size: usize::from(u8_at(c, 6)?),
|
||||
bit: usize::from(u8_at(c, 7)?),
|
||||
};
|
||||
if col.offset.saturating_add(col.size) > b1 {
|
||||
return Err(Error::Corrupt("a table column outside its row"));
|
||||
}
|
||||
cols.push(col);
|
||||
}
|
||||
let trailer = format.trailer_size();
|
||||
let block_capacity = 8192usize.saturating_sub(trailer);
|
||||
if bm > block_capacity {
|
||||
return Err(Error::Corrupt("a table row larger than a block"));
|
||||
}
|
||||
let tree = bth::read(&node.heap, row_index_hid, max_rows, Error::Cap(cap))?;
|
||||
let index_size = match format {
|
||||
Format::Unicode => 4,
|
||||
Format::Ansi => 2,
|
||||
};
|
||||
if tree.key_size != 4 || tree.data_size != index_size {
|
||||
return Err(Error::Corrupt("a table's row index"));
|
||||
}
|
||||
let mut rows = Vec::with_capacity(tree.records.len());
|
||||
for r in &tree.records {
|
||||
let index = match format {
|
||||
Format::Unicode => u32_at(r, 4)?,
|
||||
Format::Ansi => u32::from(u16_at(r, 4)?),
|
||||
};
|
||||
rows.push((u32_at(r, 0)?, index));
|
||||
}
|
||||
let matrix = if rows_hnid == 0 {
|
||||
Matrix::None
|
||||
} else if is_hid(rows_hnid) {
|
||||
Matrix::Heap(node.heap.item(rows_hnid)?.to_vec())
|
||||
} else {
|
||||
let entry = node.subnodes.get(&rows_hnid).ok_or(Error::NotFound("a table's rows"))?;
|
||||
let max = ndb.limits.max_item_bytes;
|
||||
let (ids, _) = ndb.data_blocks(entry.data, max, Cap::ItemSize)?;
|
||||
Matrix::Blocks { ids, per_block: block_capacity.checked_div(bm).unwrap_or(1), cached: None }
|
||||
};
|
||||
if matches!(matrix, Matrix::None) && !rows.is_empty() {
|
||||
return Err(Error::NotFound("a table's rows"));
|
||||
}
|
||||
Ok(Table { node, cols, row_size: bm, ceb_start: b1, rows, matrix })
|
||||
}
|
||||
|
||||
/// The bytes of the row at `index` in the row matrix.
|
||||
fn row_bytes<R: Read + Seek>(&mut self, ndb: &mut Ndb<R>, index: u32, budget: &mut Budget) -> Result<Vec<u8>> {
|
||||
let index = index as usize;
|
||||
let size = self.row_size;
|
||||
match &mut self.matrix {
|
||||
Matrix::None => Err(Error::NotFound("a table's rows")),
|
||||
Matrix::Heap(b) => Ok(slice(b, index.checked_mul(size).ok_or(Error::OutOfBounds)?, size)?.to_vec()),
|
||||
Matrix::Blocks { ids, per_block, cached } => {
|
||||
let per = (*per_block).max(1);
|
||||
let block = index.checked_div(per).unwrap_or(0);
|
||||
let within = index.checked_rem(per).unwrap_or(0);
|
||||
if cached.as_ref().is_none_or(|(i, _)| *i != block) {
|
||||
let bid = *ids.get(block).ok_or(Error::Corrupt("a table row past its blocks"))?;
|
||||
if is_internal(bid) {
|
||||
return Err(Error::Corrupt("a table row in an internal block"));
|
||||
}
|
||||
let b = ndb.block(bid)?;
|
||||
budget.take(b.len() as u64)?;
|
||||
*cached = Some((block, b));
|
||||
}
|
||||
let data = cached.as_ref().map(|(_, d)| d.as_slice()).unwrap_or(&[]);
|
||||
Ok(slice(data, within.saturating_mul(size), size)?.to_vec())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The row with row id `id` at `index`: its cells, those that exist. Values larger than the
|
||||
/// inline cap come back as [`PropValue::Large`].
|
||||
pub(crate) fn row<R: Read + Seek>(&mut self, ndb: &mut Ndb<R>, id: u32, index: u32, budget: &mut Budget) -> Result<Properties> {
|
||||
let row = self.row_bytes(ndb, index, budget)?;
|
||||
if u32_at(&row, 0)? != id {
|
||||
return Err(Error::Corrupt("a table row under another row's id"));
|
||||
}
|
||||
let mut out = Vec::new();
|
||||
for col in self.cols.clone() {
|
||||
let byte = row.get(self.ceb_start.saturating_add(col.bit / 8)).copied().unwrap_or(0);
|
||||
if byte & (0x80u8 >> (col.bit % 8)) == 0 {
|
||||
continue;
|
||||
}
|
||||
let cell = slice(&row, col.offset, col.size)?;
|
||||
let value = match fixed_size(col.ty) {
|
||||
Some(n) if n <= 8 => {
|
||||
if col.size < n {
|
||||
PropValue::Unknown { ty: col.ty, data: cell.to_vec() }
|
||||
} else {
|
||||
salvage(col.ty, prop::fixed(col.ty, cell))?
|
||||
}
|
||||
}
|
||||
Some(_) => salvage(col.ty, u32_at(cell, 0).and_then(|h| self.node.heap.item(h).and_then(|b| prop::fixed(col.ty, b))))?,
|
||||
// A cell too short for its reference loses that property alone.
|
||||
None if is_known_variable(col.ty) => salvage(col.ty, u32_at(cell, 0).and_then(|hnid| self.node.apart(ndb, col.ty, hnid, budget)))?,
|
||||
None => PropValue::Unknown { ty: col.ty, data: cell.to_vec() },
|
||||
};
|
||||
let p = Property { id: col.id, ty: col.ty, value };
|
||||
budget.keep(p.kept() as usize)?;
|
||||
out.push(p);
|
||||
}
|
||||
out.sort_by_key(|p| p.id);
|
||||
Ok(Properties(out))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
//! The named property map ([MS-PST] 2.4.7): which name (a property set and a number or a string)
|
||||
//! each property id from 0x8000 stands for in this file. Read from its entry, GUID and string
|
||||
//! streams; the hash table is only for writers and is not read.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::io::{Read, Seek};
|
||||
|
||||
use crate::bytes::{array16, slice, u16_at, u32_at, utf16};
|
||||
use crate::error::{Cap, Error, Result};
|
||||
use crate::ltp::{Budget, NodeData, pc_records};
|
||||
use crate::ndb::Ndb;
|
||||
|
||||
/// PS_MAPI ([MS-OXPROPS] 1.3.2), as stored.
|
||||
pub const PS_MAPI: [u8; 16] = [0x28, 0x03, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46];
|
||||
/// PS_PUBLIC_STRINGS ([MS-OXPROPS] 1.3.2), as stored.
|
||||
pub const PS_PUBLIC_STRINGS: [u8; 16] = [0x29, 0x03, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46];
|
||||
|
||||
/// The ids 0x8000 to 0xFFFF.
|
||||
const MAX_ENTRIES: usize = 0x8000;
|
||||
const MAX_STRINGS: u64 = 16 * 1024 * 1024;
|
||||
/// One name, as stored (UTF-16): longer ones are left out.
|
||||
pub(crate) const MAX_NAME: usize = 1024;
|
||||
/// All names together, as stored: each entry may name the same string, so this is counted per
|
||||
/// entry and not per string.
|
||||
pub(crate) const MAX_NAMES: usize = 4 * 1024 * 1024;
|
||||
|
||||
/// A named property's name.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum PropName {
|
||||
Id(u32),
|
||||
Name(String),
|
||||
}
|
||||
|
||||
/// What one property id from 0x8000 stands for.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct NamedProperty {
|
||||
pub id: u16,
|
||||
/// The property set, as stored (the first three fields little-endian).
|
||||
pub guid: [u8; 16],
|
||||
pub name: PropName,
|
||||
}
|
||||
|
||||
/// The file's named properties, by id.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct NamedMap {
|
||||
map: BTreeMap<u16, NamedProperty>,
|
||||
left_out: Vec<(Option<u16>, Error)>,
|
||||
}
|
||||
|
||||
impl NamedMap {
|
||||
pub fn get(&self, id: u16) -> Option<&NamedProperty> {
|
||||
self.map.get(&id)
|
||||
}
|
||||
|
||||
/// The id that stands for a name in a property set, if the file has one.
|
||||
pub fn find(&self, guid: &[u8; 16], name: &PropName) -> Option<u16> {
|
||||
self.map.values().find(|p| &p.guid == guid && &p.name == name).map(|p| p.id)
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = &NamedProperty> {
|
||||
self.map.values()
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.map.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.map.is_empty()
|
||||
}
|
||||
|
||||
/// The ids whose names were left out (damaged, or past a cap), and why; `None` for an
|
||||
/// entry whose index names no id (0x8000 or more).
|
||||
pub fn left_out(&self) -> &[(Option<u16>, Error)] {
|
||||
&self.left_out
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn read<R: Read + Seek>(ndb: &mut Ndb<R>, node: &NodeData) -> Result<NamedMap> {
|
||||
let mut guids = Vec::new();
|
||||
let mut entries = Vec::new();
|
||||
let mut strings = Vec::new();
|
||||
let mut budget = Budget::new(ndb.limits.max_item_bytes, MAX_STRINGS, Cap::NamedProperties);
|
||||
for (id, ty, raw) in pc_records(node)? {
|
||||
if ty != crate::prop::types::BINARY {
|
||||
continue;
|
||||
}
|
||||
let max = match id {
|
||||
0x0002 => (MAX_ENTRIES as u64).saturating_mul(16),
|
||||
0x0003 => (MAX_ENTRIES as u64).saturating_mul(8),
|
||||
0x0004 => MAX_STRINGS,
|
||||
_ => continue,
|
||||
};
|
||||
let b = node.hnid_bytes(ndb, raw, max, Cap::NamedProperties)?;
|
||||
budget.take(b.len() as u64)?;
|
||||
match id {
|
||||
0x0002 => guids = b,
|
||||
0x0003 => entries = b,
|
||||
_ => strings = b,
|
||||
}
|
||||
}
|
||||
let mut map = BTreeMap::new();
|
||||
let mut left_out = Vec::new();
|
||||
let mut names = 0usize;
|
||||
for e in entries.as_chunks::<8>().0 {
|
||||
let value = u32_at(e, 0)?;
|
||||
let flags = u16_at(e, 4)?;
|
||||
let index = u16_at(e, 6)?;
|
||||
let Some(id) = 0x8000u16.checked_add(index) else {
|
||||
left_out.push((None, Error::Corrupt("a named property's index")));
|
||||
continue;
|
||||
};
|
||||
let is_string = flags & 1 != 0;
|
||||
let name = || -> Result<(NamedProperty, usize)> {
|
||||
let guid = match flags >> 1 {
|
||||
0 => [0u8; 16],
|
||||
1 => PS_MAPI,
|
||||
2 => PS_PUBLIC_STRINGS,
|
||||
n => array16(&guids, usize::from(n.saturating_sub(3)).saturating_mul(16))?,
|
||||
};
|
||||
let name = if is_string {
|
||||
let at = value as usize;
|
||||
let len = u32_at(&strings, at)? as usize;
|
||||
if len > MAX_NAME || names.saturating_add(len) > MAX_NAMES {
|
||||
return Err(Error::Cap(Cap::NamedProperties));
|
||||
}
|
||||
(PropName::Name(utf16(slice(&strings, at.saturating_add(4), len)?)), len)
|
||||
} else {
|
||||
(PropName::Id(value), 0)
|
||||
};
|
||||
Ok((NamedProperty { id, guid, name: name.0 }, name.1))
|
||||
};
|
||||
match name() {
|
||||
Ok((p, stored)) => {
|
||||
names = names.saturating_add(stored);
|
||||
map.entry(id).or_insert(p);
|
||||
}
|
||||
Err(reason) => left_out.push((Some(id), reason)),
|
||||
}
|
||||
}
|
||||
Ok(NamedMap { map, left_out })
|
||||
}
|
||||
@@ -0,0 +1,546 @@
|
||||
//! The node database ([MS-PST] 2.2): the node and block B-trees (pages of 512 bytes), blocks
|
||||
//! (at most 8 KB, each with its CRC, signature and id checked), data trees (XBLOCK, XXBLOCK)
|
||||
//! and subnode trees (SLBLOCK, SIBLOCK).
|
||||
//!
|
||||
//! A hostile file is kept bounded: a B-tree is at most 9 pages deep and each page must be one
|
||||
//! level below the page that points to it, so a lookup ends; a walk of the whole node tree
|
||||
//! visits a page at most once and at most [`Limits::max_btree_pages`] pages; a data tree is at
|
||||
//! most two levels, its claimed size is checked against the cap before a block is read, and it
|
||||
//! may not be spread over more blocks than its size needs.
|
||||
|
||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||
use std::io::{Read, Seek};
|
||||
|
||||
use crate::bytes::{slice, u8_at, u16_at, u32_at};
|
||||
use crate::crc::crc32;
|
||||
use crate::crypt::{Encryption, decode};
|
||||
use crate::error::{Cap, Damage, Error, Limits, MAX_DAMAGE, Result};
|
||||
use crate::header::{Bref, Format, Header};
|
||||
use crate::source::Source;
|
||||
|
||||
const PAGE: usize = 512;
|
||||
const PTYPE_BBT: u8 = 0x80;
|
||||
const PTYPE_NBT: u8 = 0x81;
|
||||
/// "Up to 8 intermediate levels" (2.2.2.7.7).
|
||||
const MAX_LEVEL: u8 = 8;
|
||||
/// A data tree's average block holds at least this much, or it is refused ([`Cap::DataBlocks`]).
|
||||
const MIN_AVERAGE_BLOCK: u64 = 64;
|
||||
/// B-tree pages kept once read (512 bytes each), so that the upper pages, which every lookup
|
||||
/// passes through, are read from the file once.
|
||||
const CACHED_PAGES: usize = 256;
|
||||
/// Damaged pages told apart (so that one met again is not counted again); past them the
|
||||
/// count of [`Damage::pages`] stops, and is a lower bound.
|
||||
const MAX_NOTED: usize = 1 << 16;
|
||||
|
||||
/// A leaf of the node B-tree (`NBTENTRY`).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) struct NodeEntry {
|
||||
pub nid: u32,
|
||||
pub data: u64,
|
||||
pub sub: u64,
|
||||
pub parent: u32,
|
||||
}
|
||||
|
||||
/// A subnode (`SLENTRY`): its data and its own subnodes.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) struct SubEntry {
|
||||
pub data: u64,
|
||||
pub sub: u64,
|
||||
}
|
||||
|
||||
pub(crate) type Subnodes = BTreeMap<u32, SubEntry>;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(crate) struct BlockEntry {
|
||||
pub ib: u64,
|
||||
pub cb: u16,
|
||||
}
|
||||
|
||||
struct Page {
|
||||
data: Vec<u8>,
|
||||
count: usize,
|
||||
entry_size: usize,
|
||||
level: u8,
|
||||
}
|
||||
|
||||
impl Page {
|
||||
fn entry(&self, i: usize) -> Result<&[u8]> {
|
||||
slice(&self.data, i.checked_mul(self.entry_size).ok_or(Error::OutOfBounds)?, self.entry_size)
|
||||
}
|
||||
}
|
||||
|
||||
/// The block signature (5.5).
|
||||
pub(crate) fn signature(ib: u64, bid: u64) -> u16 {
|
||||
let x = ib ^ bid;
|
||||
((x >> 16) as u16) ^ (x as u16)
|
||||
}
|
||||
|
||||
/// Whether a block id names an internal block (an XBLOCK, XXBLOCK, SLBLOCK or SIBLOCK).
|
||||
pub(crate) fn is_internal(bid: u64) -> bool {
|
||||
bid & 2 != 0
|
||||
}
|
||||
|
||||
/// Whether an error is damage to the file (noted and skipped) rather than a cap reached or
|
||||
/// the reader under the file failing (which end what is being done).
|
||||
fn is_damage(e: &Error) -> bool {
|
||||
!matches!(e, Error::Cap(_) | Error::Io(_))
|
||||
}
|
||||
|
||||
/// The id a block is looked up by: the reserved low bit is ignored (2.2.2.2).
|
||||
fn key(bid: u64) -> u64 {
|
||||
bid & !1
|
||||
}
|
||||
|
||||
pub(crate) struct Ndb<R> {
|
||||
src: Source<R>,
|
||||
pub header: Header,
|
||||
pub limits: Limits,
|
||||
/// Pages as read, by offset; checked again each time they are used.
|
||||
pages: HashMap<u64, Vec<u8>>,
|
||||
/// Pages below a tree's root that could not be read, each once.
|
||||
pub damage: Vec<Damage>,
|
||||
noted: HashSet<u64>,
|
||||
}
|
||||
|
||||
impl<R: Read + Seek> Ndb<R> {
|
||||
pub(crate) fn new(src: Source<R>, header: Header, limits: Limits) -> Self {
|
||||
Ndb { src, header, limits, pages: HashMap::new(), damage: Vec::new(), noted: HashSet::new() }
|
||||
}
|
||||
|
||||
/// Notes a page below a root that could not be read. A cap or a failure of the reader
|
||||
/// under the file is not damage to the file, and is not noted.
|
||||
pub(crate) fn note(&mut self, ib: u64, ptype: u8, e: &Error) {
|
||||
if !is_damage(e) {
|
||||
return;
|
||||
}
|
||||
if self.damage.len() >= MAX_DAMAGE {
|
||||
// Past the cap the pages are counted (once each, to the same cap as the walk's).
|
||||
if self.noted.len() < MAX_NOTED
|
||||
&& self.noted.insert(ib)
|
||||
&& let Some(last) = self.damage.last_mut()
|
||||
{
|
||||
last.pages = last.pages.saturating_add(1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if !self.noted.insert(ib) {
|
||||
return;
|
||||
}
|
||||
let what = if ptype == PTYPE_NBT { "a page of the node tree" } else { "a page of the block tree" };
|
||||
let pages = 1;
|
||||
self.damage.push(Damage { offset: ib, what, reason: e.clone(), pages });
|
||||
if self.damage.len() == MAX_DAMAGE {
|
||||
// The last entry is the one that counts the rest.
|
||||
if let Some(last) = self.damage.last_mut() {
|
||||
last.what = if ptype == PTYPE_NBT { "pages of the node tree, from this one" } else { "pages of the block tree, from this one" };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The page at `bref`, at the level `expect` when one is expected.
|
||||
fn page_at(&mut self, bref: Bref, ptype: u8, expect: Option<u8>) -> Result<Page> {
|
||||
let page = self.read_page(bref, ptype)?;
|
||||
if expect.is_some_and(|e| e != page.level) {
|
||||
return Err(Error::Corrupt("a B-tree page at the wrong level"));
|
||||
}
|
||||
Ok(page)
|
||||
}
|
||||
|
||||
fn page_bytes(&mut self, ib: u64) -> Result<Vec<u8>> {
|
||||
if let Some(p) = self.pages.get(&ib) {
|
||||
return Ok(p.clone());
|
||||
}
|
||||
let data = self.src.read_at(ib, PAGE)?;
|
||||
// ponytail: emptied when full, not least-recently-used; the root and upper pages come
|
||||
// back at once. An LRU if lookups in very large files show it matters.
|
||||
if self.pages.len() >= CACHED_PAGES {
|
||||
self.pages.clear();
|
||||
}
|
||||
self.pages.insert(ib, data.clone());
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
fn format(&self) -> Format {
|
||||
self.header.format
|
||||
}
|
||||
|
||||
fn read_page(&mut self, bref: Bref, ptype: u8) -> Result<Page> {
|
||||
let f = self.format();
|
||||
let data = self.page_bytes(bref.ib)?;
|
||||
let t = PAGE.saturating_sub(f.trailer_size());
|
||||
let (crc_at, bid_at) = match f {
|
||||
Format::Unicode => (t.saturating_add(4), t.saturating_add(8)),
|
||||
Format::Ansi => (t.saturating_add(8), t.saturating_add(4)),
|
||||
};
|
||||
if u8_at(&data, t)? != ptype || u8_at(&data, t.saturating_add(1))? != ptype {
|
||||
return Err(Error::Corrupt("a B-tree page of the wrong type"));
|
||||
}
|
||||
if crc32(slice(&data, 0, t)?) != u32_at(&data, crc_at)? {
|
||||
return Err(Error::BadChecksum("a B-tree page"));
|
||||
}
|
||||
let bid = f.read_id(&data, bid_at)?;
|
||||
if key(bid) != key(bref.bid) || u16_at(&data, t.saturating_add(2))? != signature(bref.ib, bid) {
|
||||
return Err(Error::BadChecksum("a B-tree page's id"));
|
||||
}
|
||||
let (area, meta) = match f {
|
||||
Format::Unicode => (488, 488),
|
||||
Format::Ansi => (496, 496),
|
||||
};
|
||||
let count = usize::from(u8_at(&data, meta)?);
|
||||
let entry_size = usize::from(u8_at(&data, meta.saturating_add(2))?);
|
||||
let level = u8_at(&data, meta.saturating_add(3))?;
|
||||
// A level the format doesn't have is damage to that page, like a bad count.
|
||||
if level > MAX_LEVEL {
|
||||
return Err(Error::Corrupt("a B-tree page's level"));
|
||||
}
|
||||
let id = f.id_size();
|
||||
let needed = match (level, ptype) {
|
||||
(1.., _) => id.saturating_mul(3),
|
||||
(0, PTYPE_NBT) => id.saturating_mul(3).saturating_add(4),
|
||||
_ => id.saturating_mul(2).saturating_add(4),
|
||||
};
|
||||
if entry_size < needed || count.saturating_mul(entry_size) > area {
|
||||
return Err(Error::Corrupt("a B-tree page's entries"));
|
||||
}
|
||||
Ok(Page { data, count, entry_size, level })
|
||||
}
|
||||
|
||||
/// The leaf entry of `target` in the tree at `root`, if there is one.
|
||||
fn find(&mut self, root: Bref, ptype: u8, target: u64) -> Result<Option<Vec<u8>>> {
|
||||
let f = self.format();
|
||||
let id = f.id_size();
|
||||
let mask = |k: u64| if ptype == PTYPE_BBT { key(k) } else { k };
|
||||
let target = mask(target);
|
||||
let mut bref = root;
|
||||
let mut expect: Option<u8> = None;
|
||||
loop {
|
||||
// A page below the root that can't be read loses what is under it, not the file:
|
||||
// it is noted, and this lookup fails.
|
||||
let page = match self.page_at(bref, ptype, expect) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
if bref != root {
|
||||
self.note(bref.ib, ptype, &e);
|
||||
}
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
if page.level == 0 {
|
||||
for i in 0..page.count {
|
||||
let e = page.entry(i)?;
|
||||
if mask(f.read_id(e, 0)?) == target {
|
||||
return Ok(Some(e.to_vec()));
|
||||
}
|
||||
}
|
||||
return Ok(None);
|
||||
}
|
||||
let mut best: Option<(u64, Bref)> = None;
|
||||
for i in 0..page.count {
|
||||
let e = page.entry(i)?;
|
||||
let k = mask(f.read_id(e, 0)?);
|
||||
if k <= target && best.is_none_or(|(b, _)| k >= b) {
|
||||
best = Some((k, f.read_bref(e, id)?));
|
||||
}
|
||||
}
|
||||
match best {
|
||||
Some((_, next)) => bref = next,
|
||||
None => return Ok(None),
|
||||
}
|
||||
expect = page.level.checked_sub(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn node_entry(&self, e: &[u8]) -> Result<NodeEntry> {
|
||||
let f = self.format();
|
||||
let id = f.id_size();
|
||||
Ok(NodeEntry {
|
||||
nid: f.read_id(e, 0)? as u32,
|
||||
data: f.read_id(e, id)?,
|
||||
sub: f.read_id(e, id.saturating_mul(2))?,
|
||||
parent: u32_at(e, id.saturating_mul(3))?,
|
||||
})
|
||||
}
|
||||
|
||||
/// The node `nid` of the node B-tree.
|
||||
pub(crate) fn node(&mut self, nid: u32) -> Result<NodeEntry> {
|
||||
let root = self.header.nbt;
|
||||
match self.find(root, PTYPE_NBT, u64::from(nid))? {
|
||||
Some(e) => self.node_entry(&e),
|
||||
None => Err(Error::NotFound("a node")),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn block_entry(&mut self, bid: u64) -> Result<BlockEntry> {
|
||||
let root = self.header.bbt;
|
||||
let e = self.find(root, PTYPE_BBT, bid)?.ok_or(Error::NotFound("a block"))?;
|
||||
let f = self.format();
|
||||
let bref = f.read_bref(&e, 0)?;
|
||||
let cb = u16_at(&e, f.id_size().saturating_mul(2))?;
|
||||
Ok(BlockEntry { ib: bref.ib, cb })
|
||||
}
|
||||
|
||||
/// The bytes of block `bid`, decoded when it holds data.
|
||||
pub(crate) fn block(&mut self, bid: u64) -> Result<Vec<u8>> {
|
||||
let f = self.format();
|
||||
let entry = self.block_entry(bid)?;
|
||||
let cb = usize::from(entry.cb);
|
||||
let ts = f.trailer_size();
|
||||
let total = cb.saturating_add(ts).div_ceil(64).saturating_mul(64);
|
||||
let mut raw = self.src.read_at(entry.ib, total)?;
|
||||
let t = total.saturating_sub(ts);
|
||||
let (crc_at, bid_at) = match f {
|
||||
Format::Unicode => (t.saturating_add(4), t.saturating_add(8)),
|
||||
Format::Ansi => (t.saturating_add(8), t.saturating_add(4)),
|
||||
};
|
||||
let stored_bid = f.read_id(&raw, bid_at)?;
|
||||
if usize::from(u16_at(&raw, t)?) != cb || key(stored_bid) != key(bid) {
|
||||
return Err(Error::BadChecksum("a block's id"));
|
||||
}
|
||||
if u16_at(&raw, t.saturating_add(2))? != signature(entry.ib, stored_bid) {
|
||||
return Err(Error::BadChecksum("a block's signature"));
|
||||
}
|
||||
if crc32(slice(&raw, 0, cb)?) != u32_at(&raw, crc_at)? {
|
||||
return Err(Error::BadChecksum("a block"));
|
||||
}
|
||||
raw.truncate(cb);
|
||||
if !is_internal(bid) {
|
||||
// The key of cyclic encoding is the block's id without the reserved bit A, which
|
||||
// a file may set on a reference or on the trailer alone.
|
||||
decode(self.header.encryption, &mut raw, key(stored_bid));
|
||||
}
|
||||
Ok(raw)
|
||||
}
|
||||
|
||||
/// The size of the data stream at `bid`, from its top block alone.
|
||||
pub(crate) fn data_size(&mut self, bid: u64) -> Result<u64> {
|
||||
if bid == 0 {
|
||||
return Ok(0);
|
||||
}
|
||||
if !is_internal(bid) {
|
||||
return Ok(u64::from(self.block_entry(bid)?.cb));
|
||||
}
|
||||
let top = self.block(bid)?;
|
||||
if u8_at(&top, 0)? != 1 {
|
||||
return Err(Error::Corrupt("a data tree's block type"));
|
||||
}
|
||||
Ok(u64::from(u32_at(&top, 4)?))
|
||||
}
|
||||
|
||||
/// The data blocks of the data tree at `bid`, in order, refused past `max` bytes.
|
||||
pub(crate) fn data_blocks(&mut self, bid: u64, max: u64, cap: Cap) -> Result<(Vec<u64>, u64)> {
|
||||
if bid == 0 {
|
||||
return Ok((Vec::new(), 0));
|
||||
}
|
||||
if !is_internal(bid) {
|
||||
let cb = u64::from(self.block_entry(bid)?.cb);
|
||||
if cb > max {
|
||||
return Err(Error::Cap(cap));
|
||||
}
|
||||
return Ok((vec![bid], cb));
|
||||
}
|
||||
let f = self.format();
|
||||
let id = f.id_size();
|
||||
let top = self.block(bid)?;
|
||||
if u8_at(&top, 0)? != 1 {
|
||||
return Err(Error::Corrupt("a data tree's block type"));
|
||||
}
|
||||
let level = u8_at(&top, 1)?;
|
||||
let total = u64::from(u32_at(&top, 4)?);
|
||||
if total > max {
|
||||
return Err(Error::Cap(cap));
|
||||
}
|
||||
let allowed = usize::try_from(total / MIN_AVERAGE_BLOCK).unwrap_or(usize::MAX).saturating_add(8);
|
||||
let ids = |b: &[u8]| -> Result<Vec<u64>> {
|
||||
let n = usize::from(u16_at(b, 2)?);
|
||||
if n > allowed {
|
||||
return Err(Error::Cap(Cap::DataBlocks));
|
||||
}
|
||||
(0..n).map(|i| f.read_id(b, i.saturating_mul(id).saturating_add(8))).collect()
|
||||
};
|
||||
let mut out = Vec::new();
|
||||
// A block named twice in one data tree would be read twice: refused.
|
||||
let mut seen = HashSet::new();
|
||||
let mut once = |bid: u64| if seen.insert(key(bid)) { Ok(()) } else { Err(Error::Cycle("a data tree")) };
|
||||
match level {
|
||||
1 => {
|
||||
for child in ids(&top)? {
|
||||
once(child)?;
|
||||
if is_internal(child) {
|
||||
return Err(Error::Corrupt("an XBLOCK pointing at an internal block"));
|
||||
}
|
||||
out.push(child);
|
||||
}
|
||||
}
|
||||
2 => {
|
||||
for child in ids(&top)? {
|
||||
once(child)?;
|
||||
if !is_internal(child) {
|
||||
return Err(Error::Corrupt("an XXBLOCK pointing at a data block"));
|
||||
}
|
||||
let x = self.block(child)?;
|
||||
if u8_at(&x, 0)? != 1 || u8_at(&x, 1)? != 1 {
|
||||
return Err(Error::Corrupt("an XXBLOCK pointing at something else than an XBLOCK"));
|
||||
}
|
||||
for grandchild in ids(&x)? {
|
||||
once(grandchild)?;
|
||||
if is_internal(grandchild) {
|
||||
return Err(Error::Corrupt("an XBLOCK pointing at an internal block"));
|
||||
}
|
||||
if out.len() >= allowed {
|
||||
return Err(Error::Cap(Cap::DataBlocks));
|
||||
}
|
||||
out.push(grandchild);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => return Err(Error::Corrupt("a data tree's level")),
|
||||
}
|
||||
Ok((out, total))
|
||||
}
|
||||
|
||||
/// The whole data stream at `bid`, one piece per data block, refused past `max` bytes.
|
||||
pub(crate) fn read_pieces(&mut self, bid: u64, max: u64, cap: Cap) -> Result<Vec<Vec<u8>>> {
|
||||
let (blocks, total) = self.data_blocks(bid, max, cap)?;
|
||||
let mut pieces = Vec::with_capacity(blocks.len());
|
||||
let mut sum = 0u64;
|
||||
for b in blocks {
|
||||
let piece = self.block(b)?;
|
||||
sum = sum.saturating_add(piece.len() as u64);
|
||||
if sum > total {
|
||||
return Err(Error::Corrupt("a data tree longer than it says"));
|
||||
}
|
||||
pieces.push(piece);
|
||||
}
|
||||
if sum != total {
|
||||
return Err(Error::Corrupt("a data tree shorter than it says"));
|
||||
}
|
||||
Ok(pieces)
|
||||
}
|
||||
|
||||
/// The whole data stream at `bid` as one buffer, refused past `max` bytes.
|
||||
pub(crate) fn read_data(&mut self, bid: u64, max: u64, cap: Cap) -> Result<Vec<u8>> {
|
||||
Ok(self.read_pieces(bid, max, cap)?.concat())
|
||||
}
|
||||
|
||||
/// The subnodes of the subnode tree at `bid`.
|
||||
pub(crate) fn subnodes(&mut self, bid: u64) -> Result<Subnodes> {
|
||||
let mut out = Subnodes::new();
|
||||
if bid == 0 {
|
||||
return Ok(out);
|
||||
}
|
||||
if !is_internal(bid) {
|
||||
return Err(Error::Corrupt("a subnode tree in a data block"));
|
||||
}
|
||||
let top = self.block(bid)?;
|
||||
if u8_at(&top, 0)? != 2 {
|
||||
return Err(Error::Corrupt("a subnode tree's block type"));
|
||||
}
|
||||
match u8_at(&top, 1)? {
|
||||
0 => self.leaf_entries(&top, &mut out)?,
|
||||
1 => {
|
||||
let mut seen = HashSet::new();
|
||||
for child in self.intermediate_entries(&top)? {
|
||||
if !seen.insert(key(child)) {
|
||||
return Err(Error::Cycle("a subnode tree"));
|
||||
}
|
||||
if !is_internal(child) {
|
||||
return Err(Error::Corrupt("a subnode tree in a data block"));
|
||||
}
|
||||
let leaf = self.block(child)?;
|
||||
if u8_at(&leaf, 0)? != 2 || u8_at(&leaf, 1)? != 0 {
|
||||
return Err(Error::Corrupt("an SIBLOCK pointing at something else than an SLBLOCK"));
|
||||
}
|
||||
self.leaf_entries(&leaf, &mut out)?;
|
||||
}
|
||||
}
|
||||
_ => return Err(Error::Corrupt("a subnode tree's level")),
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn entries_start(&self) -> usize {
|
||||
match self.format() {
|
||||
Format::Unicode => 8,
|
||||
Format::Ansi => 4,
|
||||
}
|
||||
}
|
||||
|
||||
fn leaf_entries(&self, b: &[u8], out: &mut Subnodes) -> Result<()> {
|
||||
let f = self.format();
|
||||
let id = f.id_size();
|
||||
let size = id.saturating_mul(3);
|
||||
let n = usize::from(u16_at(b, 2)?);
|
||||
for i in 0..n {
|
||||
let at = i.saturating_mul(size).saturating_add(self.entries_start());
|
||||
let e = slice(b, at, size)?;
|
||||
let nid = f.read_id(e, 0)? as u32;
|
||||
let entry = SubEntry { data: f.read_id(e, id)?, sub: f.read_id(e, id.saturating_mul(2))? };
|
||||
if out.len() >= self.limits.max_subnodes {
|
||||
return Err(Error::Cap(Cap::Subnodes));
|
||||
}
|
||||
out.entry(nid).or_insert(entry);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn intermediate_entries(&self, b: &[u8]) -> Result<Vec<u64>> {
|
||||
let f = self.format();
|
||||
let id = f.id_size();
|
||||
let size = id.saturating_mul(2);
|
||||
let n = usize::from(u16_at(b, 2)?);
|
||||
(0..n).map(|i| f.read_id(b, i.saturating_mul(size).saturating_add(self.entries_start()).saturating_add(id))).collect()
|
||||
}
|
||||
|
||||
/// Every node of the node B-tree, in key order, until `f` returns false.
|
||||
/// A page below the root that can't be read, or that is met twice, is noted and skipped
|
||||
/// with what is under it; the root must be read.
|
||||
pub(crate) fn for_each_node(&mut self, f: &mut dyn FnMut(NodeEntry) -> bool) -> Result<()> {
|
||||
let root = self.header.nbt;
|
||||
let mut visited = HashSet::new();
|
||||
let mut stack: Vec<(Bref, Option<u8>)> = vec![(root, None)];
|
||||
while let Some((bref, expect)) = stack.pop() {
|
||||
if !visited.insert(bref.ib) {
|
||||
self.note(bref.ib, PTYPE_NBT, &Error::Cycle("the node tree"));
|
||||
continue;
|
||||
}
|
||||
if visited.len() > self.limits.max_btree_pages {
|
||||
return Err(Error::Cap(Cap::BTreePages));
|
||||
}
|
||||
let page = match self.page_at(bref, PTYPE_NBT, expect) {
|
||||
Ok(p) => p,
|
||||
// Skipped only when it is damage, and named; a cap or the reader under the
|
||||
// file failing ends the walk.
|
||||
Err(e) if bref != root && is_damage(&e) => {
|
||||
self.note(bref.ib, PTYPE_NBT, &e);
|
||||
continue;
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
if page.level == 0 {
|
||||
for i in 0..page.count {
|
||||
let e = self.node_entry(page.entry(i)?)?;
|
||||
if !f(e) {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let id = self.format().id_size();
|
||||
let below = page.level.checked_sub(1);
|
||||
// Children pushed last-first so that they come off the stack in key order.
|
||||
for i in (0..page.count).rev() {
|
||||
let child = self.format().read_bref(page.entry(i)?, id)?;
|
||||
stack.push((child, below));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn encryption(&self) -> Encryption {
|
||||
self.header.encryption
|
||||
}
|
||||
|
||||
pub(crate) fn file_size(&self) -> u64 {
|
||||
self.src.size()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,344 @@
|
||||
//! Property values: the types of [MS-OXCDATA] 2.11.1 as a .pst file stores them, single and
|
||||
//! multi-valued ([MS-PST] 2.3.3.4).
|
||||
|
||||
use crate::bytes::{slice, u16_at, u32_at, u64_at, utf16};
|
||||
use crate::error::{Error, Result};
|
||||
|
||||
/// Property types ([MS-OXCDATA] 2.11.1).
|
||||
pub mod types {
|
||||
pub const NULL: u16 = 0x0001;
|
||||
pub const INT16: u16 = 0x0002;
|
||||
pub const INT32: u16 = 0x0003;
|
||||
pub const FLOAT32: u16 = 0x0004;
|
||||
pub const FLOAT64: u16 = 0x0005;
|
||||
pub const CURRENCY: u16 = 0x0006;
|
||||
pub const APP_TIME: u16 = 0x0007;
|
||||
pub const ERROR: u16 = 0x000A;
|
||||
pub const BOOLEAN: u16 = 0x000B;
|
||||
pub const OBJECT: u16 = 0x000D;
|
||||
pub const INT64: u16 = 0x0014;
|
||||
pub const STRING8: u16 = 0x001E;
|
||||
pub const STRING: u16 = 0x001F;
|
||||
pub const TIME: u16 = 0x0040;
|
||||
pub const GUID: u16 = 0x0048;
|
||||
pub const SERVER_ID: u16 = 0x00FB;
|
||||
pub const RESTRICTION: u16 = 0x00FD;
|
||||
pub const RULE_ACTION: u16 = 0x00FE;
|
||||
pub const BINARY: u16 = 0x0102;
|
||||
/// Set on a type for its multi-valued form.
|
||||
pub const MULTIPLE: u16 = 0x1000;
|
||||
}
|
||||
|
||||
use types::{
|
||||
APP_TIME, BINARY, BOOLEAN, CURRENCY, ERROR, FLOAT32, FLOAT64, GUID, INT16, INT32, INT64, MULTIPLE, NULL, OBJECT, RESTRICTION, RULE_ACTION,
|
||||
SERVER_ID, STRING, STRING8, TIME,
|
||||
};
|
||||
|
||||
/// A property's value.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[non_exhaustive]
|
||||
pub enum PropValue {
|
||||
Null,
|
||||
Int16(i16),
|
||||
Int32(i32),
|
||||
Float32(f32),
|
||||
Float64(f64),
|
||||
/// Currency, in ten-thousandths.
|
||||
Currency(i64),
|
||||
/// An OLE automation date.
|
||||
AppTime(f64),
|
||||
Error(u32),
|
||||
Bool(bool),
|
||||
Int64(i64),
|
||||
/// A FILETIME: 100-nanosecond intervals since 1601-01-01 UTC.
|
||||
Time(u64),
|
||||
Guid([u8; 16]),
|
||||
String(String),
|
||||
/// Text in the item's code page, as stored (ANSI files, and some properties of Unicode ones).
|
||||
String8(Vec<u8>),
|
||||
Binary(Vec<u8>),
|
||||
/// An object in a subnode (an attached message's data): its subnode id and size.
|
||||
Object {
|
||||
nid: u32,
|
||||
size: u32,
|
||||
},
|
||||
MultiInt16(Vec<i16>),
|
||||
MultiInt32(Vec<i32>),
|
||||
MultiFloat32(Vec<f32>),
|
||||
MultiFloat64(Vec<f64>),
|
||||
MultiCurrency(Vec<i64>),
|
||||
MultiAppTime(Vec<f64>),
|
||||
MultiInt64(Vec<i64>),
|
||||
MultiTime(Vec<u64>),
|
||||
MultiGuid(Vec<[u8; 16]>),
|
||||
MultiString(Vec<String>),
|
||||
MultiString8(Vec<Vec<u8>>),
|
||||
MultiBinary(Vec<Vec<u8>>),
|
||||
/// Larger than [`crate::Limits::max_inline_value`]: not read with the rest; read it with
|
||||
/// [`crate::Pst::read_property`].
|
||||
Large {
|
||||
size: u64,
|
||||
},
|
||||
/// A type this reader doesn't know, as stored.
|
||||
Unknown {
|
||||
ty: u16,
|
||||
data: Vec<u8>,
|
||||
},
|
||||
/// A value that could not be read, and why: kept in its place so that it is accounted for,
|
||||
/// and named in the `left_out` of whatever holds it.
|
||||
Damaged {
|
||||
ty: u16,
|
||||
reason: crate::Error,
|
||||
},
|
||||
}
|
||||
|
||||
/// One property of an object.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Property {
|
||||
pub id: u16,
|
||||
pub ty: u16,
|
||||
pub value: PropValue,
|
||||
}
|
||||
|
||||
impl PropValue {
|
||||
/// The bytes it holds outside itself (its text, bytes or values).
|
||||
pub(crate) fn held(&self) -> usize {
|
||||
use std::mem::size_of_val;
|
||||
let vecs = |v: &[Vec<u8>]| v.iter().map(|b| size_of_val(b).saturating_add(b.capacity())).sum::<usize>();
|
||||
match self {
|
||||
PropValue::String(s) => s.capacity(),
|
||||
PropValue::String8(b) | PropValue::Binary(b) | PropValue::Unknown { data: b, .. } => b.capacity(),
|
||||
PropValue::MultiInt16(v) => size_of_val(v.as_slice()),
|
||||
PropValue::MultiInt32(v) => size_of_val(v.as_slice()),
|
||||
PropValue::MultiFloat32(v) => size_of_val(v.as_slice()),
|
||||
PropValue::MultiFloat64(v) | PropValue::MultiAppTime(v) => size_of_val(v.as_slice()),
|
||||
PropValue::MultiCurrency(v) | PropValue::MultiInt64(v) => size_of_val(v.as_slice()),
|
||||
PropValue::MultiTime(v) => size_of_val(v.as_slice()),
|
||||
PropValue::MultiGuid(v) => size_of_val(v.as_slice()),
|
||||
PropValue::MultiString(v) => v.iter().map(|s| size_of_val(s).saturating_add(s.capacity())).sum(),
|
||||
PropValue::MultiString8(v) | PropValue::MultiBinary(v) => vecs(v),
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Property {
|
||||
/// What keeping it costs: itself, and what it holds.
|
||||
pub(crate) fn kept(&self) -> u64 {
|
||||
std::mem::size_of::<Property>().saturating_add(self.value.held()) as u64
|
||||
}
|
||||
|
||||
/// The 32-bit property tag (id in the high half, type in the low).
|
||||
pub fn tag(&self) -> u32 {
|
||||
(u32::from(self.id) << 16) | u32::from(self.ty)
|
||||
}
|
||||
}
|
||||
|
||||
/// An object's properties, in the order of their ids.
|
||||
#[derive(Debug, Clone, Default, PartialEq)]
|
||||
pub struct Properties(pub Vec<Property>);
|
||||
|
||||
impl Properties {
|
||||
pub fn get(&self, id: u16) -> Option<&PropValue> {
|
||||
self.0.iter().find(|p| p.id == id).map(|p| &p.value)
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = &Property> {
|
||||
self.0.iter()
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
|
||||
/// A text property: Unicode, or 8-bit text read as Latin-1. Latin-1 is right only for
|
||||
/// part of the code pages an ANSI file may use; the import decodes [`PropValue::String8`]
|
||||
/// with the item's own code page (`PidTagMessageCodepage`, `PidTagInternetCodepage`)
|
||||
/// instead of calling this.
|
||||
pub fn text(&self, id: u16) -> Option<String> {
|
||||
match self.get(id)? {
|
||||
PropValue::String(s) => Some(s.clone()),
|
||||
PropValue::String8(b) => Some(b.iter().map(|&c| char::from(c)).collect()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The properties that could not be read ([`PropValue::Damaged`]): id and reason.
|
||||
pub fn damaged(&self) -> impl Iterator<Item = (u16, &crate::Error)> {
|
||||
self.0.iter().filter_map(|p| match &p.value {
|
||||
PropValue::Damaged { reason, .. } => Some((p.id, reason)),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn int(&self, id: u16) -> Option<i64> {
|
||||
match self.get(id)? {
|
||||
PropValue::Int16(v) => Some(i64::from(*v)),
|
||||
PropValue::Int32(v) => Some(i64::from(*v)),
|
||||
PropValue::Int64(v) => Some(*v),
|
||||
PropValue::Error(v) => Some(i64::from(*v)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bool(&self, id: u16) -> Option<bool> {
|
||||
match self.get(id)? {
|
||||
PropValue::Bool(v) => Some(*v),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn binary(&self, id: u16) -> Option<&[u8]> {
|
||||
match self.get(id)? {
|
||||
PropValue::Binary(v) => Some(v),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn time(&self, id: u16) -> Option<u64> {
|
||||
match self.get(id)? {
|
||||
PropValue::Time(v) => Some(*v),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The size of a fixed-size type, `None` for a variable-size one.
|
||||
pub(crate) fn fixed_size(ty: u16) -> Option<usize> {
|
||||
match ty {
|
||||
NULL => Some(0),
|
||||
BOOLEAN => Some(1),
|
||||
INT16 => Some(2),
|
||||
INT32 | FLOAT32 | ERROR => Some(4),
|
||||
FLOAT64 | CURRENCY | APP_TIME | INT64 | TIME => Some(8),
|
||||
GUID => Some(16),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// A fixed-size value from exactly its bytes (or more: the first are read).
|
||||
pub(crate) fn fixed(ty: u16, b: &[u8]) -> Result<PropValue> {
|
||||
Ok(match ty {
|
||||
NULL => PropValue::Null,
|
||||
BOOLEAN => PropValue::Bool(b.first().copied().ok_or(Error::Corrupt("a short value"))? != 0),
|
||||
INT16 => PropValue::Int16(u16_at(b, 0)? as i16),
|
||||
INT32 => PropValue::Int32(u32_at(b, 0)? as i32),
|
||||
FLOAT32 => PropValue::Float32(f32::from_bits(u32_at(b, 0)?)),
|
||||
ERROR => PropValue::Error(u32_at(b, 0)?),
|
||||
FLOAT64 => PropValue::Float64(f64::from_bits(u64_at(b, 0)?)),
|
||||
CURRENCY => PropValue::Currency(u64_at(b, 0)? as i64),
|
||||
APP_TIME => PropValue::AppTime(f64::from_bits(u64_at(b, 0)?)),
|
||||
INT64 => PropValue::Int64(u64_at(b, 0)? as i64),
|
||||
TIME => PropValue::Time(u64_at(b, 0)?),
|
||||
GUID => PropValue::Guid(crate::bytes::array16(b, 0)?),
|
||||
other => PropValue::Unknown { ty: other, data: b.to_vec() },
|
||||
})
|
||||
}
|
||||
|
||||
/// What building a value from its `b` bytes holds beyond them, at most: for a list of
|
||||
/// strings or binaries, each item's own vector and its place while the list is made (an
|
||||
/// empty item is four bytes in the file and far more in memory). Other values hold no more
|
||||
/// than their bytes.
|
||||
pub(crate) fn building_cost(ty: u16, b: &[u8]) -> usize {
|
||||
let variable = matches!(ty, t if t == STRING | MULTIPLE || t == STRING8 | MULTIPLE || t == BINARY | MULTIPLE);
|
||||
if !variable {
|
||||
return 0;
|
||||
}
|
||||
let count = u32_at(b, 0).map_or(0, |c| (c as usize).min(b.len() / 4));
|
||||
count.saturating_mul(std::mem::size_of::<Vec<u8>>().saturating_add(std::mem::size_of::<&[u8]>()))
|
||||
}
|
||||
|
||||
/// A value stored apart (in the heap or a subnode), from all its bytes.
|
||||
pub(crate) fn stored(ty: u16, b: &[u8]) -> Result<PropValue> {
|
||||
if ty & MULTIPLE != 0 {
|
||||
return multi(ty & !MULTIPLE, b);
|
||||
}
|
||||
Ok(match ty {
|
||||
STRING => PropValue::String(utf16(b)),
|
||||
STRING8 => PropValue::String8(trim_nul(b).to_vec()),
|
||||
BINARY | SERVER_ID | RESTRICTION | RULE_ACTION => PropValue::Binary(b.to_vec()),
|
||||
OBJECT => PropValue::Object { nid: u32_at(b, 0)?, size: u32_at(b, 4)? },
|
||||
t if fixed_size(t).is_some() => fixed(t, b)?,
|
||||
other => PropValue::Unknown { ty: other, data: b.to_vec() },
|
||||
})
|
||||
}
|
||||
|
||||
fn trim_nul(b: &[u8]) -> &[u8] {
|
||||
let mut end = b.len();
|
||||
while end > 0 && b.get(end.saturating_sub(1)) == Some(&0) {
|
||||
end = end.saturating_sub(1);
|
||||
}
|
||||
b.get(..end).unwrap_or(b)
|
||||
}
|
||||
|
||||
fn multi(base: u16, b: &[u8]) -> Result<PropValue> {
|
||||
if let Some(size) = fixed_size(base).filter(|&s| s > 1) {
|
||||
if !b.len().is_multiple_of(size) {
|
||||
return Err(Error::Corrupt("a multi-valued property's size"));
|
||||
}
|
||||
// Each value straight into its own type: no value is held twice on the way.
|
||||
return Ok(match base {
|
||||
INT16 => PropValue::MultiInt16(values(b, size, |c| Ok(u16_at(c, 0)? as i16))?),
|
||||
INT32 => PropValue::MultiInt32(values(b, size, |c| Ok(u32_at(c, 0)? as i32))?),
|
||||
FLOAT32 => PropValue::MultiFloat32(values(b, size, |c| Ok(f32::from_bits(u32_at(c, 0)?)))?),
|
||||
FLOAT64 => PropValue::MultiFloat64(values(b, size, |c| Ok(f64::from_bits(u64_at(c, 0)?)))?),
|
||||
CURRENCY => PropValue::MultiCurrency(values(b, size, |c| Ok(u64_at(c, 0)? as i64))?),
|
||||
APP_TIME => PropValue::MultiAppTime(values(b, size, |c| Ok(f64::from_bits(u64_at(c, 0)?)))?),
|
||||
INT64 => PropValue::MultiInt64(values(b, size, |c| Ok(u64_at(c, 0)? as i64))?),
|
||||
TIME => PropValue::MultiTime(values(b, size, |c| u64_at(c, 0))?),
|
||||
GUID => PropValue::MultiGuid(values(b, size, |c| crate::bytes::array16(c, 0))?),
|
||||
_ => PropValue::Unknown { ty: base | MULTIPLE, data: b.to_vec() },
|
||||
});
|
||||
}
|
||||
// Only the lists of strings and binaries have their items read; any other is kept as bytes.
|
||||
if !matches!(base, STRING | STRING8 | BINARY) {
|
||||
return Ok(PropValue::Unknown { ty: base | MULTIPLE, data: b.to_vec() });
|
||||
}
|
||||
let items = variable_items(b)?;
|
||||
Ok(match base {
|
||||
STRING => PropValue::MultiString(items.into_iter().map(utf16).collect()),
|
||||
STRING8 => PropValue::MultiString8(items.into_iter().map(|i| trim_nul(i).to_vec()).collect()),
|
||||
_ => PropValue::MultiBinary(items.into_iter().map(<[u8]>::to_vec).collect()),
|
||||
})
|
||||
}
|
||||
|
||||
/// The items of a multi-valued property of a variable-size type (2.3.3.4.2).
|
||||
/// Each `size` bytes of `b` straight into a value of its own type: none is held twice.
|
||||
fn values<T>(b: &[u8], size: usize, f: impl Fn(&[u8]) -> Result<T>) -> Result<Vec<T>> {
|
||||
let mut out = Vec::with_capacity(b.len().checked_div(size).unwrap_or(0));
|
||||
for c in b.chunks_exact(size) {
|
||||
out.push(f(c)?);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn variable_items(b: &[u8]) -> Result<Vec<&[u8]>> {
|
||||
if b.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let count = u32_at(b, 0)? as usize;
|
||||
// Each item has its 4-byte offset, so the count can't pass a quarter of the size.
|
||||
if count > b.len() / 4 {
|
||||
return Err(Error::Corrupt("a multi-valued property's count"));
|
||||
}
|
||||
let table_end = count.saturating_mul(4).saturating_add(4);
|
||||
let offset = |i: usize| -> Result<usize> {
|
||||
if i >= count {
|
||||
return Ok(b.len());
|
||||
}
|
||||
Ok(u32_at(b, i.saturating_mul(4).saturating_add(4))? as usize)
|
||||
};
|
||||
let mut items = Vec::with_capacity(count);
|
||||
for i in 0..count {
|
||||
let (start, end) = (offset(i)?, offset(i.saturating_add(1))?);
|
||||
if start < table_end || end < start || end > b.len() {
|
||||
return Err(Error::Corrupt("a multi-valued property's offsets"));
|
||||
}
|
||||
items.push(slice(b, start, end.saturating_sub(start))?);
|
||||
}
|
||||
Ok(items)
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,268 @@
|
||||
//! How an appointment repeats, as Outlook keeps it in PidLidAppointmentRecur: the
|
||||
//! AppointmentRecurrencePattern and RecurrencePattern of [MS-OXOCAL] 2.2.1.44, and the time zone
|
||||
//! of PidLidAppointmentTimeZoneDefinitionRecur (TZDEFINITION, 2.2.1.41).
|
||||
//!
|
||||
//! The bytes are hostile: each count is checked against the bytes there are, nothing is
|
||||
//! allocated by what the blob says, and a part past its end is an error. The extended
|
||||
//! exceptions, which only add the wide-character subject and location, may be missing or
|
||||
//! damaged without the rest being lost.
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
|
||||
/// Most exceptions kept of a series.
|
||||
pub const MAX_EXCEPTIONS: usize = 5_000;
|
||||
/// Longest a time zone's key name may be, in UTF-16 units.
|
||||
const MAX_ZONE_KEY: usize = 256;
|
||||
|
||||
/// RecurFrequency.
|
||||
pub const FREQ_DAILY: u16 = 0x200A;
|
||||
pub const FREQ_WEEKLY: u16 = 0x200B;
|
||||
pub const FREQ_MONTHLY: u16 = 0x200C;
|
||||
pub const FREQ_YEARLY: u16 = 0x200D;
|
||||
/// PatternType.
|
||||
pub const PATTERN_DAY: u16 = 0;
|
||||
pub const PATTERN_WEEK: u16 = 1;
|
||||
pub const PATTERN_MONTH: u16 = 2;
|
||||
pub const PATTERN_MONTH_NTH: u16 = 3;
|
||||
pub const PATTERN_MONTH_END: u16 = 4;
|
||||
/// EndType: on a date, after a number of occurrences. Anything else never ends.
|
||||
pub const END_DATE: u32 = 0x2021;
|
||||
pub const END_COUNT: u32 = 0x2022;
|
||||
|
||||
const ARO_SUBJECT: u16 = 0x0001;
|
||||
const ARO_MEETINGTYPE: u16 = 0x0002;
|
||||
const ARO_REMINDERDELTA: u16 = 0x0004;
|
||||
const ARO_REMINDER: u16 = 0x0008;
|
||||
const ARO_LOCATION: u16 = 0x0010;
|
||||
const ARO_BUSYSTATUS: u16 = 0x0020;
|
||||
const ARO_ATTACHMENT: u16 = 0x0040;
|
||||
const ARO_SUBTYPE: u16 = 0x0080;
|
||||
const ARO_APPTCOLOR: u16 = 0x0100;
|
||||
|
||||
/// One occurrence of a series that was changed. Times are minutes since 1601 by the clock of
|
||||
/// the series' zone.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct RecurException {
|
||||
pub start: u32,
|
||||
pub end: u32,
|
||||
/// When it would have started.
|
||||
pub original: u32,
|
||||
/// In the item's code page.
|
||||
pub subject_8bit: Option<Vec<u8>>,
|
||||
pub location_8bit: Option<Vec<u8>>,
|
||||
/// From the extended exceptions.
|
||||
pub subject: Option<String>,
|
||||
pub location: Option<String>,
|
||||
pub reminder_delta: Option<u32>,
|
||||
pub reminder_set: Option<bool>,
|
||||
pub busy: Option<u32>,
|
||||
pub all_day: Option<bool>,
|
||||
}
|
||||
|
||||
/// A repeating appointment. Dates are minutes since 1601, at midnight by the series' clock.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct Recur {
|
||||
pub frequency: u16,
|
||||
pub pattern: u16,
|
||||
/// 0 and 1 are the Gregorian calendar.
|
||||
pub calendar: u16,
|
||||
/// Minutes for a daily series, weeks for a weekly one, months for the others.
|
||||
pub period: u32,
|
||||
/// The days of the week (bit 0 is Sunday), for a weekly or nth-weekday series.
|
||||
pub weekdays: u32,
|
||||
/// The day of the month, or which of the weekdays (1 to 4, 5 for the last).
|
||||
pub day_or_nth: u32,
|
||||
pub end_type: u32,
|
||||
pub count: u32,
|
||||
/// The day weeks start on (0 is Sunday): what an interval of weeks is counted by.
|
||||
pub first_day_of_week: u32,
|
||||
/// Days that do not happen as the series says: deleted, and changed ones too.
|
||||
pub deleted: Vec<u32>,
|
||||
pub modified: Vec<u32>,
|
||||
pub start_date: u32,
|
||||
pub end_date: u32,
|
||||
/// Minutes from midnight to the start and to the end of an occurrence.
|
||||
pub start_offset: u32,
|
||||
pub end_offset: u32,
|
||||
pub exceptions: Vec<RecurException>,
|
||||
}
|
||||
|
||||
struct Cursor<'a> {
|
||||
b: &'a [u8],
|
||||
at: usize,
|
||||
}
|
||||
|
||||
const CUT: Error = Error::Corrupt("recurrence cut short");
|
||||
|
||||
impl<'a> Cursor<'a> {
|
||||
fn take(&mut self, n: usize) -> Result<&'a [u8]> {
|
||||
let end = self.at.checked_add(n).ok_or(CUT)?;
|
||||
let s = self.b.get(self.at..end).ok_or(CUT)?;
|
||||
self.at = end;
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
fn u16(&mut self) -> Result<u16> {
|
||||
let s = self.take(2)?;
|
||||
Ok(u16::from_le_bytes(<[u8; 2]>::try_from(s).map_err(|_| CUT)?))
|
||||
}
|
||||
|
||||
fn u32(&mut self) -> Result<u32> {
|
||||
let s = self.take(4)?;
|
||||
Ok(u32::from_le_bytes(<[u8; 4]>::try_from(s).map_err(|_| CUT)?))
|
||||
}
|
||||
|
||||
fn left(&self) -> usize {
|
||||
self.b.len().saturating_sub(self.at)
|
||||
}
|
||||
|
||||
/// A list of `u32`s after its count, the count checked against the bytes there are.
|
||||
fn dates(&mut self) -> Result<Vec<u32>> {
|
||||
let n = usize::try_from(self.u32()?).map_err(|_| CUT)?;
|
||||
if n > self.left() / 4 {
|
||||
return Err(CUT);
|
||||
}
|
||||
(0..n).map(|_| self.u32()).collect()
|
||||
}
|
||||
|
||||
/// A block a size is written before, skipped.
|
||||
fn skip_block(&mut self) -> Result<()> {
|
||||
let n = usize::try_from(self.u32()?).map_err(|_| CUT)?;
|
||||
self.take(n).map(|_| ())
|
||||
}
|
||||
|
||||
fn wide(&mut self) -> Result<String> {
|
||||
let n = usize::from(self.u16()?);
|
||||
let s = self.take(n.checked_mul(2).ok_or(CUT)?)?;
|
||||
let units = s.as_chunks::<2>().0.iter().map(|c| u16::from_le_bytes(*c));
|
||||
Ok(char::decode_utf16(units).map(|c| c.unwrap_or('\u{FFFD}')).collect())
|
||||
}
|
||||
|
||||
/// An exception's 8-bit text: its length plus one, its length, then the bytes.
|
||||
fn text_8bit(&mut self) -> Result<Vec<u8>> {
|
||||
let _with_end = self.u16()?;
|
||||
let n = usize::from(self.u16()?);
|
||||
Ok(self.take(n)?.to_vec())
|
||||
}
|
||||
}
|
||||
|
||||
fn exception(c: &mut Cursor) -> Result<(RecurException, u16)> {
|
||||
let mut e = RecurException { start: c.u32()?, end: c.u32()?, original: c.u32()?, ..Default::default() };
|
||||
let flags = c.u16()?;
|
||||
if flags & ARO_SUBJECT != 0 {
|
||||
e.subject_8bit = Some(c.text_8bit()?);
|
||||
}
|
||||
if flags & ARO_MEETINGTYPE != 0 {
|
||||
c.u32()?;
|
||||
}
|
||||
if flags & ARO_REMINDERDELTA != 0 {
|
||||
e.reminder_delta = Some(c.u32()?);
|
||||
}
|
||||
if flags & ARO_REMINDER != 0 {
|
||||
e.reminder_set = Some(c.u32()? != 0);
|
||||
}
|
||||
if flags & ARO_LOCATION != 0 {
|
||||
e.location_8bit = Some(c.text_8bit()?);
|
||||
}
|
||||
if flags & ARO_BUSYSTATUS != 0 {
|
||||
e.busy = Some(c.u32()?);
|
||||
}
|
||||
if flags & ARO_ATTACHMENT != 0 {
|
||||
c.u32()?;
|
||||
}
|
||||
if flags & ARO_SUBTYPE != 0 {
|
||||
e.all_day = Some(c.u32()? != 0);
|
||||
}
|
||||
if flags & ARO_APPTCOLOR != 0 {
|
||||
c.u32()?;
|
||||
}
|
||||
Ok((e, flags))
|
||||
}
|
||||
|
||||
/// One extended exception: the wide-character subject and location of the exception before it.
|
||||
fn extended(c: &mut Cursor, writer: u32, e: &mut RecurException, flags: u16) -> Result<()> {
|
||||
if writer >= 0x3009 {
|
||||
c.skip_block()?;
|
||||
}
|
||||
c.skip_block()?;
|
||||
if flags & (ARO_SUBJECT | ARO_LOCATION) != 0 {
|
||||
c.take(12)?;
|
||||
if flags & ARO_SUBJECT != 0 {
|
||||
e.subject = Some(c.wide()?);
|
||||
}
|
||||
if flags & ARO_LOCATION != 0 {
|
||||
e.location = Some(c.wide()?);
|
||||
}
|
||||
c.skip_block()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The series in an AppointmentRecurrencePattern.
|
||||
pub fn appointment_recurrence(blob: &[u8]) -> Result<Recur> {
|
||||
let mut c = Cursor { b: blob, at: 0 };
|
||||
if c.u16()? != 0x3004 || c.u16()? != 0x3004 {
|
||||
return Err(Error::Corrupt("recurrence of a version not known"));
|
||||
}
|
||||
let mut r = Recur { frequency: c.u16()?, pattern: c.u16()?, calendar: c.u16()?, ..Default::default() };
|
||||
let _first_date_time = c.u32()?;
|
||||
r.period = c.u32()?;
|
||||
let _sliding = c.u32()?;
|
||||
match r.pattern {
|
||||
PATTERN_DAY => {}
|
||||
PATTERN_WEEK => r.weekdays = c.u32()?,
|
||||
// (The Hijri and Hebrew patterns have the shape of these.)
|
||||
PATTERN_MONTH | PATTERN_MONTH_END | 0x0A | 0x0C => r.day_or_nth = c.u32()?,
|
||||
PATTERN_MONTH_NTH | 0x0B => {
|
||||
r.weekdays = c.u32()?;
|
||||
r.day_or_nth = c.u32()?;
|
||||
}
|
||||
_ => return Err(Error::Corrupt("recurrence of a pattern not known")),
|
||||
}
|
||||
r.end_type = c.u32()?;
|
||||
r.count = c.u32()?;
|
||||
r.first_day_of_week = c.u32()?;
|
||||
r.deleted = c.dates()?;
|
||||
r.modified = c.dates()?;
|
||||
r.start_date = c.u32()?;
|
||||
r.end_date = c.u32()?;
|
||||
let _reader = c.u32()?;
|
||||
let writer = c.u32()?;
|
||||
r.start_offset = c.u32()?;
|
||||
r.end_offset = c.u32()?;
|
||||
let count = usize::from(c.u16()?);
|
||||
if count > MAX_EXCEPTIONS {
|
||||
return Err(Error::Corrupt("recurrence with too many exceptions"));
|
||||
}
|
||||
let mut flags = Vec::new();
|
||||
for _ in 0..count {
|
||||
let (e, f) = exception(&mut c)?;
|
||||
r.exceptions.push(e);
|
||||
flags.push(f);
|
||||
}
|
||||
// The wide-character texts add to what is read: without them the 8-bit ones stand.
|
||||
if c.skip_block().is_ok() {
|
||||
for (e, f) in r.exceptions.iter_mut().zip(flags) {
|
||||
if extended(&mut c, writer, e, f).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
/// The Windows name of the time zone in a TZDEFINITION (`W. Europe Standard Time`).
|
||||
pub fn time_zone_key(blob: &[u8]) -> Result<String> {
|
||||
let mut c = Cursor { b: blob, at: 0 };
|
||||
let head = c.take(2)?;
|
||||
if head.first() != Some(&2) {
|
||||
return Err(Error::Corrupt("time zone of a version not known"));
|
||||
}
|
||||
let _header_size = c.u16()?;
|
||||
let _reserved = c.u16()?;
|
||||
let key = c.wide()?;
|
||||
if key.is_empty() || key.chars().count() > MAX_ZONE_KEY {
|
||||
return Err(Error::Corrupt("time zone without a usable name"));
|
||||
}
|
||||
Ok(key)
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
//! The file under the reader: any `Read + Seek`, read a page or a block at a time and never
|
||||
//! whole. Every read is checked against the file's size before anything is allocated for it.
|
||||
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
|
||||
use crate::error::{Cap, Error, Result};
|
||||
|
||||
/// The largest single read: a block ([MS-PST] 2.2.2.8: at most 8 KB).
|
||||
pub(crate) const MAX_READ: usize = 8192;
|
||||
|
||||
pub(crate) struct Source<R> {
|
||||
inner: R,
|
||||
size: u64,
|
||||
/// Bytes read so far, and how many may be ([`crate::Limits::read_multiple`]).
|
||||
read: u64,
|
||||
allowed: u64,
|
||||
}
|
||||
|
||||
impl<R: Read + Seek> Source<R> {
|
||||
pub(crate) fn new(mut inner: R, limits: &crate::Limits) -> Result<Self> {
|
||||
let size = inner.seek(SeekFrom::End(0))?;
|
||||
if size > limits.max_file_size {
|
||||
return Err(Error::Cap(Cap::FileSize));
|
||||
}
|
||||
let allowed = size.saturating_mul(limits.read_multiple).saturating_add(limits.read_floor);
|
||||
Ok(Source { inner, size, read: 0, allowed })
|
||||
}
|
||||
|
||||
pub(crate) fn size(&self) -> u64 {
|
||||
self.size
|
||||
}
|
||||
|
||||
/// `len` bytes at `offset`, all of them inside the file.
|
||||
pub(crate) fn read_at(&mut self, offset: u64, len: usize) -> Result<Vec<u8>> {
|
||||
if len > MAX_READ {
|
||||
return Err(Error::Corrupt("a read larger than a block"));
|
||||
}
|
||||
// Counted before it is checked, so that reads past the end aren't free either.
|
||||
self.read = self.read.saturating_add(len as u64);
|
||||
if self.read > self.allowed {
|
||||
return Err(Error::Cap(Cap::TotalRead));
|
||||
}
|
||||
let end = offset.checked_add(len as u64).ok_or(Error::OutOfBounds)?;
|
||||
if end > self.size {
|
||||
return Err(Error::OutOfBounds);
|
||||
}
|
||||
// Within the file's size, so a short read is the reader failing (the file cut short or
|
||||
// changed under the import), not damage to the file.
|
||||
let io = |e: std::io::Error| Error::Io(e.kind());
|
||||
self.inner.seek(SeekFrom::Start(offset)).map_err(io)?;
|
||||
let mut buf = vec![0u8; len];
|
||||
self.inner.read_exact(&mut buf).map_err(io)?;
|
||||
Ok(buf)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
//! Nothing that is left out vanishes: a damaged property, row or name is named with its
|
||||
//! reason. And the file variants the reader is tolerant of, or refuses in words: the reserved
|
||||
//! bit A of block ids, Windows-1252 passwords, versions 36 and 37.
|
||||
|
||||
use std::io::Cursor;
|
||||
|
||||
use super::writer::{self, Data, Layout, binary, build, hnid_ref, int32, nodes, raw, tc};
|
||||
use super::{first_item, note, pattern, sample};
|
||||
use crate::crc::crc32;
|
||||
use crate::prop::types::BINARY;
|
||||
use crate::{Encryption, Error, Format, MessageRef, Nid, Part, PropValue, Pst};
|
||||
|
||||
fn open(bytes: &[u8]) -> Pst<Cursor<&[u8]>> {
|
||||
Pst::open(Cursor::new(bytes)).unwrap()
|
||||
}
|
||||
|
||||
/// A subnode id that names nothing.
|
||||
const NOWHERE: u32 = (0x7777 << 5) | 0x1F;
|
||||
|
||||
fn inbox(pst: &mut Pst<Cursor<&[u8]>>) -> Nid {
|
||||
pst.walk_folders().unwrap().folders.iter().find(|f| f.folder.name == "Inbox").unwrap().folder.id
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_damaged_property_of_a_message_is_named_in_left_out() {
|
||||
let mut spec = sample(Layout::new(Format::Unicode, Encryption::Permute), None);
|
||||
let mut m = note("A property that points nowhere");
|
||||
m.extra = vec![(0x6001, hnid_ref(BINARY, NOWHERE)), (0x6002, binary(b"fine"))];
|
||||
spec.folders[0].messages = vec![m];
|
||||
let bytes = build(&spec);
|
||||
let mut pst = open(&bytes);
|
||||
let item = first_item(&mut pst);
|
||||
let m = pst.message(&MessageRef::item(item)).unwrap();
|
||||
assert!(matches!(m.properties.get(0x6001), Some(PropValue::Damaged { ty: BINARY, reason: Error::NotFound(_) })));
|
||||
assert_eq!(m.properties.get(0x6002), Some(&PropValue::Binary(b"fine".to_vec())));
|
||||
assert_eq!(m.left_out.len(), 1);
|
||||
assert_eq!(m.left_out[0].part, Part::Property { id: 0x6001 });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_variable_cell_too_short_for_its_reference_loses_that_property_only() {
|
||||
let spec = sample(Layout::new(Format::Unicode, Encryption::None), None);
|
||||
let mut all = nodes(&spec);
|
||||
let (tag, number) = (0x6100_0102u32, 0x6101_0003u32);
|
||||
let rows = vec![(((0x20000) << 5) | 4, vec![(tag, binary(b"ok")), (number, int32(7))])];
|
||||
let (mut blocks, subs) = tc(&spec.layout, &[tag, number], &rows);
|
||||
// The binary column's description (tag, offset, width, bit): its width cut to 2 bytes.
|
||||
let heap = &mut blocks[0];
|
||||
let at = heap.windows(4).position(|w| w == tag.to_le_bytes()).unwrap();
|
||||
assert_eq!(heap[at + 6], 4);
|
||||
heap[at + 6] = 2;
|
||||
let table = all.iter_mut().find(|n| n.nid == (0x403 << 5) | 0x0E).unwrap();
|
||||
table.data = Data::Blocks(blocks);
|
||||
table.subnodes = subs;
|
||||
let bytes = raw(&spec.layout, &all);
|
||||
let mut pst = open(&bytes);
|
||||
let folder = inbox(&mut pst);
|
||||
let list = pst.items(folder).unwrap();
|
||||
assert_eq!(list.items.len(), 1);
|
||||
let props = &list.items[0].properties;
|
||||
assert_eq!(props.get(0x6101), Some(&PropValue::Int32(7)), "the rest of the row is read");
|
||||
assert!(matches!(props.get(0x6100), Some(PropValue::Damaged { .. })));
|
||||
assert_eq!(list.left_out.len(), 1);
|
||||
assert_eq!((list.left_out[0].property, &list.left_out[0].reason), (Some(0x6100), &Error::Corrupt("a structure runs past its end")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_damaged_cell_of_an_item_list_is_named_in_left_out() {
|
||||
let spec = sample(Layout::new(Format::Unicode, Encryption::None), None);
|
||||
let mut all = nodes(&spec);
|
||||
let tag = 0x6100_0102u32;
|
||||
let rows = vec![(((0x20000) << 5) | 4, vec![(tag, hnid_ref(BINARY, NOWHERE))]), (((0x20001) << 5) | 4, vec![(tag, binary(b"ok"))])];
|
||||
let (blocks, subs) = tc(&spec.layout, &[tag], &rows);
|
||||
let table = all.iter_mut().find(|n| n.nid == (0x403 << 5) | 0x0E).unwrap();
|
||||
table.data = Data::Blocks(blocks);
|
||||
table.subnodes = subs;
|
||||
let bytes = raw(&spec.layout, &all);
|
||||
let mut pst = open(&bytes);
|
||||
let folder = inbox(&mut pst);
|
||||
let list = pst.items(folder).unwrap();
|
||||
assert_eq!(list.items.len(), 2);
|
||||
assert_eq!(list.left_out.len(), 1);
|
||||
let l = &list.left_out[0];
|
||||
assert_eq!((l.id, l.parent, l.property), (Nid(((0x20000) << 5) | 4), Some(folder), Some(0x6100)));
|
||||
assert!(matches!(l.reason, Error::NotFound(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_row_that_cannot_be_read_is_listed_and_named() {
|
||||
// The row index names a row id that its row in the matrix doesn't have.
|
||||
let spec = sample(Layout::new(Format::Unicode, Encryption::None), None);
|
||||
let mut all = nodes(&spec);
|
||||
let rows = vec![(((0x20000) << 5) | 4, vec![(0x0E07_0003, int32(1))])];
|
||||
let (mut blocks, subs) = tc(&spec.layout, &[0x0E07_0003], &rows);
|
||||
// The matrix is the heap item holding the row id: change the id in it, not in the index.
|
||||
let id = (((0x20000u32) << 5) | 4).to_le_bytes();
|
||||
let block = &mut blocks[0];
|
||||
let at = block.windows(4).rposition(|w| w == id).unwrap();
|
||||
block[at] ^= 0x80;
|
||||
let table = all.iter_mut().find(|n| n.nid == (0x403 << 5) | 0x0E).unwrap();
|
||||
table.data = Data::Blocks(blocks);
|
||||
table.subnodes = subs;
|
||||
let bytes = raw(&spec.layout, &all);
|
||||
let mut pst = open(&bytes);
|
||||
let folder = inbox(&mut pst);
|
||||
let list = pst.items(folder).unwrap();
|
||||
assert_eq!(list.items.len(), 1);
|
||||
assert!(list.items[0].properties.is_empty());
|
||||
assert_eq!(list.left_out.len(), 1);
|
||||
assert_eq!(list.left_out[0].property, None);
|
||||
assert!(matches!(list.left_out[0].reason, Error::Corrupt(_) | Error::NotFound(_)), "{:?}", list.left_out[0].reason);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn passwords_are_also_tried_in_windows_1252() {
|
||||
for (password, ansi) in [("Müller", true), ("Müller", false), ("plain", true)] {
|
||||
let mut spec = sample(Layout::new(Format::Ansi, Encryption::Permute), Some(password));
|
||||
spec.password_ansi = ansi;
|
||||
let bytes = build(&spec);
|
||||
let mut pst = open(&bytes);
|
||||
assert!(pst.password_required());
|
||||
assert_eq!(pst.unlock("Muller"), Err(Error::WrongPassword));
|
||||
pst.unlock(password).unwrap();
|
||||
}
|
||||
// A character of the 0x80 to 0x9F range of the code page (the euro sign, 0x80).
|
||||
let mut spec = sample(Layout::new(Format::Unicode, Encryption::None), Some("x"));
|
||||
let mut all = nodes(&spec);
|
||||
spec.password = None;
|
||||
let mut spill = writer::Spill::new();
|
||||
let store = writer::pc(&spec.layout, &[(0x3001, writer::unicode("Personal Folders")), (0x67FF, int32(crc32(&[0x80, b'5']) as i32))], &mut spill);
|
||||
let n = all.iter_mut().find(|n| n.nid == 0x21).unwrap();
|
||||
n.data = Data::Blocks(store);
|
||||
let bytes = raw(&spec.layout, &all);
|
||||
let mut pst = open(&bytes);
|
||||
assert_eq!(pst.unlock("e5"), Err(Error::WrongPassword));
|
||||
pst.unlock("€5").unwrap();
|
||||
}
|
||||
|
||||
fn with_version(version: u16) -> Vec<u8> {
|
||||
let mut b = build(&sample(Layout::new(Format::Unicode, Encryption::None), None));
|
||||
b[10..12].copy_from_slice(&version.to_le_bytes());
|
||||
let full = crc32(&b[8..524]);
|
||||
b[524..528].copy_from_slice(&full.to_le_bytes());
|
||||
let partial = crc32(&b[8..479]);
|
||||
b[4..8].copy_from_slice(&partial.to_le_bytes());
|
||||
b
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offline_files_with_4k_pages_and_protected_files_are_refused_in_words() {
|
||||
for (version, words) in [(36u16, "4 KB pages"), (37, "Windows Information Protection")] {
|
||||
let b = with_version(version);
|
||||
let e = Pst::open(Cursor::new(&b[..])).err().unwrap();
|
||||
assert_eq!(e, Error::UnsupportedVersion(version));
|
||||
assert!(e.to_string().contains(words), "{e}");
|
||||
}
|
||||
assert!(Pst::open(Cursor::new(&with_version(24)[..])).is_ok(), "other Unicode versions are read");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_reserved_bit_a_of_block_ids_is_ignored() {
|
||||
for (refs, trailers) in [(true, false), (false, true), (true, true)] {
|
||||
for format in [Format::Unicode, Format::Ansi] {
|
||||
let layout = Layout { bit_a_in_references: refs, bit_a_in_trailers: trailers, ..Layout::tiny(format, Encryption::Cyclic) };
|
||||
let bytes = build(&sample(layout, None));
|
||||
let mut pst = open(&bytes);
|
||||
let what = format!("{format:?} refs {refs} trailers {trailers}");
|
||||
let tree = pst.walk_folders().unwrap();
|
||||
assert!(tree.left_out.is_empty(), "{what}: {:?}", tree.left_out);
|
||||
assert_eq!(tree.folders.len(), 7, "{what}");
|
||||
let item = pst.items(tree.folders[2].folder.id).unwrap().items[0].id;
|
||||
let m = pst.message(&MessageRef::item(item)).unwrap();
|
||||
assert!(m.left_out.is_empty(), "{what}: {:?}", m.left_out);
|
||||
assert_eq!(pst.attachment_bytes(&m.attachments[0].id).unwrap(), pattern(20_000, 7), "{what}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
//! Each cap of [`Limits`]: past it, the part is left out and named, and nothing panics.
|
||||
|
||||
use std::io::{self, Cursor, Read, Seek, SeekFrom};
|
||||
|
||||
use super::writer::{AttachSpec, Data, FolderSpec, Layout, Node, build, raw};
|
||||
use super::{note, person, sample};
|
||||
use crate::{AttachmentContent, Cap, Encryption, Error, Format, Limits, MessageRef, Nid, Part, Pst};
|
||||
|
||||
fn open(bytes: &[u8], limits: Limits) -> Pst<Cursor<&[u8]>> {
|
||||
Pst::open_with(Cursor::new(bytes), limits).unwrap()
|
||||
}
|
||||
|
||||
fn unicode() -> Layout {
|
||||
Layout::new(Format::Unicode, Encryption::Permute)
|
||||
}
|
||||
|
||||
/// A reader of a file that says it is 200 GB and holds nothing but zeros.
|
||||
struct Huge(u64);
|
||||
|
||||
impl Read for Huge {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
buf.fill(0);
|
||||
Ok(buf.len())
|
||||
}
|
||||
}
|
||||
|
||||
impl Seek for Huge {
|
||||
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
|
||||
self.0 = match pos {
|
||||
SeekFrom::End(_) => 200 * 1024 * 1024 * 1024,
|
||||
SeekFrom::Start(n) => n,
|
||||
SeekFrom::Current(n) => self.0.saturating_add_signed(n),
|
||||
};
|
||||
Ok(self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_file_over_100_gb_is_refused_before_it_is_read() {
|
||||
assert_eq!(Pst::open(Huge(0)).err(), Some(Error::Cap(Cap::FileSize)));
|
||||
let bytes = build(&sample(unicode(), None));
|
||||
let small = Limits { max_file_size: 1000, ..Default::default() };
|
||||
assert_eq!(Pst::open_with(Cursor::new(&bytes[..]), small).err(), Some(Error::Cap(Cap::FileSize)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn items_past_the_cap_are_counted_and_left_out() {
|
||||
let bytes = build(&sample(unicode(), None));
|
||||
let mut pst = open(&bytes, Limits { max_items: 3, ..Default::default() });
|
||||
let tree = pst.walk_folders().unwrap();
|
||||
let id = |name: &str| tree.folders.iter().find(|f| f.folder.name == name).unwrap().folder.id;
|
||||
let inbox = pst.items(id("Inbox")).unwrap();
|
||||
assert_eq!((inbox.items.len(), inbox.left_out.len()), (2, 0));
|
||||
let projects = pst.items(id("Projects")).unwrap();
|
||||
assert_eq!((projects.items.len(), projects.left_out.len()), (1, 0));
|
||||
let calendar = pst.items(id("Calendar")).unwrap();
|
||||
assert_eq!((calendar.items.len(), calendar.past_cap), (0, 1));
|
||||
assert_eq!(calendar.left_out, vec![crate::LeftOut { id: id("Calendar"), parent: None, property: None, reason: Error::Cap(Cap::Items) }]);
|
||||
// A table of more rows than the cap is refused as it is read.
|
||||
let mut small = open(&bytes, Limits { max_items: 1, ..Default::default() });
|
||||
assert_eq!(small.items(id("Inbox")).err(), Some(Error::Cap(Cap::Items)));
|
||||
// Listing a folder again doesn't count it twice.
|
||||
let again = pst.items(id("Inbox")).unwrap();
|
||||
assert_eq!((again.items.len(), again.left_out.len()), (2, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn folders_past_the_cap_are_left_out() {
|
||||
let mut spec = sample(unicode(), None);
|
||||
spec.folders[2].children = (0..20).map(|i| FolderSpec { name: format!("Old {i}"), ..Default::default() }).collect();
|
||||
let bytes = build(&spec);
|
||||
let mut pst = open(&bytes, Limits { max_folders: 6, ..Default::default() });
|
||||
let tree = pst.walk_folders().unwrap();
|
||||
assert!(tree.folders.len() <= 6, "{}", tree.folders.len());
|
||||
assert!(tree.left_out.iter().any(|l| l.reason == Error::Cap(Cap::Folders)));
|
||||
// A hierarchy table with more rows than the cap is refused as a whole.
|
||||
assert_eq!(pst.subfolders(tree.folders.iter().find(|f| f.folder.name == "Contacts").unwrap().folder.id).err(), Some(Error::Cap(Cap::Folders)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn folders_nested_past_the_cap_are_left_out() {
|
||||
let mut spec = sample(unicode(), None);
|
||||
let mut deep = FolderSpec { name: "Level 6".into(), ..Default::default() };
|
||||
for level in (2..6).rev() {
|
||||
deep = FolderSpec { name: format!("Level {level}"), children: vec![deep], ..Default::default() };
|
||||
}
|
||||
spec.folders[2].children = vec![deep];
|
||||
let bytes = build(&spec);
|
||||
let mut pst = open(&bytes, Limits { max_folder_depth: 4, ..Default::default() });
|
||||
let tree = pst.walk_folders().unwrap();
|
||||
assert!(tree.folders.iter().all(|f| f.depth <= 4));
|
||||
let left: Vec<&Error> = tree.left_out.iter().map(|l| &l.reason).collect();
|
||||
assert_eq!(left, vec![&Error::Cap(Cap::FolderDepth)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recipients_and_attachments_past_their_caps_are_left_out() {
|
||||
let bytes = build(&sample(unicode(), None));
|
||||
let mut pst = open(&bytes, Limits { max_recipients: 2, max_attachments: 1, ..Default::default() });
|
||||
let inbox = pst.walk_folders().unwrap().folders[2].folder.id;
|
||||
let item = pst.items(inbox).unwrap().items[0].id;
|
||||
let m = pst.message(&MessageRef::item(item)).unwrap();
|
||||
assert_eq!(m.recipients.len(), 2);
|
||||
assert_eq!(m.attachments.len(), 1);
|
||||
let parts: Vec<&Part> = m.left_out.iter().map(|p| &p.part).collect();
|
||||
assert_eq!(parts, vec![&Part::Recipients { count: 1 }, &Part::Attachments { count: 1 }]);
|
||||
assert_eq!(m.left_out[0].reason, Error::Cap(Cap::Recipients));
|
||||
assert_eq!(m.left_out[1].reason, Error::Cap(Cap::Attachments));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_attachment_past_its_cap_is_refused_before_it_is_read() {
|
||||
let bytes = build(&sample(unicode(), None));
|
||||
let mut pst = open(&bytes, Limits { max_attachment_bytes: 10_000, ..Default::default() });
|
||||
let inbox = pst.walk_folders().unwrap().folders[2].folder.id;
|
||||
let item = pst.items(inbox).unwrap().items[0].id;
|
||||
let m = pst.message(&MessageRef::item(item)).unwrap();
|
||||
assert_eq!(m.attachments[0].content, AttachmentContent::Data { size: 20_000 });
|
||||
assert_eq!(pst.attachment_reader(&m.attachments[0].id).err(), Some(Error::Cap(Cap::AttachmentSize)));
|
||||
assert_eq!(pst.attachment_bytes(&m.attachments[0].id).err(), Some(Error::Cap(Cap::AttachmentSize)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_item_past_its_cap_is_left_out() {
|
||||
let mut spec = sample(unicode(), None);
|
||||
spec.folders[0].messages[1].body = "Words and more words. ".repeat(500);
|
||||
let bytes = build(&spec);
|
||||
let mut pst = open(&bytes, Limits { max_item_bytes: 4096, ..Default::default() });
|
||||
let inbox = pst.walk_folders().unwrap().folders[2].folder.id;
|
||||
let items = pst.items(inbox).unwrap();
|
||||
assert_eq!(pst.message(&MessageRef::item(items.items[1].id)).err(), Some(Error::Cap(Cap::ItemSize)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn messages_attached_past_the_depth_cap_are_left_out() {
|
||||
let mut spec = sample(unicode(), None);
|
||||
let mut nested = note("Innermost");
|
||||
for level in 0..4 {
|
||||
let mut outer = note(&format!("Wrapper {level}"));
|
||||
outer.attachments = vec![AttachSpec::Message(Box::new(nested))];
|
||||
nested = outer;
|
||||
}
|
||||
spec.folders[0].messages = vec![nested];
|
||||
let bytes = build(&spec);
|
||||
let mut pst = open(&bytes, Limits { max_embedded_depth: 2, ..Default::default() });
|
||||
let inbox = pst.walk_folders().unwrap().folders[2].folder.id;
|
||||
let first = pst.items(inbox).unwrap().items[0].id;
|
||||
let mut m = pst.message(&MessageRef::item(first)).unwrap();
|
||||
let mut depth = 0;
|
||||
loop {
|
||||
match m.attachments[0].content.clone() {
|
||||
AttachmentContent::Message(inner) => {
|
||||
depth += 1;
|
||||
m = pst.message(&inner).unwrap();
|
||||
}
|
||||
AttachmentContent::MessageTooDeep => break,
|
||||
other => panic!("{other:?}"),
|
||||
}
|
||||
}
|
||||
assert_eq!(depth, 2);
|
||||
let too_deep = MessageRef { node: m.id.node, path: vec![Nid(1); 6] };
|
||||
assert_eq!(pst.message(&too_deep).err(), Some(Error::Cap(Cap::EmbeddedDepth)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_item_with_too_many_parts_is_left_out() {
|
||||
let mut spec = sample(unicode(), None);
|
||||
let mut m = note("Many files");
|
||||
m.recipients = vec![person("Ada Fernsby", "ada", 1)];
|
||||
m.attachments = (0..40).map(|i| AttachSpec::File { name: format!("f{i}.txt"), mime: "text/plain".into(), data: vec![b'x'; 10] }).collect();
|
||||
spec.folders[0].messages = vec![m];
|
||||
let bytes = build(&spec);
|
||||
let mut pst = open(&bytes, Limits { max_subnodes: 20, ..Default::default() });
|
||||
let inbox = pst.walk_folders().unwrap().folders[2].folder.id;
|
||||
let item = pst.items(inbox).unwrap().items[0].id;
|
||||
assert_eq!(pst.message(&MessageRef::item(item)).err(), Some(Error::Cap(Cap::Subnodes)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn data_spread_over_too_many_blocks_is_refused() {
|
||||
// The message store's data in 600 blocks of one byte each.
|
||||
let layout = Layout { block_data: 1, xblock_ids: 1000, ..Layout::new(Format::Unicode, Encryption::None) };
|
||||
let nodes = vec![Node { nid: 0x21, parent: 0, data: Data::Stream(vec![0xEC; 600]), subnodes: vec![] }];
|
||||
let bytes = raw(&layout, &nodes);
|
||||
assert_eq!(Pst::open(Cursor::new(&bytes[..])).err(), Some(Error::Cap(Cap::DataBlocks)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_store_past_the_item_cap_is_refused_before_it_is_read() {
|
||||
let layout = Layout { block_data: 8000, ..Layout::new(Format::Unicode, Encryption::None) };
|
||||
let nodes = vec![Node { nid: 0x21, parent: 0, data: Data::Stream(vec![0; 40_000]), subnodes: vec![] }];
|
||||
let bytes = raw(&layout, &nodes);
|
||||
let limits = Limits { max_item_bytes: 30_000, ..Default::default() };
|
||||
assert_eq!(Pst::open_with(Cursor::new(&bytes[..]), limits).err(), Some(Error::Cap(Cap::ItemSize)));
|
||||
}
|
||||
@@ -0,0 +1,583 @@
|
||||
//! Files made to cost far more to read than their size: one value or block named many times,
|
||||
//! loads that were not counted, attached messages that hold themselves or multiply. Each is
|
||||
//! held to a budget for the whole call, or refused.
|
||||
|
||||
use std::io::Cursor;
|
||||
|
||||
use super::writer::{self, AttachSpec, Data, FolderSpec, Layout, Node, Spill, SubNode, binary, hnid_ref, int32, nodes, pc, raw, tc, unicode};
|
||||
use super::{first_item, note, pattern, sample};
|
||||
use crate::header::{HEADER_READ, Header};
|
||||
use crate::ndb::Ndb;
|
||||
use crate::prop::types::BINARY;
|
||||
use crate::source::Source;
|
||||
use crate::{AttachmentContent, Cap, Encryption, Error, Format, Limits, MessageRef, Nid, Part, PropName, PropValue, Pst};
|
||||
|
||||
fn unicode_layout() -> Layout {
|
||||
Layout::new(Format::Unicode, Encryption::Permute)
|
||||
}
|
||||
|
||||
fn open(bytes: &[u8], limits: Limits) -> Pst<Cursor<&[u8]>> {
|
||||
Pst::open_with(Cursor::new(bytes), limits).unwrap()
|
||||
}
|
||||
|
||||
/// The first spilled value of a fresh [`Spill`] (and of a table's): its subnode id.
|
||||
const FIRST_SPILL: u32 = (0x101 << 5) | 0x1F;
|
||||
|
||||
/// Bytes kept in binary values, to measure what a walk or a listing holds.
|
||||
fn binary_bytes<'a>(props: impl Iterator<Item = &'a crate::Property>) -> usize {
|
||||
props.map(|p| if let PropValue::Binary(b) = &p.value { b.len() } else { 0 }).sum()
|
||||
}
|
||||
|
||||
fn inbox(pst: &mut Pst<Cursor<&[u8]>>) -> Nid {
|
||||
pst.walk_folders().unwrap().folders.iter().find(|f| f.folder.name == "Inbox").unwrap().folder.id
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn named_property_names_are_capped_one_by_one_and_in_all() {
|
||||
// One 2,048-byte name, then 5,000 entries that all name the same 1,000-byte string: a
|
||||
// stream of 3 KB that would come out as 5 MB of names.
|
||||
let mut strings = Vec::new();
|
||||
for len in [2048usize, 1000] {
|
||||
strings.extend_from_slice(&(len as u32).to_le_bytes());
|
||||
strings.extend(std::iter::repeat_n(b'a', len).enumerate().map(|(i, c)| if i % 2 == 0 { c } else { 0 }));
|
||||
}
|
||||
let mut entries = Vec::new();
|
||||
for i in 0..5001u32 {
|
||||
let at: u32 = if i == 0 { 0 } else { 2052 };
|
||||
entries.extend_from_slice(&at.to_le_bytes());
|
||||
entries.extend_from_slice(&((2u16 << 1) | 1).to_le_bytes());
|
||||
entries.extend_from_slice(&(i as u16).to_le_bytes());
|
||||
}
|
||||
let spec = sample(unicode_layout(), None);
|
||||
let mut all = nodes(&spec);
|
||||
let mut spill = Spill::new();
|
||||
let blocks = pc(&spec.layout, &[(0x0001, int32(251)), (0x0002, binary(&[])), (0x0003, binary(&entries)), (0x0004, binary(&strings))], &mut spill);
|
||||
let map = all.iter_mut().find(|n| n.nid == 0x61).unwrap();
|
||||
map.data = Data::Blocks(blocks);
|
||||
map.subnodes = spill.subnodes;
|
||||
let bytes = raw(&spec.layout, &all);
|
||||
let mut pst = open(&bytes, Limits::default());
|
||||
let named = pst.named_properties().unwrap();
|
||||
assert_eq!(named.left_out()[0], (Some(0x8000), Error::Cap(Cap::NamedProperties)), "a name over 1 KB is left out");
|
||||
assert!(named.left_out().iter().all(|(_, e)| *e == Error::Cap(Cap::NamedProperties)));
|
||||
assert_eq!(named.len() + named.left_out().len(), 5001, "every entry is read or named as left out");
|
||||
let kept: usize = named.iter().map(|p| if let PropName::Name(n) = &p.name { n.len() * 2 } else { 0 }).sum();
|
||||
assert!(kept <= crate::named::MAX_NAMES, "{kept}");
|
||||
assert!(named.len() >= 4000, "names within the total are kept: {}", named.len());
|
||||
}
|
||||
|
||||
/// The file's folders under Contacts, each with 64 binary properties that all name one value of
|
||||
/// `size` bytes.
|
||||
fn aliased_folders(size: usize, count: usize) -> (Layout, Vec<u8>) {
|
||||
let mut spec = sample(unicode_layout(), None);
|
||||
spec.folders[2].children = (0..count).map(|i| FolderSpec { name: format!("Box {i}"), ..Default::default() }).collect();
|
||||
let mut all = nodes(&spec);
|
||||
for n in all.iter_mut().filter(|n| n.nid & 0x1F == 2 && n.nid != 0x122 && n.nid != writer::IPM_SUBTREE) {
|
||||
let mut spill = Spill::new();
|
||||
let blob = spill.put(0x1F, pattern(size, 3));
|
||||
assert_eq!(blob, FIRST_SPILL);
|
||||
let mut props = vec![(0x3001, unicode("Aliased"))];
|
||||
props.extend((0..64u16).map(|i| (0x6000 + i, hnid_ref(BINARY, blob))));
|
||||
n.data = Data::Blocks(pc(&spec.layout, &props, &mut spill));
|
||||
n.subnodes = spill.subnodes;
|
||||
}
|
||||
let bytes = raw(&spec.layout, &all);
|
||||
(spec.layout, bytes)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_folder_walk_reads_within_one_budget() {
|
||||
// 25 folders × 64 names of one 60 KB value: 96 MB from a file of under 2 MB.
|
||||
let (_, bytes) = aliased_folders(60_000, 20);
|
||||
assert!(bytes.len() < 2_000_000);
|
||||
let limit = 4 * 1024 * 1024;
|
||||
let mut pst = open(&bytes, Limits { max_listing_bytes: limit, ..Default::default() });
|
||||
let tree = pst.walk_folders().unwrap();
|
||||
let kept: usize = tree.folders.iter().map(|f| binary_bytes(f.folder.properties.iter())).sum();
|
||||
assert!(kept as u64 <= limit, "{kept}");
|
||||
assert!(tree.left_out.iter().any(|l| l.reason == Error::Cap(Cap::ListingSize)), "{:?}", tree.left_out);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_folder_walk_leaves_large_values_for_later() {
|
||||
let (_, bytes) = aliased_folders(100_000, 1);
|
||||
let mut pst = open(&bytes, Limits::default());
|
||||
let tree = pst.walk_folders().unwrap();
|
||||
let boxed = tree.folders.iter().find(|f| f.folder.name == "Aliased").unwrap().folder.clone();
|
||||
assert_eq!(boxed.properties.get(0x6000), Some(&PropValue::Large { size: 100_000 }), "past the summary cap");
|
||||
let whole = pst.folder(boxed.id).unwrap();
|
||||
assert_eq!(whole.properties.get(0x6000), Some(&PropValue::Binary(pattern(100_000, 3))), "read whole on its own");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_listing_of_items_reads_within_one_budget() {
|
||||
// 200 rows whose binary cell names the first row's 50 KB value: 10 MB from 60 KB.
|
||||
let spec = sample(unicode_layout(), None);
|
||||
let mut all = nodes(&spec);
|
||||
let tag = 0x6100_0102u32;
|
||||
let rows: Vec<(u32, Vec<(u32, writer::Value)>)> = (0..200u32)
|
||||
.map(|i| {
|
||||
let v = if i == 0 { binary(&pattern(50_000, 9)) } else { hnid_ref(BINARY, FIRST_SPILL) };
|
||||
(((0x20000 + i) << 5) | 4, vec![(tag, v)])
|
||||
})
|
||||
.collect();
|
||||
let (blocks, subs) = tc(&spec.layout, &[tag], &rows);
|
||||
let inbox_contents = (0x403 << 5) | 0x0E;
|
||||
let table = all.iter_mut().find(|n| n.nid == inbox_contents).unwrap();
|
||||
table.data = Data::Blocks(blocks);
|
||||
table.subnodes = subs;
|
||||
let bytes = raw(&spec.layout, &all);
|
||||
assert!(bytes.len() < 200_000, "{}", bytes.len());
|
||||
let limit = 1024 * 1024;
|
||||
let mut pst = open(&bytes, Limits { max_listing_bytes: limit, ..Default::default() });
|
||||
let folder = inbox(&mut pst);
|
||||
let list = pst.items(folder).unwrap();
|
||||
// Listed until the budget is spent; the rest are counted, and the folder named once.
|
||||
assert!(!list.items.is_empty() && list.items.len() < 200, "{}", list.items.len());
|
||||
assert_eq!(list.past_cap, 200 - list.items.len());
|
||||
let kept: usize = list.items.iter().map(|i| binary_bytes(i.properties.iter())).sum();
|
||||
assert!(kept as u64 <= limit, "{kept}");
|
||||
let short: Vec<&crate::LeftOut> = list.left_out.iter().filter(|l| l.reason == Error::Cap(Cap::ListingSize)).collect();
|
||||
assert_eq!(short.len(), 1);
|
||||
assert_eq!((short[0].id, short[0].parent), (folder, None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_data_tree_naming_one_block_twice_is_refused() {
|
||||
for levels in [1u8, 2] {
|
||||
let layout = Layout::new(Format::Unicode, Encryption::None);
|
||||
let store = Node { nid: 0x21, parent: 0, data: Data::Repeated { piece: vec![0xEC; 8000], times: 32, levels }, subnodes: vec![] };
|
||||
let bytes = raw(&layout, &[store]);
|
||||
assert!(bytes.len() < 30_000);
|
||||
assert_eq!(Pst::open(Cursor::new(&bytes[..])).err(), Some(Error::Cycle("a data tree")), "levels {levels}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_load_of_a_message_is_charged_to_its_budget() {
|
||||
// 30 attachments whose data (3,000 bytes each) is kept in their own heaps: each heap is
|
||||
// loaded, and counts, though the data itself is left for later.
|
||||
let mut spec = sample(unicode_layout(), None);
|
||||
let mut m = note("Many small files");
|
||||
m.attachments =
|
||||
(0..30).map(|i| AttachSpec::File { name: format!("f{i}.bin"), mime: "application/octet-stream".into(), data: pattern(3000, i) }).collect();
|
||||
spec.folders[0].messages = vec![m];
|
||||
let bytes = build_spec(&spec);
|
||||
let mut pst = open(&bytes, Limits::default());
|
||||
let item = first_item(&mut pst);
|
||||
assert_eq!(pst.message(&MessageRef::item(item)).unwrap().attachments.len(), 30);
|
||||
let mut pst = open(&bytes, Limits { max_item_bytes: 60_000, ..Default::default() });
|
||||
let item = first_item(&mut pst);
|
||||
assert_eq!(pst.message(&MessageRef::item(item)).err(), Some(Error::Cap(Cap::ItemSize)));
|
||||
}
|
||||
|
||||
fn build_spec(spec: &writer::FileSpec) -> Vec<u8> {
|
||||
writer::build(spec)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_that_is_read_is_held_to_the_file_size() {
|
||||
let bytes = build_spec(&sample(unicode_layout(), None));
|
||||
let mut pst = open(&bytes, Limits { read_multiple: 3, read_floor: 0, ..Default::default() });
|
||||
let mut stopped = None;
|
||||
for _ in 0..100 {
|
||||
// A walk names what it could not read in its left_out, or fails as a whole.
|
||||
let reason = match pst.walk_folders() {
|
||||
Err(e) => Some(e),
|
||||
Ok(tree) => tree.left_out.first().map(|l| l.reason.clone()),
|
||||
};
|
||||
if reason.is_some() {
|
||||
stopped = reason;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert_eq!(stopped, Some(Error::Cap(Cap::TotalRead)));
|
||||
let mut pst = open(&bytes, Limits::default());
|
||||
for _ in 0..20 {
|
||||
pst.walk_folders().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
/// A file whose first Inbox item has one attachment: a message whose object is `object`.
|
||||
fn with_object(object: SubNode) -> Vec<u8> {
|
||||
let mut spec = sample(unicode_layout(), None);
|
||||
let mut m = note("Holds itself");
|
||||
m.attachments = vec![AttachSpec::Object(object)];
|
||||
spec.folders[0].messages = vec![m];
|
||||
build_spec(&spec)
|
||||
}
|
||||
|
||||
const OBJECT: u32 = (0x300 << 5) | 0x04;
|
||||
const ATTACHMENT: u32 = (0x200 << 5) | 0x05;
|
||||
|
||||
/// The data and subnode tree of a node, read from the file's node tree.
|
||||
fn node_bids(bytes: &[u8], nid: u32) -> (u64, u64) {
|
||||
let limits = Limits::default();
|
||||
let mut src = Source::new(Cursor::new(bytes), &limits).unwrap();
|
||||
let header = Header::parse(&src.read_at(0, HEADER_READ).unwrap()).unwrap();
|
||||
let e = Ndb::new(src, header, limits).node(nid).unwrap();
|
||||
(e.data, e.sub)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_attached_message_that_is_the_message_it_is_attached_to_is_a_cycle() {
|
||||
// First with a stand-in for the object, to learn where the message's own data and subnode
|
||||
// tree are; then with the object naming them. Nothing else moves between the two.
|
||||
let first = with_object(SubNode::with_sub_bid(OBJECT, 4, 6));
|
||||
let mut pst = open(&first, Limits::default());
|
||||
let item = first_item(&mut pst);
|
||||
let (data, sub) = node_bids(&first, item.0);
|
||||
let bytes = with_object(SubNode::with_sub_bid(OBJECT, data, sub));
|
||||
assert_eq!(node_bids(&bytes, item.0), (data, sub));
|
||||
let mut pst = open(&bytes, Limits::default());
|
||||
let m = pst.message(&MessageRef::item(item)).unwrap();
|
||||
assert!(m.attachments.is_empty());
|
||||
assert_eq!(m.left_out[0].part, Part::Attachment { id: Nid(ATTACHMENT) });
|
||||
assert_eq!(m.left_out[0].reason, Error::Cycle("an attached message"));
|
||||
let inner = MessageRef { node: item, path: vec![Nid(ATTACHMENT), Nid(OBJECT)] };
|
||||
assert_eq!(pst.message(&inner).err(), Some(Error::Cycle("an attached message")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_attachment_table_naming_a_row_twice_is_refused() {
|
||||
let mut spec = sample(unicode_layout(), None);
|
||||
let mut m = note("Twice");
|
||||
m.attachments = vec![
|
||||
AttachSpec::File { name: "a.txt".into(), mime: "text/plain".into(), data: b"one".to_vec() },
|
||||
AttachSpec::File { name: "b.txt".into(), mime: "text/plain".into(), data: b"two".to_vec() },
|
||||
];
|
||||
spec.folders[0].messages = vec![m];
|
||||
let mut all = nodes(&spec);
|
||||
let message = all.iter_mut().find(|n| n.nid & 0x1F == 4).unwrap();
|
||||
let rows = vec![(ATTACHMENT, vec![(0x3705_0003, int32(1))]), (ATTACHMENT, vec![(0x3705_0003, int32(1))])];
|
||||
let (blocks, subs) = tc(&spec.layout, &[0x3705_0003], &rows);
|
||||
let table = message.subnodes.iter_mut().find(|s| s.nid == 0x671).unwrap();
|
||||
table.data = Data::Blocks(blocks);
|
||||
table.subnodes = subs;
|
||||
let bytes = raw(&spec.layout, &all);
|
||||
let mut pst = open(&bytes, Limits::default());
|
||||
let item = first_item(&mut pst);
|
||||
// A table's row ids are the keys of its row index, which must strictly increase.
|
||||
let m = pst.message(&MessageRef::item(item)).unwrap();
|
||||
assert!(m.attachments.is_empty());
|
||||
assert_eq!(m.left_out[0].part, Part::AttachmentTable);
|
||||
assert_eq!(m.left_out[0].reason, Error::Corrupt("a B-tree-on-heap's keys out of order"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attached_messages_are_counted_per_item() {
|
||||
// Three attached messages, each with three of its own: twelve under the item.
|
||||
let mut spec = sample(unicode_layout(), None);
|
||||
let leaf = note("Leaf");
|
||||
let mut middle = note("Middle");
|
||||
middle.attachments = vec![AttachSpec::Message(Box::new(leaf)); 3];
|
||||
let mut top = note("Top");
|
||||
top.attachments = vec![AttachSpec::Message(Box::new(middle)); 3];
|
||||
spec.folders[0].messages = vec![top.clone(), top];
|
||||
let bytes = build_spec(&spec);
|
||||
let mut pst = open(&bytes, Limits { max_embedded_messages: 5, ..Default::default() });
|
||||
let folder = inbox(&mut pst);
|
||||
let items = pst.items(folder).unwrap().items;
|
||||
// The first item twice: reading it again starts its count again.
|
||||
for item in [&items[0], &items[0], &items[1]] {
|
||||
let (mut read, mut refused) = (0, 0);
|
||||
let mut queue = vec![MessageRef::item(item.id)];
|
||||
while let Some(m) = queue.pop() {
|
||||
match pst.message(&m) {
|
||||
Ok(msg) => {
|
||||
read += usize::from(!m.path.is_empty());
|
||||
for a in msg.attachments {
|
||||
if let AttachmentContent::Message(inner) = a.content {
|
||||
queue.push(inner);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
assert_eq!(e, Error::Cap(Cap::EmbeddedMessages));
|
||||
refused += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
assert_eq!((read, refused), (5, 4), "counted again for each item");
|
||||
}
|
||||
}
|
||||
|
||||
/// A heap block (not the first) whose page map holds `k` items that all span the same leaf of
|
||||
/// `l` property records: the odd items run from its start to its end, the even ones back.
|
||||
fn aliasing_block(k: usize, l: usize) -> Vec<u8> {
|
||||
let mut b = vec![0u8; 2];
|
||||
let start = b.len() as u16;
|
||||
for i in 0..l {
|
||||
b.extend_from_slice(&((0x6000 + (i % 0x1000)) as u16).to_le_bytes());
|
||||
b.extend_from_slice(&0x0003u16.to_le_bytes());
|
||||
b.extend_from_slice(&(i as u32).to_le_bytes());
|
||||
}
|
||||
let end = b.len() as u16;
|
||||
b[0..2].copy_from_slice(&end.to_le_bytes());
|
||||
b.extend_from_slice(&((2 * k - 1) as u16).to_le_bytes());
|
||||
b.extend_from_slice(&0u16.to_le_bytes());
|
||||
for i in 0..2 * k {
|
||||
b.extend_from_slice(&(if i % 2 == 0 { start } else { end }).to_le_bytes());
|
||||
}
|
||||
b
|
||||
}
|
||||
|
||||
/// A message store whose property context's index names every aliasing item of `blocks`
|
||||
/// aliasing blocks: `blocks × k × l` records from a few kilobytes.
|
||||
fn aliased_store(blocks: usize, k: usize, l: usize) -> Vec<u8> {
|
||||
let mut index = Vec::new();
|
||||
for j in 1..=blocks {
|
||||
for i in 0..k {
|
||||
index.extend_from_slice(&0x6000u16.to_le_bytes());
|
||||
index.extend_from_slice(&(((j as u32) << 16) | (((2 * i + 1) as u32) << 5)).to_le_bytes());
|
||||
}
|
||||
}
|
||||
let mut b0 = vec![0u8; 12];
|
||||
b0[2] = 0xEC;
|
||||
b0[3] = 0xBC;
|
||||
b0[4..8].copy_from_slice(&(1u32 << 5).to_le_bytes());
|
||||
let s1 = b0.len() as u16;
|
||||
b0.extend_from_slice(&[0xB5, 2, 6, 1]);
|
||||
b0.extend_from_slice(&(2u32 << 5).to_le_bytes());
|
||||
let s2 = b0.len() as u16;
|
||||
b0.extend_from_slice(&index);
|
||||
let s3 = b0.len() as u16;
|
||||
b0[0..2].copy_from_slice(&s3.to_le_bytes());
|
||||
b0.extend_from_slice(&2u16.to_le_bytes());
|
||||
b0.extend_from_slice(&0u16.to_le_bytes());
|
||||
for o in [s1, s2, s3] {
|
||||
b0.extend_from_slice(&o.to_le_bytes());
|
||||
}
|
||||
let mut heap = vec![b0];
|
||||
heap.extend((0..blocks).map(|_| aliasing_block(k, l)));
|
||||
let layout = Layout::new(Format::Unicode, Encryption::None);
|
||||
let mut all = nodes(&sample(layout.clone(), None));
|
||||
let store = all.iter_mut().find(|n| n.nid == 0x21).unwrap();
|
||||
store.data = Data::Blocks(heap);
|
||||
store.subnodes = vec![];
|
||||
raw(&layout, &all)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn heap_items_that_share_bytes_are_refused() {
|
||||
// 2 blocks × 500 items × 450 records: 450,000 properties from a file of about 70 KB.
|
||||
let bytes = aliased_store(2, 500, 450);
|
||||
assert!(bytes.len() < 100_000, "{}", bytes.len());
|
||||
assert_eq!(Pst::open(Cursor::new(&bytes[..])).err(), Some(Error::Corrupt("a heap's page map")));
|
||||
// A page map whose offsets run back, alone.
|
||||
let mut block = vec![12u8, 0, 0xEC, 0xBC, 0, 0, 0, 0, 0, 0, 0, 0];
|
||||
block.extend_from_slice(&[2, 0, 0, 0, 12, 0, 8, 0, 12, 0]);
|
||||
assert!(crate::heap::Heap::new(vec![block]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_property_context_naming_an_id_twice_is_refused() {
|
||||
// 65,537 records: one id is named twice, whatever the order.
|
||||
let spec = sample(unicode_layout(), None);
|
||||
let mut all = nodes(&spec);
|
||||
let props: Vec<(u16, writer::Value)> = (0..=65_536u32).map(|i| (i as u16, int32(1))).collect();
|
||||
let mut spill = Spill::new();
|
||||
let store = all.iter_mut().find(|n| n.nid == 0x21).unwrap();
|
||||
store.data = Data::Blocks(pc(&spec.layout, &props, &mut spill));
|
||||
let bytes = raw(&spec.layout, &all);
|
||||
assert_eq!(Pst::open(Cursor::new(&bytes[..])).err(), Some(Error::Corrupt("a B-tree-on-heap's keys out of order")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_table_that_cannot_be_read_is_named_not_taken_for_none() {
|
||||
// The Inbox's hierarchy and contents tables name a block that isn't in the file.
|
||||
let spec = sample(unicode_layout(), None);
|
||||
let mut all = nodes(&spec);
|
||||
for kind in [0x0D, 0x0E] {
|
||||
let table = all.iter_mut().find(|n| n.nid == (0x403 << 5) | kind).unwrap();
|
||||
table.data = Data::Bid(0x9_9990);
|
||||
table.subnodes = vec![];
|
||||
}
|
||||
let bytes = raw(&spec.layout, &all);
|
||||
let mut pst = open(&bytes, Limits::default());
|
||||
let tree = pst.walk_folders().unwrap();
|
||||
let inbox = Nid((0x403 << 5) | 2);
|
||||
assert!(tree.folders.iter().any(|f| f.folder.id == inbox));
|
||||
assert!(!tree.folders.iter().any(|f| f.folder.name == "Projects"));
|
||||
assert_eq!(tree.left_out.len(), 1);
|
||||
assert_eq!((tree.left_out[0].id, &tree.left_out[0].reason), (inbox, &Error::NotFound("a block")));
|
||||
assert_eq!(pst.items(inbox).err(), Some(Error::NotFound("a block")));
|
||||
// A folder with no tables at all has no subfolders and no items.
|
||||
let mut all = nodes(&spec);
|
||||
// The folder made last (Projects), without its tables.
|
||||
let bare = Nid(all.iter().filter(|n| n.nid & 0x1F == 2).map(|n| n.nid).max().unwrap());
|
||||
all.retain(|n| n.nid & !0x1F != bare.0 & !0x1F || n.nid == bare.0);
|
||||
let bytes = raw(&spec.layout, &all);
|
||||
let mut pst = open(&bytes, Limits::default());
|
||||
assert_eq!(pst.subfolders(bare), Ok(vec![]));
|
||||
assert!(pst.items(bare).unwrap().items.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_item_and_its_attached_messages_share_one_budget() {
|
||||
let body = "Words, and more words. ".repeat(1500);
|
||||
let mut inner = note("Inside");
|
||||
inner.body = body.clone();
|
||||
let mut top = note("Outside");
|
||||
top.body = body;
|
||||
top.attachments = vec![AttachSpec::Message(Box::new(inner))];
|
||||
let mut spec = sample(unicode_layout(), None);
|
||||
spec.folders[0].messages = vec![top];
|
||||
let bytes = build_spec(&spec);
|
||||
// Each message costs about 105 KB, read and kept: one fits in 150 KB, the two together don't.
|
||||
let mut pst = open(&bytes, Limits { max_item_bytes: 150_000, ..Default::default() });
|
||||
let item = first_item(&mut pst);
|
||||
let m = pst.message(&MessageRef::item(item)).unwrap();
|
||||
let AttachmentContent::Message(inner) = m.attachments[0].content.clone() else { panic!() };
|
||||
assert_eq!(pst.message(&inner).err(), Some(Error::Cap(Cap::ItemSize)));
|
||||
// Reading the item again starts its budget again.
|
||||
pst.message(&MessageRef::item(item)).unwrap();
|
||||
let mut pst = open(&bytes, Limits::default());
|
||||
pst.message(&MessageRef::item(item)).unwrap();
|
||||
pst.message(&inner).unwrap();
|
||||
}
|
||||
|
||||
/// What a call's answer keeps, as the reader charges it: each structure, its strings, and
|
||||
/// each property with what it holds. (A vector's spare room, at most as much again, is not.)
|
||||
fn kept_props(p: &crate::Properties) -> usize {
|
||||
p.iter().map(|p| p.kept() as usize).sum()
|
||||
}
|
||||
|
||||
fn kept_text(s: &Option<String>) -> usize {
|
||||
s.as_ref().map_or(0, String::capacity)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn what_a_call_keeps_is_within_its_budget() {
|
||||
use std::mem::size_of;
|
||||
// One byte on file becomes a whole property kept: 250 one-byte columns in 2,000 rows,
|
||||
// 8,000 one-byte properties in a message, 300 folders.
|
||||
let mut spec = sample(unicode_layout(), None);
|
||||
spec.folders[0].children = (0..300).map(|i| FolderSpec { name: format!("Folder {i}"), ..Default::default() }).collect();
|
||||
let mut all = nodes(&spec);
|
||||
let columns: Vec<u32> = (0..250u32).map(|i| ((0x6800 + i) << 16) | 0x000B).collect();
|
||||
let rows: Vec<(u32, Vec<(u32, writer::Value)>)> =
|
||||
(0..2000u32).map(|i| (((0x20000 + i) << 5) | 4, columns.iter().map(|&c| (c, writer::boolean(true))).collect())).collect();
|
||||
let (blocks, subs) = tc(&spec.layout, &columns, &rows);
|
||||
let table = all.iter_mut().find(|n| n.nid == (0x403 << 5) | 0x0E).unwrap();
|
||||
table.data = Data::Blocks(blocks);
|
||||
table.subnodes = subs;
|
||||
let props: Vec<(u16, writer::Value)> = (0..8000u16).map(|i| (0x8000 + i, writer::boolean(true))).collect();
|
||||
let mut spill = Spill::new();
|
||||
let message = all.iter_mut().find(|n| n.nid & 0x1F == 4).unwrap();
|
||||
let item = Nid(message.nid);
|
||||
message.data = Data::Blocks(pc(&spec.layout, &props, &mut spill));
|
||||
message.subnodes = spill.subnodes;
|
||||
let bytes = raw(&spec.layout, &all);
|
||||
assert!(bytes.len() < 1_500_000, "{}", bytes.len());
|
||||
|
||||
// A walk of the folders.
|
||||
let budget = 64 * 1024;
|
||||
let mut pst = open(&bytes, Limits { max_listing_bytes: budget, ..Default::default() });
|
||||
let tree = pst.walk_folders().unwrap();
|
||||
let kept: usize = tree
|
||||
.folders
|
||||
.iter()
|
||||
.map(|f| size_of::<crate::FolderEntry>() + f.folder.name.capacity() + kept_text(&f.folder.container_class) + kept_props(&f.folder.properties))
|
||||
.sum::<usize>()
|
||||
+ tree.left_out.len() * size_of::<crate::LeftOut>();
|
||||
assert!(kept <= budget as usize, "{kept}");
|
||||
assert!(tree.left_out.iter().any(|l| l.reason == Error::Cap(Cap::ListingSize)));
|
||||
|
||||
// A listing of items.
|
||||
let inbox = Nid((0x403 << 5) | 2);
|
||||
let budget = 256 * 1024;
|
||||
let mut pst = open(&bytes, Limits { max_listing_bytes: budget, ..Default::default() });
|
||||
let list = pst.items(inbox).unwrap();
|
||||
let kept: usize = list.items.iter().map(|i| size_of::<crate::ItemSummary>() + kept_text(&i.class) + kept_props(&i.properties)).sum::<usize>()
|
||||
+ list.left_out.len() * size_of::<crate::LeftOut>();
|
||||
assert!(kept <= budget as usize, "{kept}");
|
||||
assert!(list.past_cap > 0 && list.items.len() + list.past_cap == 2000);
|
||||
|
||||
// A message: what it keeps is far more than what it reads, and is refused past the cap.
|
||||
let mut pst = open(&bytes, Limits::default());
|
||||
let m = pst.message(&MessageRef::item(item)).unwrap();
|
||||
let kept = size_of::<crate::Message>() + kept_text(&m.class) + kept_props(&m.properties);
|
||||
let budget = 128 * 1024;
|
||||
assert!(kept > budget as usize, "{kept}");
|
||||
let mut pst = open(&bytes, Limits { max_item_bytes: budget, ..Default::default() });
|
||||
assert_eq!(pst.message(&MessageRef::item(item)).err(), Some(Error::Cap(Cap::ItemSize)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_list_value_holds_no_more_than_its_cap() {
|
||||
// 100,000 empty binaries: 400 KB in the file, each a vector of its own in memory. And
|
||||
// 100,000 two-byte numbers, which hold just their bytes.
|
||||
let count = 100_000u32;
|
||||
let mut empties = count.to_le_bytes().to_vec();
|
||||
for _ in 0..count {
|
||||
empties.extend_from_slice(&(4 + 4 * count).to_le_bytes());
|
||||
}
|
||||
let numbers: Vec<u8> = (0..count).flat_map(|i| (i as u16).to_le_bytes()).collect();
|
||||
let (binaries, int16s) = (crate::prop::types::BINARY | 0x1000, crate::prop::types::INT16 | 0x1000);
|
||||
// A list of booleans (whose items aren't read), with the same table, and with a count
|
||||
// that no table could hold.
|
||||
let booleans = crate::prop::types::BOOLEAN | 0x1000;
|
||||
let props = vec![
|
||||
(0x6900u16, writer::Value { ty: binaries, bytes: empties.clone() }),
|
||||
(0x6901, writer::Value { ty: int16s, bytes: numbers.clone() }),
|
||||
(0x6902, writer::Value { ty: booleans, bytes: empties.clone() }),
|
||||
(0x6903, writer::Value { ty: booleans, bytes: vec![0xFF; 8] }),
|
||||
];
|
||||
let spec = sample(unicode_layout(), None);
|
||||
let mut all = nodes(&spec);
|
||||
let mut spill = Spill::new();
|
||||
let message = all.iter_mut().find(|n| n.nid & 0x1F == 4).unwrap();
|
||||
let item = MessageRef::item(Nid(message.nid));
|
||||
message.data = Data::Blocks(pc(&spec.layout, &props, &mut spill));
|
||||
message.subnodes = spill.subnodes;
|
||||
let bytes = raw(&spec.layout, &all);
|
||||
let mut pst = open(&bytes, Limits::default());
|
||||
// Within a cap of twice its bytes: refused before it is made.
|
||||
let max = 2 * empties.len() as u64;
|
||||
assert_eq!(pst.read_property(&item, 0x6900, max).err(), Some(Error::Cap(Cap::ItemSize)));
|
||||
let whole = pst.read_property(&item, 0x6900, 64 << 20).unwrap().unwrap();
|
||||
let PropValue::MultiBinary(v) = &whole else { panic!() };
|
||||
assert_eq!(v.len(), count as usize);
|
||||
assert!(whole.held() as u64 <= 64 << 20);
|
||||
// What is checked before it is made covers what making it holds.
|
||||
assert!(crate::prop::building_cost(binaries, &empties) >= whole.held(), "{}", whole.held());
|
||||
assert_eq!(crate::prop::building_cost(int16s, &numbers), 0);
|
||||
// The numbers fit a cap of exactly their bytes.
|
||||
let Some(PropValue::MultiInt16(n)) = pst.read_property(&item, 0x6901, numbers.len() as u64).unwrap() else { panic!() };
|
||||
assert_eq!((n.len(), n.capacity(), n[7]), (count as usize, count as usize, 7), "no spare room past what is charged");
|
||||
// Lists of other types are kept as their bytes, within a cap of exactly those, and
|
||||
// whatever their bytes say.
|
||||
let max = empties.len() as u64;
|
||||
assert_eq!(pst.read_property(&item, 0x6902, max).unwrap(), Some(PropValue::Unknown { ty: booleans, data: empties.clone() }));
|
||||
assert_eq!(pst.read_property(&item, 0x6903, max).unwrap(), Some(PropValue::Unknown { ty: booleans, data: vec![0xFF; 8] }));
|
||||
assert_eq!(crate::prop::building_cost(booleans, &empties), 0);
|
||||
// And in a message the list is refused within the item's cap, not made.
|
||||
let mut pst = open(&bytes, Limits { max_item_bytes: 1 << 20, max_inline_value: 1 << 20, ..Default::default() });
|
||||
let m = pst.message(&item);
|
||||
assert_eq!(m.err(), Some(Error::Cap(Cap::ItemSize)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_named_entry_that_names_no_id_is_left_out_alone() {
|
||||
// Three numeric names; the middle one's index (0x8000) names no id.
|
||||
let mut entries = Vec::new();
|
||||
for (value, index) in [(0x10u32, 0u16), (0x11, 0x8000), (0x12, 1)] {
|
||||
entries.extend_from_slice(&value.to_le_bytes());
|
||||
entries.extend_from_slice(&(1u16 << 1).to_le_bytes());
|
||||
entries.extend_from_slice(&index.to_le_bytes());
|
||||
}
|
||||
let spec = sample(unicode_layout(), None);
|
||||
let mut all = nodes(&spec);
|
||||
let mut spill = Spill::new();
|
||||
let blocks = pc(&spec.layout, &[(0x0001, int32(251)), (0x0002, binary(&[])), (0x0003, binary(&entries)), (0x0004, binary(&[]))], &mut spill);
|
||||
let map = all.iter_mut().find(|n| n.nid == 0x61).unwrap();
|
||||
map.data = Data::Blocks(blocks);
|
||||
map.subnodes = spill.subnodes;
|
||||
let bytes = raw(&spec.layout, &all);
|
||||
let mut pst = open(&bytes, Limits::default());
|
||||
let named = pst.named_properties().unwrap();
|
||||
assert_eq!(named.len(), 2);
|
||||
assert_eq!(named.get(0x8001).unwrap().name, PropName::Id(0x12));
|
||||
assert_eq!(named.left_out(), &[(None, Error::Corrupt("a named property's index"))]);
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
//! Damaged files: checksums, offsets, trees that lead back to themselves. Each ends in an
|
||||
//! error value, never a panic.
|
||||
|
||||
use std::io::Cursor;
|
||||
|
||||
use super::writer::{self, HeapBuilder, Layout, build, nodes, raw};
|
||||
use super::{exercise, sample};
|
||||
use crate::crc::crc32;
|
||||
use crate::header::{HEADER_READ, Header};
|
||||
use crate::heap::Heap;
|
||||
use crate::ndb::{Ndb, signature};
|
||||
use crate::source::Source;
|
||||
use crate::{Encryption, Error, Format, Limits, Pst};
|
||||
|
||||
fn base(format: Format) -> Vec<u8> {
|
||||
build(&sample(Layout::tiny(format, Encryption::Cyclic), None))
|
||||
}
|
||||
|
||||
fn open(bytes: &[u8]) -> crate::Result<Pst<Cursor<&[u8]>>> {
|
||||
Pst::open(Cursor::new(bytes))
|
||||
}
|
||||
|
||||
/// Puts a page's CRC right after it has been changed.
|
||||
fn fix_page(bytes: &mut [u8], format: Format, ib: usize) {
|
||||
let t = 512 - format.trailer_size();
|
||||
let crc = crc32(&bytes[ib..ib + t]);
|
||||
let at = ib + t + if format == Format::Unicode { 4 } else { 8 };
|
||||
bytes[at..at + 4].copy_from_slice(&crc.to_le_bytes());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn not_a_pst_file() {
|
||||
assert_eq!(open(b"").err(), Some(Error::NotPst));
|
||||
assert_eq!(open(b"!BDN").err(), Some(Error::NotPst));
|
||||
assert_eq!(open(&[0u8; 600]).err(), Some(Error::NotPst));
|
||||
let mut b = base(Format::Unicode);
|
||||
b[10] = 20; // wVer
|
||||
assert_eq!(open(&b).err(), Some(Error::UnsupportedVersion(20)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_damaged_header_is_refused() {
|
||||
for format in [Format::Unicode, Format::Ansi] {
|
||||
let mut b = base(format);
|
||||
b[40] ^= 1;
|
||||
assert_eq!(open(&b).err(), Some(Error::BadChecksum("the file's header")));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn data_encrypted_another_way_is_refused() {
|
||||
let mut b = base(Format::Unicode);
|
||||
b[513] = 0x10;
|
||||
let full = crc32(&b[8..524]);
|
||||
b[524..528].copy_from_slice(&full.to_le_bytes());
|
||||
let partial = crc32(&b[8..479]);
|
||||
b[4..8].copy_from_slice(&partial.to_le_bytes());
|
||||
assert_eq!(open(&b).err(), Some(Error::UnsupportedEncryption(0x10)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_damaged_block_or_page_is_found_by_its_checksum() {
|
||||
for format in [Format::Unicode, Format::Ansi] {
|
||||
let clean = base(format);
|
||||
let h = Header::parse(&clean).unwrap();
|
||||
// A byte of the first data block (the store's first heap block, written first).
|
||||
let mut b = clean.clone();
|
||||
b[0x4400 + 3] ^= 0xFF;
|
||||
assert!(matches!(open(&b).err(), Some(Error::BadChecksum(_))), "{format:?}");
|
||||
// A byte of the node tree's root page.
|
||||
let mut b = clean.clone();
|
||||
b[h.nbt.ib as usize + 5] ^= 0xFF;
|
||||
assert_eq!(open(&b).err(), Some(Error::BadChecksum("a B-tree page")));
|
||||
// The page's id.
|
||||
let mut b = clean.clone();
|
||||
let t = h.nbt.ib as usize + 512 - format.trailer_size();
|
||||
b[t + 2] ^= 1; // its signature
|
||||
assert_eq!(open(&b).err(), Some(Error::BadChecksum("a B-tree page's id")));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offsets_past_the_end_are_refused() {
|
||||
let clean = base(Format::Unicode);
|
||||
let h = Header::parse(&clean).unwrap();
|
||||
// Cut the file before its node tree.
|
||||
let cut = &clean[..h.nbt.ib as usize + 100];
|
||||
assert_eq!(open(cut).err(), Some(Error::OutOfBounds));
|
||||
// The header pointing far past the end.
|
||||
let mut b = clean.clone();
|
||||
b[224..232].copy_from_slice(&(1u64 << 40).to_le_bytes());
|
||||
let full = crc32(&b[8..524]);
|
||||
b[524..528].copy_from_slice(&full.to_le_bytes());
|
||||
let partial = crc32(&b[8..479]);
|
||||
b[4..8].copy_from_slice(&partial.to_le_bytes());
|
||||
assert_eq!(open(&b).err(), Some(Error::OutOfBounds));
|
||||
// Every cut of the file ends in an error or a partial read, never a panic.
|
||||
for len in (0..clean.len()).step_by(997) {
|
||||
exercise(&clean[..len], Limits::default());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_node_tree_that_points_at_itself_is_refused() {
|
||||
for format in [Format::Unicode, Format::Ansi] {
|
||||
let mut b = base(format);
|
||||
let h = Header::parse(&b).unwrap();
|
||||
let root = h.nbt.ib as usize;
|
||||
let meta = if format == Format::Unicode { 488 } else { 496 };
|
||||
assert!(b[root + meta + 3] >= 1, "the tiny layout gives the node tree several levels");
|
||||
// Its first entry's child is the root page itself.
|
||||
let n = format.id_size();
|
||||
let me: Vec<u8> = match format {
|
||||
Format::Unicode => [h.nbt.bid.to_le_bytes(), h.nbt.ib.to_le_bytes()].concat(),
|
||||
Format::Ansi => [(h.nbt.bid as u32).to_le_bytes(), (h.nbt.ib as u32).to_le_bytes()].concat(),
|
||||
};
|
||||
b[root + n..root + 3 * n].copy_from_slice(&me);
|
||||
fix_page(&mut b, format, root);
|
||||
assert!(matches!(open(&b).err(), Some(Error::Corrupt(_) | Error::NotFound(_))), "{format:?}");
|
||||
exercise(&b, Limits::default());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_page_deeper_than_the_format_allows_is_refused() {
|
||||
let mut b = base(Format::Unicode);
|
||||
let h = Header::parse(&b).unwrap();
|
||||
let root = h.nbt.ib as usize;
|
||||
b[root + 488 + 3] = 9;
|
||||
fix_page(&mut b, Format::Unicode, root);
|
||||
assert_eq!(open(&b).err(), Some(Error::Corrupt("a B-tree page's level")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_folder_that_contains_its_parent_is_a_cycle() {
|
||||
let spec = sample(Layout::new(Format::Unicode, Encryption::None), None);
|
||||
let mut all = nodes(&spec);
|
||||
// Inbox's hierarchy table lists the top of the folders, its own parent, as a subfolder.
|
||||
let inbox = all.iter().find(|n| n.parent == writer::IPM_SUBTREE && n.nid & 0x1F == 2).unwrap().nid;
|
||||
let l = &spec.layout;
|
||||
let (blocks, subs) = writer::tc(l, &[0x3001_001F], &[(writer::IPM_SUBTREE, vec![(0x3001_001F, writer::unicode("Again"))])]);
|
||||
let table = all.iter_mut().find(|n| n.nid == (inbox & !0x1F) | 0x0D).unwrap();
|
||||
table.data = writer::Data::Blocks(blocks);
|
||||
table.subnodes = subs;
|
||||
let bytes = raw(l, &all);
|
||||
let mut pst = open(&bytes).unwrap();
|
||||
let tree = pst.walk_folders().unwrap();
|
||||
assert!(tree.left_out.iter().any(|x| x.id.0 == writer::IPM_SUBTREE && matches!(x.reason, Error::Cycle(_))));
|
||||
assert_eq!(tree.folders.iter().filter(|f| f.folder.id.0 == writer::IPM_SUBTREE).count(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_heap_tree_that_points_at_itself_is_refused() {
|
||||
let mut heap = HeapBuilder::new(0xBC, 3000);
|
||||
// The first item (HID 0x20) is an index record pointing at itself.
|
||||
let mut item = 0u16.to_le_bytes().to_vec();
|
||||
item.extend_from_slice(&0x20u32.to_le_bytes());
|
||||
assert_eq!(heap.alloc(item), 0x20);
|
||||
let mut header = vec![0xB5, 2, 6, 3];
|
||||
header.extend_from_slice(&0x20u32.to_le_bytes());
|
||||
heap.user_root = heap.alloc(header);
|
||||
let heap = Heap::new(heap.finish()).unwrap();
|
||||
assert!(matches!(crate::bth::read(&heap, heap.user_root, 1000, Error::Corrupt("")).err(), Some(Error::Cycle(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn heap_ids_past_the_heap_are_refused() {
|
||||
let mut heap = HeapBuilder::new(0xBC, 3000);
|
||||
heap.user_root = heap.alloc(vec![1, 2, 3]);
|
||||
let heap = Heap::new(heap.finish()).unwrap();
|
||||
assert_eq!(heap.item(0x20).unwrap(), &[1, 2, 3]);
|
||||
assert!(heap.item(0x40).is_err());
|
||||
assert!(heap.item(0x1_0020).is_err());
|
||||
assert!(heap.item(0x21).is_err());
|
||||
assert!(Heap::new(vec![]).is_err());
|
||||
assert!(Heap::new(vec![vec![0, 0, 0xEB, 0xBC]]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_signatures_are_checked() {
|
||||
let clean = base(Format::Ansi);
|
||||
// The message store's block, found through the file's own trees.
|
||||
let limits = Limits::default();
|
||||
let mut src = Source::new(Cursor::new(&clean[..]), &limits).unwrap();
|
||||
let header = Header::parse(&src.read_at(0, HEADER_READ).unwrap()).unwrap();
|
||||
let mut ndb = Ndb::new(src, header, limits);
|
||||
let store = ndb.node(0x21).unwrap();
|
||||
let entry = ndb.block_entry(store.data).unwrap();
|
||||
// Its trailer (ANSI: cb, wSig, bid, CRC): only the signature changed. The CRC covers the
|
||||
// data alone, so it still holds.
|
||||
let ts = Format::Ansi.trailer_size();
|
||||
let trailer = entry.ib as usize + (usize::from(entry.cb) + ts).div_ceil(64) * 64 - ts;
|
||||
let mut b = clean.clone();
|
||||
b[trailer + 2] ^= 0xFF;
|
||||
assert_eq!(open(&b).err(), Some(Error::BadChecksum("a block's signature")));
|
||||
assert_eq!(signature(0x4400, 4), 0x4404);
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
//! A distribution list's stream ([MS-OXOCNTC] 2.2.2.2.4), as the writer makes it, and hostile.
|
||||
|
||||
use proptest::prelude::*;
|
||||
|
||||
use super::writer::{list_stream_bytes, one_off, wrapped_entry_id};
|
||||
use crate::{Error, ListMember, MAX_LIST_ENTRY, list_stream};
|
||||
|
||||
fn members(n: usize) -> Vec<(Vec<u8>, Vec<u8>)> {
|
||||
(0..n)
|
||||
.map(|i| {
|
||||
let o = one_off(&format!("Member {i}"), "SMTP", &format!("m{i}@example.org"), true);
|
||||
if i % 2 == 0 { (o, Vec::new()) } else { (wrapped_entry_id(0xC3, &[i as u8; 46]), o) }
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_stream_reads_back_member_by_member() {
|
||||
let m = members(40);
|
||||
let b = list_stream_bytes(&m);
|
||||
let s = list_stream(&b, 1000).unwrap();
|
||||
assert_eq!((s.more, s.damaged, s.totals_differ), (0, 0, false));
|
||||
let want: Vec<ListMember> = m.iter().map(|(e, o)| ListMember { entry_id: e, one_off: o }).collect();
|
||||
assert_eq!(s.members, want);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_empty_list_has_no_members() {
|
||||
let b = list_stream_bytes(&[]);
|
||||
let s = list_stream(&b, 1000).unwrap();
|
||||
assert!(s.members.is_empty());
|
||||
// Without its terminators too.
|
||||
assert!(list_stream(&b[..28], 1000).unwrap().members.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn past_the_cap_members_are_counted_not_kept() {
|
||||
let b = list_stream_bytes(&members(25));
|
||||
let s = list_stream(&b, 10).unwrap();
|
||||
assert_eq!((s.members.len(), s.more, s.damaged), (10, 15, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_short_header_or_another_version_is_refused() {
|
||||
let b = list_stream_bytes(&members(2));
|
||||
assert!(matches!(list_stream(&b[..27], 10), Err(Error::Corrupt(_))));
|
||||
assert!(matches!(list_stream(&[], 10), Err(Error::Corrupt(_))));
|
||||
let mut v2 = b.clone();
|
||||
v2[0] = 2;
|
||||
assert!(matches!(list_stream(&v2, 10), Err(Error::Corrupt(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_count_the_bytes_cannot_hold_is_refused() {
|
||||
let mut b = list_stream_bytes(&members(3));
|
||||
b[12..16].copy_from_slice(&u32::MAX.to_le_bytes());
|
||||
assert_eq!(list_stream(&b, 10), Err(Error::Corrupt("a distribution list's stream counts more members than it has room for")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_stream_cut_short_keeps_the_members_before_the_cut() {
|
||||
let m = members(6);
|
||||
let b = list_stream_bytes(&m);
|
||||
// Cut inside the fifth member.
|
||||
let fifth = 28 + m[..4].iter().map(|(e, o)| 12 + e.len() + o.len()).sum::<usize>();
|
||||
let s = list_stream(&b[..fifth + 20], 10).unwrap();
|
||||
assert_eq!((s.members.len(), s.damaged), (4, 2));
|
||||
assert!(!s.totals_differ);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_length_that_runs_past_the_end_stops_the_reading_there() {
|
||||
let m = members(3);
|
||||
let mut b = list_stream_bytes(&m);
|
||||
// The second member's entry id says it is longer than all there is.
|
||||
let second = 28 + 12 + m[0].0.len() + m[0].1.len();
|
||||
b[second..second + 4].copy_from_slice(&0x7FFF_FFFFu32.to_le_bytes());
|
||||
let s = list_stream(&b, 10).unwrap();
|
||||
assert_eq!((s.members.len(), s.damaged), (1, 2));
|
||||
// And one longer than an entry may be, though it would fit.
|
||||
let big = vec![(vec![0u8; MAX_LIST_ENTRY + 1], Vec::new()), m[0].clone()];
|
||||
let b = list_stream_bytes(&big);
|
||||
let s = list_stream(&b, 10).unwrap();
|
||||
assert_eq!((s.members.len(), s.damaged), (0, 2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn totals_that_lie_are_noticed_and_read_past() {
|
||||
let mut b = list_stream_bytes(&members(4));
|
||||
b[16..20].copy_from_slice(&7u32.to_le_bytes());
|
||||
let s = list_stream(&b, 10).unwrap();
|
||||
assert_eq!((s.members.len(), s.totals_differ), (4, true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extra_member_info_is_stepped_over() {
|
||||
let m = members(2);
|
||||
let mut b = list_stream_bytes(&m[..1]);
|
||||
b.truncate(b.len() - 12);
|
||||
b.extend(5u32.to_le_bytes());
|
||||
b.extend([9; 5]);
|
||||
let rest = list_stream_bytes(&m[1..]);
|
||||
b.extend_from_slice(&rest[28..]);
|
||||
b[12..16].copy_from_slice(&2u32.to_le_bytes());
|
||||
let s = list_stream(&b, 10).unwrap();
|
||||
assert_eq!(s.members.len(), 2);
|
||||
assert_eq!(s.members[1].one_off, &m[1].1[..]);
|
||||
assert!(s.totals_differ);
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn any_bytes_never_panic(b in proptest::collection::vec(any::<u8>(), 0..600), cap in 0usize..20) {
|
||||
if let Ok(s) = list_stream(&b, cap) {
|
||||
prop_assert!(s.members.len() <= cap);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_header_then_any_bytes_never_panic(count in 0u32..64, tail in proptest::collection::vec(any::<u8>(), 0..800)) {
|
||||
let mut b = list_stream_bytes(&[]);
|
||||
b.truncate(28);
|
||||
b[12..16].copy_from_slice(&count.to_le_bytes());
|
||||
b.extend(tail);
|
||||
if let Ok(s) = list_stream(&b, 8) {
|
||||
prop_assert!(s.members.len() + s.more + s.damaged <= count as usize);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn what_is_written_reads_back(n in 0usize..30) {
|
||||
let m = members(n);
|
||||
let b = list_stream_bytes(&m);
|
||||
let s = list_stream(&b, 1000).unwrap();
|
||||
prop_assert_eq!(s.members.len(), n);
|
||||
prop_assert!(!s.totals_differ);
|
||||
}
|
||||
}
|
||||
|
||||
/// The layout of [MS-OXOCNTC] 2.2.2.2.4 written out by hand, apart from the writer: one member
|
||||
/// whose entry id is 3 bytes and whose one-off is 2.
|
||||
#[test]
|
||||
fn the_header_is_read_where_the_specification_puts_it() {
|
||||
#[rustfmt::skip]
|
||||
let b: &[u8] = &[
|
||||
0x01, 0x00, 0x00, 0x00, // StreamVersion, Reserved
|
||||
0x00, 0x71, 0x02, 0x00, // BuildVersion
|
||||
0x00, 0x00, 0x00, 0x00, // DistListStreamFlags
|
||||
0x01, 0x00, 0x00, 0x00, // CountOfEntries
|
||||
0x03, 0x00, 0x00, 0x00, // TotalEntryIDSize
|
||||
0x02, 0x00, 0x00, 0x00, // TotalOneOffSize
|
||||
0x00, 0x00, 0x00, 0x00, // TotalExtraSize
|
||||
0x03, 0x00, 0x00, 0x00, 0xAA, 0xBB, 0xCC, // EntryIdSize, EntryIdData
|
||||
0x02, 0x00, 0x00, 0x00, 0xDD, 0xEE, // one-offEntryIdSize, one-offEntryIdData
|
||||
0x00, 0x00, 0x00, 0x00, // ExtraMemberInfoSize
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Terminator1, Terminator2
|
||||
];
|
||||
let s = list_stream(b, 10).unwrap();
|
||||
assert_eq!(s.members, [ListMember { entry_id: &[0xAA, 0xBB, 0xCC], one_off: &[0xDD, 0xEE] }]);
|
||||
assert!(!s.totals_differ);
|
||||
// And the writer writes the same.
|
||||
assert_eq!(list_stream_bytes(&[(vec![0xAA, 0xBB, 0xCC], vec![0xDD, 0xEE])]), b);
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
//! Tests of the .pst reader. Every file here is made by the test writer (`writer.rs`) from
|
||||
//! invented people at `example.org`, or is a sample printed in [MS-PST]; none was made by
|
||||
//! Outlook, and none is a real archive.
|
||||
|
||||
mod accounting;
|
||||
mod caps;
|
||||
mod cost;
|
||||
mod hostile;
|
||||
mod list_stream;
|
||||
mod properties;
|
||||
mod recur;
|
||||
mod roundtrip;
|
||||
mod salvage;
|
||||
mod spec_samples;
|
||||
pub use crate::test_writer as writer;
|
||||
|
||||
use std::io::{Cursor, Read};
|
||||
|
||||
use writer::{AttachSpec, FileSpec, FolderSpec, Layout, MessageSpec, int32, unicode};
|
||||
|
||||
use crate::{AttachmentContent, Encryption, Format, Limits, MessageRef, Pst};
|
||||
|
||||
/// 2026-11-04 09:30 UTC as a FILETIME.
|
||||
pub const DELIVERED: u64 = 134_067_498_000_000_000;
|
||||
|
||||
pub fn person(name: &str, user: &str, kind: i32) -> (String, String, i32) {
|
||||
(name.to_string(), format!("{user}@example.org"), kind)
|
||||
}
|
||||
|
||||
/// Bytes that are not all alike, so that a misplaced block shows.
|
||||
pub fn pattern(len: usize, seed: u8) -> Vec<u8> {
|
||||
(0..len).map(|i| (i as u8).wrapping_mul(31).wrapping_add(seed) ^ (i >> 8) as u8).collect()
|
||||
}
|
||||
|
||||
pub fn note(subject: &str) -> MessageSpec {
|
||||
MessageSpec {
|
||||
class: "IPM.Note".into(),
|
||||
subject: subject.into(),
|
||||
body: format!("{subject}\r\n\r\nWritten for the tests."),
|
||||
read: false,
|
||||
delivered: DELIVERED,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// A file with a few folders, messages with recipients and attachments, and attached messages.
|
||||
pub fn sample(layout: Layout, password: Option<&str>) -> FileSpec {
|
||||
let mut report = note("Quarterly figures");
|
||||
report.recipients = vec![person("Ada Fernsby", "ada", 1), person("Bram Okonjo", "bram", 2), person("Cleo Varga", "cleo", 3)];
|
||||
report.read = true;
|
||||
let mut forwarded = note("Minutes of the garden committee");
|
||||
forwarded.recipients = vec![person("Dee Halloran", "dee", 1)];
|
||||
forwarded.attachments = vec![AttachSpec::File { name: "minutes.txt".into(), mime: "text/plain".into(), data: b"Present: everyone.".to_vec() }];
|
||||
report.attachments = vec![
|
||||
AttachSpec::File { name: "figures.csv".into(), mime: "text/csv".into(), data: pattern(20_000, 7) },
|
||||
AttachSpec::Message(Box::new(forwarded)),
|
||||
];
|
||||
report.extra = vec![(0x8000, int32(42)), (0x0E1D, unicode("Quarterly figures"))];
|
||||
let plain = note("Lunch on Friday?");
|
||||
let mut appointment = note("Dentist");
|
||||
appointment.class = "IPM.Appointment".into();
|
||||
FileSpec {
|
||||
layout,
|
||||
password: password.map(str::to_string),
|
||||
password_ansi: false,
|
||||
name: "Personal Folders".into(),
|
||||
folders: vec![
|
||||
FolderSpec {
|
||||
name: "Inbox".into(),
|
||||
class: Some("IPF.Note".into()),
|
||||
messages: vec![report, plain],
|
||||
children: vec![FolderSpec {
|
||||
name: "Projects".into(),
|
||||
class: Some("IPF.Note".into()),
|
||||
messages: vec![note("Shed roof")],
|
||||
children: vec![],
|
||||
}],
|
||||
},
|
||||
FolderSpec { name: "Calendar".into(), class: Some("IPF.Appointment".into()), messages: vec![appointment], children: vec![] },
|
||||
FolderSpec { name: "Contacts".into(), class: Some("IPF.Contact".into()), messages: vec![], children: vec![] },
|
||||
],
|
||||
named: vec![([0x08, 0x20, 0x06, 0, 0, 0, 0, 0, 0xC0, 0, 0, 0, 0, 0, 0, 0x46], 0x8514)],
|
||||
named_strings: vec![(crate::PS_PUBLIC_STRINGS, "Keywords".into())],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn layouts() -> Vec<Layout> {
|
||||
let mut out = Vec::new();
|
||||
for format in [Format::Unicode, Format::Ansi] {
|
||||
for enc in [Encryption::None, Encryption::Permute, Encryption::Cyclic] {
|
||||
out.push(Layout::new(format, enc));
|
||||
out.push(Layout::tiny(format, enc));
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Reads everything a file has, through every call of the API, and ignores what fails: the
|
||||
/// hostile tests only ask that it ends without a panic.
|
||||
pub fn exercise(bytes: &[u8], limits: Limits) -> usize {
|
||||
let Ok(mut pst) = Pst::open_with(Cursor::new(bytes), limits) else { return 0 };
|
||||
let mut seen = 0usize;
|
||||
let _ = pst.unlock("");
|
||||
let _ = pst.store();
|
||||
let _ = pst.named_properties().map(|m| seen += m.len());
|
||||
let _ = pst.for_each_node(|_| {
|
||||
seen += 1;
|
||||
seen < 100_000
|
||||
});
|
||||
let Ok(tree) = pst.walk_folders() else { return seen };
|
||||
for entry in &tree.folders {
|
||||
let Ok(items) = pst.items(entry.folder.id) else { continue };
|
||||
let _ = pst.associated_items(entry.folder.id);
|
||||
for item in items.items {
|
||||
let mut queue = vec![MessageRef::item(item.id)];
|
||||
while let Some(m) = queue.pop() {
|
||||
let Ok(message) = pst.message(&m) else { continue };
|
||||
seen += 1 + message.recipients.len();
|
||||
let _ = pst.read_property(&m, 0x1000, 1 << 20);
|
||||
for a in message.attachments {
|
||||
match &a.content {
|
||||
AttachmentContent::Message(inner) if queue.len() < 64 => queue.push(inner.clone()),
|
||||
AttachmentContent::Data { .. } | AttachmentContent::Object { .. } => {
|
||||
if let Ok(mut r) = pst.attachment_reader(&a.id) {
|
||||
let mut sink = Vec::new();
|
||||
let _ = r.read_to_end(&mut sink);
|
||||
seen += sink.len().min(1);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
seen
|
||||
}
|
||||
|
||||
/// The first item of the folder named Inbox.
|
||||
pub fn first_item(pst: &mut Pst<Cursor<&[u8]>>) -> crate::Nid {
|
||||
let walk = pst.walk_folders().unwrap();
|
||||
let inbox = walk.folders.iter().find(|f| f.folder.name == "Inbox").unwrap().folder.id;
|
||||
pst.items(inbox).unwrap().items[0].id
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
//! Property tests: arbitrary bytes, and valid files with bytes changed, cut or inserted, are
|
||||
//! read through every call of the API without a panic and in bounded time.
|
||||
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use proptest::prelude::*;
|
||||
|
||||
use super::writer::{Layout, build};
|
||||
use super::{exercise, sample};
|
||||
use crate::{Encryption, Format, Limits};
|
||||
|
||||
fn bases() -> &'static [Vec<u8>] {
|
||||
static BASES: OnceLock<Vec<Vec<u8>>> = OnceLock::new();
|
||||
BASES.get_or_init(|| {
|
||||
vec![
|
||||
build(&sample(Layout::tiny(Format::Unicode, Encryption::Permute), None)),
|
||||
build(&sample(Layout::tiny(Format::Ansi, Encryption::Cyclic), None)),
|
||||
build(&sample(Layout::new(Format::Unicode, Encryption::None), None)),
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
/// Small enough that a hostile file can't make a case slow.
|
||||
fn limits() -> Limits {
|
||||
Limits { max_item_bytes: 1 << 20, max_attachment_bytes: 1 << 20, max_inline_value: 1 << 16, ..Default::default() }
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#![proptest_config(ProptestConfig { cases: 200, ..ProptestConfig::default() })]
|
||||
|
||||
#[test]
|
||||
fn arbitrary_bytes_never_panic(bytes in proptest::collection::vec(any::<u8>(), 0..4096)) {
|
||||
exercise(&bytes, limits());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_header_with_anything_after_it_never_panics(tail in proptest::collection::vec(any::<u8>(), 0..20_000), which in 0usize..3) {
|
||||
let base = &bases()[which];
|
||||
let mut bytes = base[..0x4400.min(base.len())].to_vec();
|
||||
bytes.extend_from_slice(&tail);
|
||||
exercise(&bytes, limits());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn changed_bytes_never_panic(which in 0usize..3, changes in proptest::collection::vec((any::<usize>(), any::<u8>()), 1..24)) {
|
||||
let mut bytes = bases()[which].clone();
|
||||
let len = bytes.len();
|
||||
for (at, v) in changes {
|
||||
bytes[at % len] = v;
|
||||
}
|
||||
exercise(&bytes, limits());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn changed_bytes_past_the_checksums_never_panic(which in 0usize..3, changes in proptest::collection::vec((any::<usize>(), any::<u8>()), 1..8)) {
|
||||
// The changes go to blocks and pages with their CRCs put right, so that what is behind
|
||||
// the checksums (the trees, heaps and tables) meets damaged structures.
|
||||
let mut bytes = bases()[which].clone();
|
||||
let format = if which == 1 { Format::Ansi } else { Format::Unicode };
|
||||
for (at, v) in changes {
|
||||
let at = 0x4400 + at % (bytes.len() - 0x4400);
|
||||
bytes[at] = v;
|
||||
super::properties::fix_around(&mut bytes, format, at);
|
||||
}
|
||||
exercise(&bytes, limits());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cut_files_never_panic(which in 0usize..3, cut in any::<usize>()) {
|
||||
let base = &bases()[which];
|
||||
exercise(&base[..cut % base.len()], limits());
|
||||
}
|
||||
}
|
||||
|
||||
/// Puts right the CRC of the page or block that `at` falls in, when it can tell which.
|
||||
pub fn fix_around(bytes: &mut [u8], format: Format, at: usize) {
|
||||
let ts = format.trailer_size();
|
||||
// A page: 512-aligned, whose trailer names a B-tree page type twice.
|
||||
let page = at - at % 512;
|
||||
if page + 512 <= bytes.len() {
|
||||
let t = page + 512 - ts;
|
||||
if matches!(bytes[t], 0x80 | 0x81) && bytes[t] == bytes[t + 1] {
|
||||
let crc = crc32(&bytes[page..t]);
|
||||
let c = t + if format == Format::Unicode { 4 } else { 8 };
|
||||
bytes[c..c + 4].copy_from_slice(&crc.to_le_bytes());
|
||||
return;
|
||||
}
|
||||
}
|
||||
// A block: scan forward to the first 64-aligned end whose trailer's cb fits.
|
||||
let start = at - at % 64;
|
||||
for end in (start + 64..=bytes.len().min(start + 8192)).step_by(64) {
|
||||
let t = end - ts;
|
||||
let cb = usize::from(u16::from_le_bytes([bytes[t], bytes[t + 1]]));
|
||||
let Some(block_start) = end.checked_sub((cb + ts).div_ceil(64) * 64) else { continue };
|
||||
if block_start <= at && block_start + cb <= t {
|
||||
let crc = crc32(&bytes[block_start..block_start + cb]);
|
||||
let c = t + if format == Format::Unicode { 4 } else { 8 };
|
||||
bytes[c..c + 4].copy_from_slice(&crc.to_le_bytes());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
use crate::crc::crc32;
|
||||
@@ -0,0 +1,66 @@
|
||||
//! How an appointment repeats ([MS-OXOCAL] 2.2.1.44), as the writer makes it, and hostile.
|
||||
|
||||
use proptest::prelude::*;
|
||||
|
||||
use super::writer::{recurrence_bytes, time_zone_bytes};
|
||||
use crate::recur::{END_DATE, FREQ_WEEKLY, PATTERN_WEEK};
|
||||
use crate::{appointment_recurrence, time_zone_key};
|
||||
|
||||
fn weekly() -> Vec<u8> {
|
||||
let changed = vec![(2_000_600, 2_000_660, 2_000_540, Some("Moved caf\u{e9}".to_string()))];
|
||||
recurrence_bytes(FREQ_WEEKLY, PATTERN_WEEK, 1, &[0b10], (END_DATE, 4, 2_030_000), &[2_000_000, 2_010_000], 1_990_000, (540, 600), &changed)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_series_reads_back() {
|
||||
let r = appointment_recurrence(&weekly()).unwrap();
|
||||
assert_eq!((r.frequency, r.pattern, r.period, r.weekdays), (FREQ_WEEKLY, PATTERN_WEEK, 1, 0b10));
|
||||
assert_eq!((r.end_type, r.count, r.start_date, r.end_date), (END_DATE, 4, 1_990_000, 2_030_000));
|
||||
assert_eq!(r.deleted, vec![2_000_000, 2_010_000]);
|
||||
assert_eq!((r.start_offset, r.end_offset), (540, 600));
|
||||
let x = &r.exceptions[0];
|
||||
assert_eq!((x.start, x.end, x.original), (2_000_600, 2_000_660, 2_000_540));
|
||||
assert_eq!(x.subject.as_deref(), Some("Moved caf\u{e9}"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_count_past_the_bytes_is_an_error_and_allocates_nothing() {
|
||||
let mut b = weekly();
|
||||
// DeletedInstanceCount, after 22 bytes of header, the one specific word and three more.
|
||||
b[38..42].copy_from_slice(&u32::MAX.to_le_bytes());
|
||||
assert!(appointment_recurrence(&b).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_cut_extended_part_keeps_the_rest() {
|
||||
let mut b = weekly();
|
||||
b.truncate(b.len() - 10);
|
||||
let r = appointment_recurrence(&b).unwrap();
|
||||
assert_eq!(r.exceptions.len(), 1);
|
||||
assert_eq!(r.exceptions[0].subject_8bit.as_deref(), Some("Moved caf\u{e9}".as_bytes()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_time_zone_names_its_key() {
|
||||
assert_eq!(time_zone_key(&time_zone_bytes("W. Europe Standard Time")).unwrap(), "W. Europe Standard Time");
|
||||
assert!(time_zone_key(&[1, 1, 0, 0]).is_err());
|
||||
assert!(time_zone_key(&time_zone_bytes("")).is_err());
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn any_bytes_never_panic(b in prop::collection::vec(any::<u8>(), 0..600)) {
|
||||
let _ = appointment_recurrence(&b);
|
||||
let _ = time_zone_key(&b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_damaged_series_never_panics(flips in prop::collection::vec((any::<prop::sample::Index>(), any::<u8>()), 1..12)) {
|
||||
let mut b = weekly();
|
||||
for (at, v) in flips {
|
||||
let i = at.index(b.len());
|
||||
b[i] = v;
|
||||
}
|
||||
let _ = appointment_recurrence(&b);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
//! Files from the test writer read back whole, in every format, encoding and layout.
|
||||
|
||||
use std::io::{Cursor, Read};
|
||||
|
||||
use super::writer::{FolderSpec, Layout, build};
|
||||
use super::{DELIVERED, layouts, pattern, sample};
|
||||
use crate::{AttachmentContent, Encryption, Error, FolderKind, Format, MessageRef, Nid, PropName, PropValue, Pst, RecipientKind};
|
||||
|
||||
fn open(bytes: &[u8]) -> Pst<Cursor<&[u8]>> {
|
||||
Pst::open(Cursor::new(bytes)).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::cognitive_complexity, reason = "one walk over every layout, with its checks in line")]
|
||||
fn every_layout_reads_back() {
|
||||
for layout in layouts() {
|
||||
let what = format!("{:?} {:?} block {}", layout.format, layout.encryption, layout.block_data);
|
||||
let bytes = build(&sample(layout.clone(), None));
|
||||
let mut pst = open(&bytes);
|
||||
assert_eq!(pst.format(), layout.format, "{what}");
|
||||
assert_eq!(pst.encryption(), layout.encryption, "{what}");
|
||||
assert!(!pst.password_required());
|
||||
|
||||
let store = pst.store().unwrap();
|
||||
assert_eq!(store.display_name.as_deref(), Some("Personal Folders"), "{what}");
|
||||
let ipm = store.ipm_subtree.unwrap();
|
||||
|
||||
let tree = pst.walk_folders().unwrap();
|
||||
assert!(tree.left_out.is_empty(), "{what}: {:?}", tree.left_out);
|
||||
let names: Vec<(&str, usize, bool)> = tree.folders.iter().map(|f| (f.folder.name.as_str(), f.depth, f.in_ipm_subtree)).collect();
|
||||
assert_eq!(
|
||||
names,
|
||||
vec![
|
||||
("", 0, false),
|
||||
("Top of Personal Folders", 1, true),
|
||||
("Inbox", 2, true),
|
||||
("Projects", 3, true),
|
||||
("Calendar", 2, true),
|
||||
("Contacts", 2, true),
|
||||
("Search Root", 1, false),
|
||||
],
|
||||
"{what}"
|
||||
);
|
||||
assert_eq!(tree.folders[1].folder.id, ipm);
|
||||
let kinds: Vec<FolderKind> = tree.folders.iter().map(|f| f.folder.kind.clone()).collect();
|
||||
assert_eq!(kinds[2], FolderKind::Mail);
|
||||
assert_eq!(kinds[4], FolderKind::Calendar);
|
||||
assert_eq!(kinds[5], FolderKind::Contacts);
|
||||
let inbox = tree.folders[2].folder.clone();
|
||||
assert_eq!((inbox.content_count, inbox.unread_count, inbox.has_subfolders), (2, 1, true), "{what}");
|
||||
assert_eq!(inbox.parent, Some(ipm));
|
||||
|
||||
let items = pst.items(inbox.id).unwrap();
|
||||
assert!(items.left_out.is_empty(), "{what}: {:?}", items.left_out);
|
||||
let classes: Vec<Option<&str>> = items.items.iter().map(|i| i.class.as_deref()).collect();
|
||||
assert_eq!(classes, vec![Some("IPM.Note"), Some("IPM.Note")], "{what}");
|
||||
assert_eq!(items.items[0].properties.text(0x0037).as_deref(), Some("Quarterly figures"));
|
||||
assert!(pst.associated_items(inbox.id).unwrap().items.is_empty());
|
||||
|
||||
let m = pst.message(&MessageRef::item(items.items[0].id)).unwrap();
|
||||
assert!(m.left_out.is_empty(), "{what}: {:?}", m.left_out);
|
||||
assert_eq!(m.class.as_deref(), Some("IPM.Note"));
|
||||
assert_eq!(m.properties.text(0x0037).as_deref(), Some("Quarterly figures"));
|
||||
assert_eq!(m.properties.text(0x1000).as_deref(), Some("Quarterly figures\r\n\r\nWritten for the tests."));
|
||||
assert_eq!(m.properties.time(0x0E06), Some(DELIVERED));
|
||||
assert_eq!(m.properties.int(0x8000), Some(42));
|
||||
let people: Vec<(Option<String>, Option<String>, RecipientKind)> = m.recipients.iter().map(|r| (r.name(), r.address(), r.kind())).collect();
|
||||
assert_eq!(
|
||||
people,
|
||||
vec![
|
||||
(Some("Ada Fernsby".into()), Some("ada@example.org".into()), RecipientKind::To),
|
||||
(Some("Bram Okonjo".into()), Some("bram@example.org".into()), RecipientKind::Cc),
|
||||
(Some("Cleo Varga".into()), Some("cleo@example.org".into()), RecipientKind::Bcc),
|
||||
],
|
||||
"{what}"
|
||||
);
|
||||
|
||||
assert_eq!(m.attachments.len(), 2, "{what}");
|
||||
let csv = &m.attachments[0];
|
||||
assert_eq!(csv.file_name.as_deref(), Some("figures.csv"));
|
||||
assert_eq!(csv.mime_type.as_deref(), Some("text/csv"));
|
||||
assert_eq!(csv.method, Some(1));
|
||||
assert_eq!(csv.content, AttachmentContent::Data { size: 20_000 });
|
||||
assert!(matches!(csv.properties.get(0x3701), Some(PropValue::Large { size: 20_000 })));
|
||||
assert_eq!(pst.attachment_bytes(&csv.id).unwrap(), pattern(20_000, 7), "{what}");
|
||||
let mut r = pst.attachment_reader(&csv.id).unwrap();
|
||||
assert_eq!(r.size(), 20_000);
|
||||
let mut first = [0u8; 10];
|
||||
r.read_exact(&mut first).unwrap();
|
||||
assert_eq!(first.to_vec(), pattern(10, 7));
|
||||
|
||||
let AttachmentContent::Message(inner) = &m.attachments[1].content else { panic!("{what}: {:?}", m.attachments[1].content) };
|
||||
assert_eq!(inner.depth(), 1);
|
||||
let fwd = pst.message(inner).unwrap();
|
||||
assert_eq!(fwd.properties.text(0x0037).as_deref(), Some("Minutes of the garden committee"), "{what}");
|
||||
assert_eq!(fwd.recipients.len(), 1);
|
||||
assert_eq!(fwd.recipients[0].address().as_deref(), Some("dee@example.org"));
|
||||
assert_eq!(fwd.attachments.len(), 1);
|
||||
assert_eq!(pst.attachment_bytes(&fwd.attachments[0].id).unwrap(), b"Present: everyone.");
|
||||
|
||||
let named = pst.named_properties().unwrap();
|
||||
let first = named.get(0x8000).unwrap();
|
||||
assert_eq!(first.name, PropName::Id(0x8514));
|
||||
assert_eq!(named.get(0x8001).unwrap().name, PropName::Name("Keywords".into()));
|
||||
assert_eq!(named.find(&crate::PS_PUBLIC_STRINGS, &PropName::Name("Keywords".into())), Some(0x8001));
|
||||
|
||||
let mut messages = 0;
|
||||
pst.for_each_node(|n| {
|
||||
messages += usize::from(n.id.kind() == crate::node_types::NORMAL_MESSAGE);
|
||||
true
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(messages, 4, "{what}");
|
||||
|
||||
let calendar = tree.folders[4].folder.id;
|
||||
let cal = pst.items(calendar).unwrap();
|
||||
assert_eq!(cal.items[0].class.as_deref(), Some("IPM.Appointment"));
|
||||
assert!(pst.items(tree.folders[5].folder.id).unwrap().items.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_large_body_is_left_for_later_and_read_on_its_own() {
|
||||
let mut spec = sample(Layout::new(Format::Unicode, Encryption::Cyclic), None);
|
||||
let body: String = "All work and no play. ".repeat(30_000);
|
||||
spec.folders[0].messages[1].body = body.clone();
|
||||
let bytes = build(&spec);
|
||||
let mut pst = Pst::open_with(Cursor::new(&bytes[..]), crate::Limits { max_inline_value: 64 * 1024, ..Default::default() }).unwrap();
|
||||
let inbox = pst.walk_folders().unwrap().folders[2].folder.id;
|
||||
let item = pst.items(inbox).unwrap().items[1].id;
|
||||
let m = pst.message(&MessageRef::item(item)).unwrap();
|
||||
let size = (body.len() * 2) as u64;
|
||||
assert_eq!(m.properties.get(0x1000), Some(&PropValue::Large { size }));
|
||||
assert_eq!(pst.read_property(&m.id, 0x1000, 4 << 20).unwrap(), Some(PropValue::String(body)));
|
||||
assert_eq!(pst.read_property(&m.id, 0x1000, 1000).unwrap(), Some(PropValue::Large { size }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn many_folders_and_items_spread_over_pages_and_blocks() {
|
||||
for format in [Format::Unicode, Format::Ansi] {
|
||||
let mut spec = sample(Layout::tiny(format, Encryption::Permute), None);
|
||||
spec.folders[2].children = (0..30).map(|i| FolderSpec { name: format!("Box {i}"), ..Default::default() }).collect();
|
||||
spec.folders[1].messages = (0..120).map(|i| super::note(&format!("Visit {i}"))).collect();
|
||||
let bytes = build(&spec);
|
||||
let mut pst = open(&bytes);
|
||||
let tree = pst.walk_folders().unwrap();
|
||||
assert!(tree.left_out.is_empty());
|
||||
let boxes: Vec<&str> =
|
||||
tree.folders.iter().filter(|f| f.depth == 3 && f.folder.name.starts_with("Box")).map(|f| f.folder.name.as_str()).collect();
|
||||
assert_eq!(boxes.len(), 30);
|
||||
assert_eq!(boxes[29], "Box 29");
|
||||
let calendar = tree.folders.iter().find(|f| f.folder.name == "Calendar").unwrap().folder.id;
|
||||
let items = pst.items(calendar).unwrap();
|
||||
assert_eq!(items.items.len(), 120);
|
||||
assert_eq!(items.items[119].properties.text(0x0037).as_deref(), Some("Visit 119"));
|
||||
let last = pst.message(&MessageRef::item(items.items[119].id)).unwrap();
|
||||
assert_eq!(last.properties.text(0x0037).as_deref(), Some("Visit 119"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_password_is_asked_for_before_anything_is_read() {
|
||||
for format in [Format::Unicode, Format::Ansi] {
|
||||
let bytes = build(&sample(Layout::new(format, Encryption::Permute), Some("tomato soup")));
|
||||
let mut pst = open(&bytes);
|
||||
assert!(pst.password_required());
|
||||
assert_eq!(pst.store().err(), Some(Error::PasswordRequired));
|
||||
assert_eq!(pst.walk_folders().err(), Some(Error::PasswordRequired));
|
||||
assert_eq!(pst.folder(Nid(0x122)).err(), Some(Error::PasswordRequired));
|
||||
assert_eq!(pst.items(Nid(0x8022)).err(), Some(Error::PasswordRequired));
|
||||
assert_eq!(pst.message(&MessageRef::item(Nid(0x200004))).err(), Some(Error::PasswordRequired));
|
||||
assert_eq!(pst.named_properties().err(), Some(Error::PasswordRequired));
|
||||
assert_eq!(pst.for_each_node(|_| true).err(), Some(Error::PasswordRequired));
|
||||
assert!(!pst.check_password(""));
|
||||
assert!(!pst.check_password("tomato"));
|
||||
assert_eq!(pst.unlock("Tomato soup"), Err(Error::WrongPassword));
|
||||
assert!(pst.password_required());
|
||||
assert!(pst.check_password("tomato soup"));
|
||||
pst.unlock("tomato soup").unwrap();
|
||||
assert_eq!(pst.walk_folders().unwrap().folders.len(), 7);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_file_without_a_password_takes_any() {
|
||||
let bytes = build(&sample(Layout::new(Format::Unicode, Encryption::None), None));
|
||||
let mut pst = open(&bytes);
|
||||
assert!(pst.check_password("anything"));
|
||||
pst.unlock("anything").unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_search_folder_lists_no_items() {
|
||||
let bytes = build(&sample(Layout::new(Format::Unicode, Encryption::None), None));
|
||||
let mut pst = open(&bytes);
|
||||
assert_eq!(pst.items(Nid((0x500 << 5) | 0x03)).err(), Some(Error::Unsupported("a search folder's items")));
|
||||
assert_eq!(pst.items(Nid(0x21)).err(), Some(Error::Unsupported("a node that is not a folder")));
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
//! A damaged page or block loses what it holds and no more: the rest of the file reads, and
|
||||
//! each loss is named (in a walk's, list's or message's `left_out`, and a page of the file's
|
||||
//! own trees in [`Pst::damage`]).
|
||||
|
||||
use std::io::Cursor;
|
||||
|
||||
use super::sample;
|
||||
use super::writer::{Layout, build};
|
||||
use crate::header::{HEADER_READ, Header};
|
||||
use crate::ndb::Ndb;
|
||||
use crate::source::Source;
|
||||
use crate::{Encryption, Error, Format, Limits, MessageRef, Nid, Pst};
|
||||
|
||||
fn base() -> Vec<u8> {
|
||||
build(&sample(Layout::tiny(Format::Unicode, Encryption::Permute), None))
|
||||
}
|
||||
|
||||
/// The leaf pages of one tree (0x81 the node tree, 0x80 the block tree): offset and first key.
|
||||
fn leaves(bytes: &[u8], ptype: u8) -> Vec<(usize, u64)> {
|
||||
(0x4400..bytes.len().saturating_sub(511))
|
||||
.step_by(512)
|
||||
.filter(|&ib| bytes[ib + 496] == ptype && bytes[ib + 497] == ptype && bytes[ib + 491] == 0)
|
||||
.map(|ib| (ib, u64::from_le_bytes(bytes[ib..ib + 8].try_into().unwrap())))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn ndb(bytes: &[u8]) -> Ndb<Cursor<&[u8]>> {
|
||||
let limits = Limits::default();
|
||||
let mut src = Source::new(Cursor::new(bytes), &limits).unwrap();
|
||||
let header = Header::parse(&src.read_at(0, HEADER_READ).unwrap()).unwrap();
|
||||
Ndb::new(src, header, limits)
|
||||
}
|
||||
|
||||
/// Every item of every folder: read, or the reason it wasn't.
|
||||
fn read_all(pst: &mut Pst<Cursor<&[u8]>>) -> (usize, Vec<(Nid, Error)>) {
|
||||
let tree = pst.walk_folders().unwrap();
|
||||
let (mut read, mut lost) = (0, Vec::new());
|
||||
for f in &tree.folders {
|
||||
let Ok(list) = pst.items(f.folder.id) else { continue };
|
||||
for item in list.items {
|
||||
match pst.message(&MessageRef::item(item.id)) {
|
||||
Ok(_) => read += 1,
|
||||
Err(e) => lost.push((item.id, e)),
|
||||
}
|
||||
}
|
||||
}
|
||||
(read, lost)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_damaged_leaf_page_of_the_node_tree_loses_its_nodes_only() {
|
||||
let mut bytes = base();
|
||||
// The leaf with the highest ids holds messages alone.
|
||||
let &(ib, first) = leaves(&bytes, 0x81).iter().max_by_key(|l| l.1).unwrap();
|
||||
assert_eq!(first & 0x1F, 4, "a leaf of items");
|
||||
bytes[ib + 20] ^= 0xFF;
|
||||
let mut pst = Pst::open(Cursor::new(&bytes[..])).unwrap();
|
||||
let tree = pst.walk_folders().unwrap();
|
||||
assert_eq!(tree.folders.len(), 7, "every folder still read");
|
||||
let (read, lost) = read_all(&mut pst);
|
||||
assert!(read >= 1 && !lost.is_empty(), "{read} read, {lost:?}");
|
||||
assert!(lost.iter().all(|(_, e)| *e == Error::BadChecksum("a B-tree page")));
|
||||
let mut nodes = 0;
|
||||
pst.for_each_node(|_| {
|
||||
nodes += 1;
|
||||
true
|
||||
})
|
||||
.unwrap();
|
||||
assert!(nodes > 0);
|
||||
assert_eq!(pst.damage().len(), 1);
|
||||
assert_eq!((pst.damage()[0].offset, pst.damage()[0].what), (ib as u64, "a page of the node tree"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_damaged_leaf_page_of_the_block_tree_loses_the_data_it_finds_only() {
|
||||
let mut bytes = base();
|
||||
let all = leaves(&bytes, 0x80);
|
||||
// The last leaf: blocks written last, of the search root's own tables.
|
||||
let &(ib, _) = all.iter().max_by_key(|l| l.1).unwrap();
|
||||
bytes[ib + 20] ^= 0xFF;
|
||||
let mut pst = Pst::open(Cursor::new(&bytes[..])).unwrap();
|
||||
let tree = pst.walk_folders().unwrap();
|
||||
assert!(tree.folders.len() >= 5, "{}", tree.folders.len());
|
||||
let (read, _) = read_all(&mut pst);
|
||||
assert!(read >= 3, "{read}");
|
||||
let lost_folders = tree.left_out.iter().filter(|l| l.reason == Error::BadChecksum("a B-tree page")).count();
|
||||
assert!(lost_folders >= 1 || pst.damage().iter().any(|d| d.offset == ib as u64), "{:?}", tree.left_out);
|
||||
assert!(pst.damage().iter().all(|d| d.what == "a page of the block tree" && d.offset == ib as u64), "{:?}", pst.damage());
|
||||
assert_eq!(pst.damage().len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_damaged_block_of_one_message_loses_that_message_only() {
|
||||
let clean = base();
|
||||
let mut pst = Pst::open(Cursor::new(&clean[..])).unwrap();
|
||||
let item = super::first_item(&mut pst);
|
||||
let mut n = ndb(&clean);
|
||||
let entry = n.node(item.0).unwrap();
|
||||
// Its top block: its one data block, or the block of its data tree.
|
||||
let ib = n.block_entry(entry.data).unwrap().ib as usize;
|
||||
let mut bytes = clean.clone();
|
||||
bytes[ib + 1] ^= 0xFF;
|
||||
let mut pst = Pst::open(Cursor::new(&bytes[..])).unwrap();
|
||||
let tree = pst.walk_folders().unwrap();
|
||||
assert!(tree.left_out.is_empty());
|
||||
let (read, lost) = read_all(&mut pst);
|
||||
assert_eq!(lost, vec![(item, Error::BadChecksum("a block"))]);
|
||||
assert_eq!(read, 3, "the file's other items");
|
||||
assert!(pst.damage().is_empty(), "a block of a message is the message's loss, not the file's");
|
||||
// Its other parts are still there to be read.
|
||||
let list = pst.items(tree.folders[2].folder.id).unwrap();
|
||||
assert_eq!(list.items[0].properties.text(0x0037).as_deref(), Some("Quarterly figures"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_damaged_folder_is_left_out_and_the_rest_read() {
|
||||
let clean = base();
|
||||
let mut pst = Pst::open(Cursor::new(&clean[..])).unwrap();
|
||||
let calendar = pst.walk_folders().unwrap().folders.iter().find(|f| f.folder.name == "Calendar").unwrap().folder.id;
|
||||
let mut n = ndb(&clean);
|
||||
let entry = n.node(calendar.0).unwrap();
|
||||
let mut bid = entry.data;
|
||||
if crate::ndb::is_internal(bid) {
|
||||
// The first data block under the data tree's top block.
|
||||
let top = n.block(bid).unwrap();
|
||||
bid = u64::from_le_bytes(top[8..16].try_into().unwrap());
|
||||
}
|
||||
let ib = n.block_entry(bid).unwrap().ib as usize;
|
||||
let mut bytes = clean.clone();
|
||||
bytes[ib + 1] ^= 0xFF;
|
||||
let mut pst = Pst::open(Cursor::new(&bytes[..])).unwrap();
|
||||
let tree = pst.walk_folders().unwrap();
|
||||
let names: Vec<&str> = tree.folders.iter().map(|f| f.folder.name.as_str()).collect();
|
||||
assert_eq!(names, vec!["", "Top of Personal Folders", "Inbox", "Projects", "Contacts", "Search Root"]);
|
||||
assert_eq!(tree.left_out.len(), 1);
|
||||
assert_eq!((tree.left_out[0].id, &tree.left_out[0].reason), (calendar, &Error::BadChecksum("a block")));
|
||||
let (read, lost) = read_all(&mut pst);
|
||||
assert_eq!((read, lost.len()), (3, 0), "the items of the other folders");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn damaged_pages_are_named_to_a_cap_and_counted_past_it() {
|
||||
// Each named once, and quickly (a set, not a scan): 200,000 notes, 50,000 pages.
|
||||
let bytes = base();
|
||||
let mut ndb = ndb(&bytes);
|
||||
let e = Error::BadChecksum("a B-tree page");
|
||||
for i in 0..200_000u64 {
|
||||
ndb.note((i % 50_000) * 512, 0x81, &e);
|
||||
}
|
||||
// A cap reached, or the reader failing, is not damage.
|
||||
ndb.note(1 << 40, 0x81, &Error::Cap(crate::Cap::TotalRead));
|
||||
assert_eq!(ndb.damage.len(), crate::MAX_DAMAGE);
|
||||
let last = ndb.damage.last().unwrap();
|
||||
assert_eq!(last.pages, 50_000 - crate::MAX_DAMAGE as u64 + 1);
|
||||
assert_eq!(last.what, "pages of the node tree, from this one");
|
||||
assert!(ndb.damage[..crate::MAX_DAMAGE - 1].iter().all(|d| d.pages == 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_walk_of_the_nodes_that_reaches_a_cap_says_so() {
|
||||
// At every allowance a walk either meets every node or ends with the cap: never fewer
|
||||
// nodes and Ok.
|
||||
let bytes = base();
|
||||
let count = |pst: &mut Pst<Cursor<&[u8]>>| {
|
||||
let mut nodes = 0usize;
|
||||
pst.for_each_node(|_| {
|
||||
nodes += 1;
|
||||
true
|
||||
})
|
||||
.map(|()| nodes)
|
||||
};
|
||||
let all = count(&mut Pst::open(Cursor::new(&bytes[..])).unwrap()).unwrap();
|
||||
let mut capped = 0;
|
||||
for floor in (0..bytes.len() as u64).step_by(512) {
|
||||
let limits = Limits { read_multiple: 0, read_floor: floor, ..Default::default() };
|
||||
let Ok(mut pst) = Pst::open_with(Cursor::new(&bytes[..]), limits) else { continue };
|
||||
match count(&mut pst) {
|
||||
Ok(n) => assert_eq!(n, all, "at {floor}"),
|
||||
Err(e) => {
|
||||
assert_eq!(e, Error::Cap(crate::Cap::TotalRead), "at {floor}");
|
||||
capped += 1;
|
||||
}
|
||||
}
|
||||
assert!(pst.damage().is_empty(), "a cap is not damage, at {floor}");
|
||||
}
|
||||
assert!(capped > 0);
|
||||
}
|
||||
|
||||
/// A file whose reader fails once, on its `fail`th read: with an error, or (`short`) by
|
||||
/// giving nothing, as a file cut short under the import does.
|
||||
struct Flaky<'a> {
|
||||
inner: Cursor<&'a [u8]>,
|
||||
reads: usize,
|
||||
fail: usize,
|
||||
short: bool,
|
||||
}
|
||||
|
||||
impl std::io::Read for Flaky<'_> {
|
||||
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||
self.reads += 1;
|
||||
if self.reads == self.fail {
|
||||
return if self.short { Ok(0) } else { Err(std::io::Error::other("the disk")) };
|
||||
}
|
||||
self.inner.read(buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::io::Seek for Flaky<'_> {
|
||||
fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
|
||||
self.inner.seek(pos)
|
||||
}
|
||||
}
|
||||
|
||||
/// Each answer of a walk, every listing and every message, as text, and how many reads it took.
|
||||
fn answers<R: std::io::Read + std::io::Seek>(pst: &mut Pst<R>, folders: &[Nid], items: &[Nid]) -> Vec<Result<String, Error>> {
|
||||
let mut out = vec![pst.walk_folders().map(|t| format!("{t:?}"))];
|
||||
out.extend(folders.iter().map(|&f| pst.items(f).map(|l| format!("{l:?}"))));
|
||||
out.extend(items.iter().map(|&i| pst.message(&MessageRef::item(i)).map(|m| format!("{m:?}"))));
|
||||
out
|
||||
}
|
||||
|
||||
/// Whatever goes wrong with the reader under the file, or however little it may read, each
|
||||
/// call is either the whole answer or that error: never an answer with parts missing.
|
||||
#[test]
|
||||
fn a_reader_that_fails_or_runs_out_ends_the_call() {
|
||||
let bytes = base();
|
||||
let mut pst = Pst::open(Cursor::new(&bytes[..])).unwrap();
|
||||
let tree = pst.walk_folders().unwrap();
|
||||
let folders: Vec<Nid> = tree.folders.iter().map(|f| f.folder.id).collect();
|
||||
let mut items = Vec::new();
|
||||
for &f in &folders {
|
||||
items.extend(pst.items(f).unwrap().items.iter().map(|i| i.id));
|
||||
}
|
||||
assert!(items.len() >= 3);
|
||||
let whole = answers(&mut pst, &folders, &items);
|
||||
assert!(whole.iter().all(Result::is_ok));
|
||||
let mut flaky = Flaky { inner: Cursor::new(&bytes[..]), reads: 0, fail: 0, short: false };
|
||||
let mut fresh = Pst::open(&mut flaky).unwrap();
|
||||
answers(&mut fresh, &folders, &items);
|
||||
drop(fresh);
|
||||
let reads = flaky.reads;
|
||||
let check = |got: Vec<Result<String, Error>>, expected: fn(&Error) -> bool, at: &str| {
|
||||
for (n, (got, whole)) in got.iter().zip(&whole).enumerate() {
|
||||
match got {
|
||||
Ok(s) => assert_eq!(Some(s), whole.as_ref().ok(), "call {n} {at}"),
|
||||
Err(e) => assert!(expected(e), "call {n} {at}: {e:?}"),
|
||||
}
|
||||
}
|
||||
};
|
||||
let mut failed = 0;
|
||||
for (fail, short) in (1..=reads).flat_map(|n| [(n, false), (n, true)]) {
|
||||
let Ok(mut pst) = Pst::open(Flaky { inner: Cursor::new(&bytes[..]), reads: 0, fail, short }) else { continue };
|
||||
let got = answers(&mut pst, &folders, &items);
|
||||
failed += got.iter().filter(|r| r.is_err()).count();
|
||||
check(got, |e| matches!(e, Error::Io(_)), &format!("failing read {fail}, short {short}"));
|
||||
assert!(pst.damage().is_empty(), "failing read {fail}, short {short}");
|
||||
}
|
||||
assert!(failed > 0);
|
||||
let mut capped = 0;
|
||||
for floor in (0..bytes.len() as u64 * 4).step_by(256) {
|
||||
let limits = Limits { read_multiple: 0, read_floor: floor, ..Default::default() };
|
||||
let Ok(mut pst) = Pst::open_with(Cursor::new(&bytes[..]), limits) else { continue };
|
||||
let got = answers(&mut pst, &folders, &items);
|
||||
capped += got.iter().filter(|r| r.is_err()).count();
|
||||
check(got, |e| *e == Error::Cap(crate::Cap::TotalRead), &format!("allowed {floor}"));
|
||||
}
|
||||
assert!(capped > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_page_below_the_root_with_a_level_the_format_lacks_is_damage() {
|
||||
let mut bytes = base();
|
||||
let &(ib, first) = leaves(&bytes, 0x81).iter().max_by_key(|l| l.1).unwrap();
|
||||
assert_eq!(first & 0x1F, 4, "a leaf of items");
|
||||
bytes[ib + 488 + 3] = 9;
|
||||
let t = 512 - Format::Unicode.trailer_size();
|
||||
let crc = crate::crc32(&bytes[ib..ib + t]);
|
||||
bytes[ib + t + 4..ib + t + 8].copy_from_slice(&crc.to_le_bytes());
|
||||
let mut pst = Pst::open(Cursor::new(&bytes[..])).unwrap();
|
||||
pst.for_each_node(|_| true).unwrap();
|
||||
assert_eq!(pst.damage().len(), 1);
|
||||
assert_eq!((pst.damage()[0].offset, &pst.damage()[0].reason), (ib as u64, &Error::Corrupt("a B-tree page's level")));
|
||||
assert_eq!(pst.walk_folders().unwrap().folders.len(), 7, "every folder still read");
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
//! The reader against the sample structures printed in [MS-PST] section 3 (a header, the heap
|
||||
//! of a message store's property context, the heap of a hierarchy table), and the algorithms
|
||||
//! of section 5 against the values the specification gives.
|
||||
|
||||
use crate::crc::{crc32, table_head};
|
||||
use crate::crypt::{Encryption, decode, encode, tables};
|
||||
use crate::error::Error;
|
||||
use crate::header::{Format, Header};
|
||||
use crate::heap::Heap;
|
||||
use crate::ndb::signature;
|
||||
use crate::prop::PropValue;
|
||||
|
||||
#[test]
|
||||
fn crc_table_is_the_specifications() {
|
||||
assert_eq!(table_head(), [0x0000_0000, 0x7707_3096, 0xEE0E_612C, 0x2D02_EF8D]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sample_header_checks_out() {
|
||||
// 3.2: dwCRCPartial 0x379aa90e, dwCRCFull 0x1fd283d6.
|
||||
assert_eq!(crc32(&HEADER[8..479]), 0x379A_A90E);
|
||||
assert_eq!(crc32(&HEADER[8..524]), 0x1FD2_83D6);
|
||||
let h = Header::parse(HEADER).unwrap();
|
||||
assert_eq!(h.format, Format::Unicode);
|
||||
assert_eq!(h.version, 23);
|
||||
assert_eq!(h.encryption, Encryption::Permute);
|
||||
assert_eq!(h.file_eof, 0x9F_2400);
|
||||
assert_eq!((h.nbt.bid, h.nbt.ib), (0x24B, 0x90_5200));
|
||||
assert_eq!((h.bbt.bid, h.bbt.ib), (0x253, 0x90_0A00));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sample_page_signatures() {
|
||||
// 3.3 and 3.5: the intermediate page at 0x8200 (bid 0x206) and the BBT leaf at 0x900200
|
||||
// (bid 0x246) carry 0x8006 and 0x00D6.
|
||||
assert_eq!(signature(0x8200, 0x206), 0x8006);
|
||||
assert_eq!(signature(0x90_0200, 0x246), 0x00D6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn permutation_tables_undo_each_other() {
|
||||
let (r, s, i) = tables();
|
||||
for b in 0..=255u8 {
|
||||
assert_eq!(i[usize::from(r[usize::from(b)])], b);
|
||||
assert_eq!(s[usize::from(s[usize::from(b)])], b);
|
||||
}
|
||||
for method in [Encryption::Permute, Encryption::Cyclic] {
|
||||
let original: Vec<u8> = (0..=255u8).cycle().take(1000).collect();
|
||||
let mut data = original.clone();
|
||||
encode(method, &mut data, 0x1234_5678);
|
||||
assert_ne!(data, original);
|
||||
decode(method, &mut data, 0x1234_5678);
|
||||
assert_eq!(data, original);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sample_message_store_heap() {
|
||||
// 3.8–3.10: the message store's PC of a Unicode file named "UNICODE1".
|
||||
let heap = Heap::new(vec![STORE_HEAP.to_vec()]).unwrap();
|
||||
assert_eq!(heap.client, 0xBC);
|
||||
assert_eq!(heap.user_root, 0x20);
|
||||
let node = crate::ltp::NodeData { heap, subnodes: Default::default() };
|
||||
let records = crate::ltp::pc_records(&node).unwrap();
|
||||
assert_eq!(records.len(), 11);
|
||||
let ids: Vec<u16> = records.iter().map(|r| r.0).collect();
|
||||
assert!(ids.contains(&0x3001) && ids.contains(&0x67FF) && ids.contains(&0x35E0));
|
||||
let (_, ty, raw) = records.iter().find(|r| r.0 == 0x3001).copied().unwrap();
|
||||
assert_eq!(ty, 0x001F);
|
||||
let name = crate::prop::stored(ty, node.heap.item(raw).unwrap()).unwrap();
|
||||
assert_eq!(name, PropValue::String("UNICODE1".into()));
|
||||
let (_, _, raw) = records.iter().find(|r| r.0 == 0x0FF9).copied().unwrap();
|
||||
assert_eq!(node.heap.item(raw).unwrap(), &[0x22, 0x9D, 0xB5, 0x0A, 0xDC, 0xD9, 0x94, 0x43, 0x85, 0xDE, 0x90, 0xAE, 0xB0, 0x7D, 0x12, 0x70]);
|
||||
let (_, _, raw) = records.iter().find(|r| r.0 == 0x35DF).copied().unwrap();
|
||||
assert_eq!(raw, 0x89);
|
||||
let (_, _, raw) = records.iter().find(|r| r.0 == 0x67FF).copied().unwrap();
|
||||
assert_eq!(raw, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sample_hierarchy_table_heap() {
|
||||
// 3.11–3.12: the root folder's hierarchy table, three rows in the heap.
|
||||
let heap = Heap::new(vec![HIERARCHY_HEAP.to_vec()]).unwrap();
|
||||
assert_eq!(heap.client, 0x7C);
|
||||
let info = heap.item(heap.user_root).unwrap();
|
||||
assert_eq!(info[1], 0x0D);
|
||||
let index = crate::bth::read(&heap, 0x20, 1000, Error::Corrupt("")).unwrap();
|
||||
let rows: Vec<(u32, u32)> =
|
||||
index.records.iter().map(|r| (u32::from_le_bytes(r[0..4].try_into().unwrap()), u32::from_le_bytes(r[4..8].try_into().unwrap()))).collect();
|
||||
assert_eq!(rows, vec![(0x2223, 2), (0x8022, 0), (0x8042, 1)]);
|
||||
let matrix = heap.item(0x80).unwrap();
|
||||
assert_eq!(matrix.len(), 3 * 55);
|
||||
// Row 0's display name (column 0x3001 at offset 8) is "Top of Personal Folders".
|
||||
let hid = u32::from_le_bytes(matrix[8..12].try_into().unwrap());
|
||||
assert_eq!(crate::bytes::utf16(heap.item(hid).unwrap()), "Top of Personal Folders");
|
||||
}
|
||||
|
||||
pub const HEADER: &[u8] = &[
|
||||
0x21, 0x42, 0x44, 0x4E, 0x0E, 0xA9, 0x9A, 0x37, 0x53, 0x4D, 0x17, 0x00, 0x13, 0x00, 0x01, 0x01, 0x5C, 0x07, 0x00, 0x00, 0xD0, 0x7B, 0x99, 0x0B,
|
||||
0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x54, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x45, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00,
|
||||
0x00, 0x04, 0x00, 0x00, 0x04, 0x04, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x02, 0x00, 0x01, 0x00, 0x04, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00,
|
||||
0x00, 0x04, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00,
|
||||
0x04, 0x04, 0x00, 0x00, 0x04, 0x04, 0x00, 0x00, 0x04, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00,
|
||||
0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00,
|
||||
0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00,
|
||||
0x0F, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x9F, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x44, 0x9B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0xF2, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x4B, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x52, 0x90, 0x00, 0x00, 0x00, 0x00, 0x00, 0x53, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x0A, 0x90, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x01, 0x00, 0x00, 0x34, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xD6, 0x83, 0xD2, 0x1F,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
];
|
||||
|
||||
pub const STORE_HEAP: &[u8] = &[
|
||||
0xEC, 0x00, 0xEC, 0xBC, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xB5, 0x02, 0x06, 0x00, 0x40, 0x00, 0x00, 0x00, 0x34, 0x0E, 0x02, 0x01,
|
||||
0xA0, 0x00, 0x00, 0x00, 0x38, 0x0E, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF9, 0x0F, 0x02, 0x01, 0x60, 0x00, 0x00, 0x00, 0x01, 0x30, 0x1F, 0x00,
|
||||
0x80, 0x00, 0x00, 0x00, 0xDF, 0x35, 0x03, 0x00, 0x89, 0x00, 0x00, 0x00, 0xE0, 0x35, 0x02, 0x01, 0xC0, 0x00, 0x00, 0x00, 0xE3, 0x35, 0x02, 0x01,
|
||||
0x00, 0x01, 0x00, 0x00, 0xE7, 0x35, 0x02, 0x01, 0xE0, 0x00, 0x00, 0x00, 0x33, 0x66, 0x0B, 0x00, 0x01, 0x00, 0x00, 0x00, 0xFA, 0x66, 0x03, 0x00,
|
||||
0x0D, 0x00, 0x0E, 0x00, 0xFF, 0x67, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x22, 0x9D, 0xB5, 0x0A, 0xDC, 0xD9, 0x94, 0x43, 0x85, 0xDE, 0x90, 0xAE,
|
||||
0xB0, 0x7D, 0x12, 0x70, 0x55, 0x00, 0x4E, 0x00, 0x49, 0x00, 0x43, 0x00, 0x4F, 0x00, 0x44, 0x00, 0x45, 0x00, 0x31, 0x00, 0x01, 0x00, 0x00, 0x00,
|
||||
0xF5, 0x5E, 0xF6, 0x66, 0x95, 0x69, 0xCC, 0x4C, 0x83, 0xD1, 0xD8, 0x73, 0x98, 0x99, 0x02, 0x85, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x22, 0x9D, 0xB5, 0x0A, 0xDC, 0xD9, 0x94, 0x43, 0x85, 0xDE, 0x90, 0xAE, 0xB0, 0x7D, 0x12, 0x70, 0x22, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x22, 0x9D, 0xB5, 0x0A, 0xDC, 0xD9, 0x94, 0x43, 0x85, 0xDE, 0x90, 0xAE, 0xB0, 0x7D, 0x12, 0x70, 0x42, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x22, 0x9D, 0xB5, 0x0A, 0xDC, 0xD9, 0x94, 0x43, 0x85, 0xDE, 0x90, 0xAE, 0xB0, 0x7D, 0x12, 0x70, 0x62, 0x80, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00,
|
||||
0x0C, 0x00, 0x14, 0x00, 0x6C, 0x00, 0x7C, 0x00, 0x8C, 0x00, 0xA4, 0x00, 0xBC, 0x00, 0xD4, 0x00, 0xEC, 0x00,
|
||||
];
|
||||
|
||||
pub const HIERARCHY_HEAP: &[u8] = &[
|
||||
0xBC, 0x01, 0xEC, 0x7C, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xB5, 0x04, 0x04, 0x00, 0x60, 0x00, 0x00, 0x00, 0x7C, 0x0D, 0x34, 0x00,
|
||||
0x34, 0x00, 0x35, 0x00, 0x37, 0x00, 0x20, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x01, 0x30, 0x0E, 0x14, 0x00,
|
||||
0x04, 0x06, 0x14, 0x00, 0x33, 0x0E, 0x18, 0x00, 0x08, 0x07, 0x02, 0x01, 0x34, 0x0E, 0x20, 0x00, 0x04, 0x08, 0x03, 0x00, 0x38, 0x0E, 0x24, 0x00,
|
||||
0x04, 0x09, 0x1F, 0x00, 0x01, 0x30, 0x08, 0x00, 0x04, 0x02, 0x03, 0x00, 0x02, 0x36, 0x0C, 0x00, 0x04, 0x03, 0x03, 0x00, 0x03, 0x36, 0x10, 0x00,
|
||||
0x04, 0x04, 0x0B, 0x00, 0x0A, 0x36, 0x34, 0x00, 0x01, 0x05, 0x1F, 0x00, 0x13, 0x36, 0x28, 0x00, 0x04, 0x0A, 0x03, 0x00, 0x35, 0x66, 0x2C, 0x00,
|
||||
0x04, 0x0B, 0x03, 0x00, 0x36, 0x66, 0x30, 0x00, 0x04, 0x0C, 0x03, 0x00, 0xF2, 0x67, 0x00, 0x00, 0x04, 0x00, 0x03, 0x00, 0xF3, 0x67, 0x04, 0x00,
|
||||
0x04, 0x01, 0x23, 0x22, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x22, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x42, 0x80, 0x00, 0x00, 0x01, 0x00,
|
||||
0x00, 0x00, 0x22, 0x80, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0xA0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFC, 0x00, 0x42, 0x80, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x23, 0x22, 0x00, 0x00, 0x0B, 0x00, 0x00, 0x00,
|
||||
0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x54,
|
||||
0x00, 0x6F, 0x00, 0x70, 0x00, 0x20, 0x00, 0x6F, 0x00, 0x66, 0x00, 0x20, 0x00, 0x50, 0x00, 0x65, 0x00, 0x72, 0x00, 0x73, 0x00, 0x6F, 0x00, 0x6E,
|
||||
0x00, 0x61, 0x00, 0x6C, 0x00, 0x20, 0x00, 0x46, 0x00, 0x6F, 0x00, 0x6C, 0x00, 0x64, 0x00, 0x65, 0x00, 0x72, 0x00, 0x73, 0x00, 0x53, 0x00, 0x65,
|
||||
0x00, 0x61, 0x00, 0x72, 0x00, 0x63, 0x00, 0x68, 0x00, 0x20, 0x00, 0x52, 0x00, 0x6F, 0x00, 0x6F, 0x00, 0x74, 0x00, 0x53, 0x00, 0x50, 0x00, 0x41,
|
||||
0x00, 0x4D, 0x00, 0x20, 0x00, 0x53, 0x00, 0x65, 0x00, 0x61, 0x00, 0x72, 0x00, 0x63, 0x00, 0x68, 0x00, 0x20, 0x00, 0x46, 0x00, 0x6F, 0x00, 0x6C,
|
||||
0x00, 0x64, 0x00, 0x65, 0x00, 0x72, 0x00, 0x20, 0x00, 0x32, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x14, 0x00, 0x92, 0x00, 0xAA, 0x00,
|
||||
0x4F, 0x01, 0x7D, 0x01, 0x93, 0x01, 0xBB, 0x01,
|
||||
];
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user