UDOC 容器规范
从理解,到动手实现。步骤、协议与可复制的示例,都在这份文档中。
UDOC3 is UniDoc's only native save format. It deliberately does not decode
UDOC1, UDOC2, or plain JSON documents.
Goals
- Read the directory with one tail
File.sliceinstead of inflating the file. - Compress document chunks independently.
- Keep already-compressed media byte-for-byte.
- Deduplicate media by SHA-256 content identity.
- Detect damage before document data reaches the editor.
- Preserve unknown parts in future codecs without changing the editor model.
Binary layout
All integers are unsigned little-endian.
0 8
+----------------------------------+
| "UDOC3PKG" | fixed header
+----------------------------------+
| entry payload 0 |
| entry payload 1 | store, independent Brotli, or one-file ZIP
| ... |
+----------------------------------+ directoryOffset
| Brotli central directory | directoryLength (compressed)
+----------------------------------+
| "UD3DIR01" | 8 bytes
| directoryOffset (u64) | 8 bytes
| directoryLength (u64) | 8 bytes
| SHA-256(directory JSON) | 32 bytes
| version = 3 (u32) | 4 bytes
| directory raw length (u32) | 4 bytes
+----------------------------------+ EOF (64-byte footer)The central directory contains one descriptor per part:
{
"path": "document/chunks/000001.json",
"offset": 8,
"compressedSize": 1234,
"size": 8192,
"codec": "br",
"mime": "application/json",
"sha256": "64 lowercase hex digits"
}Offsets are absolute. Paths are unique, relative, slash-separated, and cannot
contain empty, . or .. segments. Entry ranges cannot overlap.
Logical package
manifest.json
document/document.json
document/chunks/000001.json
document/chunks/000002.json
...
rels/relationships.json
media/<sha256>.<extension>
embeds/<sha256>.htmlmanifest.json declares the root document part, ordered chunk list, block
count, chunk size, relationship part, and enabled features.
document/document.json holds document-level metadata, page setup, headers,
footers, and text boxes. It does not contain the block array.
Each chunk contains its absolute block start index and at most 256 ordered
logical blocks. A decoder rejects gaps, overlaps, or an incorrect total.
rels/relationships.json maps source paragraph IDs to internal or external
images, videos, and HTML embeds.
Media and embeds use their uncompressed SHA-256 as their filename. Repeated
content therefore has one physical part even if many blocks reference it.
Codec and integrity rules
- JSON, HTML, XML, CSS, JavaScript, Markdown, plain text, and SVG parts use
independent Brotli (br, quality 9) when it saves meaningful space.
- The central directory is always Brotli-compressed and its uncompressed length
is stored in the footer.
- Binary static resources use a deterministic single-file ZIP payload (
zip).
Already-compressed images, video, audio, PDF, WOFF/WOFF2, and archive formats
use ZIP Store; other binary resources may use ZIP Deflate. Transcoding is
forbidden. Attachments are opaque binary resources and always use this ZIP
side of the hybrid contract, even when their filename ends in .txt or
.html.
- Very small textual parts may remain
storewhen Brotli would not save enough
bytes. The descriptor's codec is authoritative.
- The directory SHA-256 is verified before parsing any entry.
- Every loaded entry is checked against both declared size and SHA-256.
- Brotli output is bounded by its declared size while streaming in the Rust
service.
- One entry is limited to 512 MiB; total declared expansion is limited to 2 GiB.
- The current loader verifies all document, relationship, media, and embed parts.
- Document chunks and media are decoded with bounded concurrency; duplicate
reads share one in-flight promise.
The directory-at-end design allows future lazy media hydration and incremental
append saves without changing the on-disk addressing model.