1mod chunk;
18mod compile;
19mod read;
20
21pub use chunk::{
22 CHUNK_ENCODING, ChunkData, DecodeError, FieldRecord, MAXIMUM_CHUNK_BYTES, MAXIMUM_DEPTH,
23 MAXIMUM_RECORD_BYTES, ObjectRecord, PortalRecord, RECORD_VERSION, ZSTD_LEVEL, decode_chunk,
24 decode_record, encode_chunk, encode_record,
25};
26pub use compile::{COMPILER, Written, compile, write_pack};
27pub use read::{
28 Anchor, DEFAULT_MAXIMUM_DECODED_BYTES, DEFAULT_MAXIMUM_PACK_BYTES, Definition, Identity,
29 MAXIMUM_OBJECT_INDEX, Pack, Page, Plane, StairwellTile, Summary, UidLocation, VerifyLimits,
30 Zone,
31};
32
33use std::fmt;
34
35pub const FORMAT: &str = "tilewright.pack/1";
37
38pub const APPLICATION_ID: i32 = 0x5457_5031;
40
41pub const USER_VERSION: i32 = 2;
43
44pub const META_KEYS: [&str; 7] = [
46 "chunk_encoding",
47 "compiler",
48 "format",
49 "profile",
50 "profile_id",
51 "profile_version",
52 "source_digest",
53];
54
55pub const SCHEMA_TABLES: [(&str, &str); 10] = [
57 (
58 "meta",
59 "CREATE TABLE meta (key TEXT PRIMARY KEY, value TEXT NOT NULL) WITHOUT ROWID",
60 ),
61 (
62 "strings",
63 "CREATE TABLE strings (id INTEGER PRIMARY KEY, value TEXT NOT NULL UNIQUE)",
64 ),
65 (
66 "planes",
67 "CREATE TABLE planes (id TEXT PRIMARY KEY, kind TEXT NOT NULL, chunk_size INTEGER NOT NULL, document TEXT NOT NULL) WITHOUT ROWID",
68 ),
69 (
70 "zones",
71 "CREATE TABLE zones (plane TEXT NOT NULL, id TEXT NOT NULL, parent TEXT, document TEXT NOT NULL, PRIMARY KEY (plane, id)) WITHOUT ROWID",
72 ),
73 (
74 "anchors",
75 "CREATE TABLE anchors (plane TEXT NOT NULL, id TEXT NOT NULL, layer INTEGER NOT NULL, x INTEGER NOT NULL, y INTEGER NOT NULL, PRIMARY KEY (plane, id)) WITHOUT ROWID",
76 ),
77 (
78 "stairwells",
79 "CREATE TABLE stairwells (plane TEXT NOT NULL, id TEXT NOT NULL, document TEXT NOT NULL, PRIMARY KEY (plane, id)) WITHOUT ROWID",
80 ),
81 (
82 "stairwell_steps",
83 "CREATE TABLE stairwell_steps (plane TEXT NOT NULL, layer INTEGER NOT NULL, x INTEGER NOT NULL, y INTEGER NOT NULL, stairwell TEXT NOT NULL, step INTEGER NOT NULL, proto TEXT NOT NULL, PRIMARY KEY (plane, layer, x, y)) WITHOUT ROWID",
84 ),
85 (
86 "chunks",
87 "CREATE TABLE chunks (plane TEXT NOT NULL, layer INTEGER NOT NULL, cx INTEGER NOT NULL, cy INTEGER NOT NULL, encoding TEXT NOT NULL, data BLOB NOT NULL, PRIMARY KEY (plane, layer, cx, cy)) WITHOUT ROWID",
88 ),
89 (
90 "definitions",
91 "CREATE TABLE definitions (type TEXT NOT NULL, id TEXT NOT NULL, document TEXT NOT NULL, PRIMARY KEY (type, id)) WITHOUT ROWID",
92 ),
93 (
94 "object_uids",
95 "CREATE TABLE object_uids (uid TEXT PRIMARY KEY, plane TEXT NOT NULL, layer INTEGER NOT NULL, cx INTEGER NOT NULL, cy INTEGER NOT NULL, object INTEGER NOT NULL) WITHOUT ROWID",
96 ),
97];
98
99pub(crate) fn is_identifier(text: &str) -> bool {
102 let bytes = text.as_bytes();
103 (1..=128).contains(&bytes.len())
104 && (bytes[0].is_ascii_lowercase() || bytes[0].is_ascii_digit())
105 && bytes
106 .iter()
107 .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || b"_.-".contains(byte))
108}
109
110pub(crate) fn is_namespace(text: &str) -> bool {
113 let (domain, rest) = text.split_once('/').unwrap_or((text, ""));
114 domain.contains('.')
115 && is_identifier(domain)
116 && !domain.starts_with('.')
117 && !domain.ends_with('.')
118 && (rest.is_empty() || is_identifier(rest))
119}
120
121pub(crate) fn is_field_name(text: &str) -> bool {
123 let bytes = text.as_bytes();
124 (1..=64).contains(&bytes.len())
125 && bytes[0].is_ascii_lowercase()
126 && bytes
127 .iter()
128 .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || *byte == b'_')
129}
130
131pub(crate) fn canonical_limits() -> tilewright_jsonc::Limits {
135 let limits = tilewright_jsonc::Limits::default();
136 tilewright_jsonc::Limits {
137 max_line_bytes: limits.max_input_bytes,
138 ..limits
139 }
140}
141
142pub(crate) static SCHEMA: std::sync::LazyLock<String> = std::sync::LazyLock::new(|| {
144 SCHEMA_TABLES
145 .iter()
146 .map(|(_, sql)| format!("{sql};"))
147 .collect::<Vec<_>>()
148 .join("\n")
149});
150
151#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
155#[non_exhaustive]
156pub enum ErrorCode {
157 Io,
159 Sqlite,
161 NotAFile,
163 TooLarge,
165 FileLength,
167 Schema,
169 Identity,
172 FreePages,
174 Meta,
176 Profile,
178 String,
180 StringIds,
182 Plane,
184 Zone,
186 Anchor,
188 Stairwell,
190 StairwellStep,
192 Definition,
194 Chunk,
197 ChunkEncoding,
199 ChunkData,
201 ChunkRecord,
203 Reference,
205 ObjectUid,
208 Integrity,
210 Digest,
213 DecodeBudget,
217 Invalid,
219}
220
221impl ErrorCode {
222 pub const ALL: [Self; 28] = [
224 Self::Io,
225 Self::Sqlite,
226 Self::NotAFile,
227 Self::TooLarge,
228 Self::FileLength,
229 Self::Schema,
230 Self::Identity,
231 Self::FreePages,
232 Self::Meta,
233 Self::Profile,
234 Self::String,
235 Self::StringIds,
236 Self::Plane,
237 Self::Zone,
238 Self::Anchor,
239 Self::Stairwell,
240 Self::StairwellStep,
241 Self::Definition,
242 Self::Chunk,
243 Self::ChunkEncoding,
244 Self::ChunkData,
245 Self::ChunkRecord,
246 Self::Reference,
247 Self::ObjectUid,
248 Self::Integrity,
249 Self::Digest,
250 Self::DecodeBudget,
251 Self::Invalid,
252 ];
253
254 #[must_use]
256 pub const fn as_str(self) -> &'static str {
257 match self {
258 Self::Io => "pack.io",
259 Self::Sqlite => "pack.sqlite",
260 Self::NotAFile => "pack.not_a_file",
261 Self::TooLarge => "pack.too_large",
262 Self::FileLength => "pack.file_length",
263 Self::Schema => "pack.schema",
264 Self::Identity => "pack.identity",
265 Self::FreePages => "pack.free_pages",
266 Self::Meta => "pack.meta",
267 Self::Profile => "pack.profile",
268 Self::String => "pack.string",
269 Self::StringIds => "pack.string_ids",
270 Self::Plane => "pack.plane",
271 Self::Zone => "pack.zone",
272 Self::Anchor => "pack.anchor",
273 Self::Stairwell => "pack.stairwell",
274 Self::StairwellStep => "pack.stairwell_step",
275 Self::Definition => "pack.definition",
276 Self::Chunk => "pack.chunk",
277 Self::ChunkEncoding => "pack.chunk_encoding",
278 Self::ChunkData => "pack.chunk_data",
279 Self::ChunkRecord => "pack.chunk_record",
280 Self::Reference => "pack.reference",
281 Self::ObjectUid => "pack.object_uid",
282 Self::Integrity => "pack.integrity",
283 Self::Digest => "pack.digest",
284 Self::DecodeBudget => "pack.decode_budget",
285 Self::Invalid => "pack.invalid",
286 }
287 }
288}
289
290impl fmt::Display for ErrorCode {
291 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
292 formatter.write_str(self.as_str())
293 }
294}
295
296#[derive(Debug)]
297pub enum Error {
298 Io(std::io::Error),
299 Sqlite(rusqlite::Error),
300 Chunk(DecodeError),
301 Rejected(ErrorCode, String),
303 Invalid(String),
305}
306
307impl Error {
308 #[must_use]
310 pub const fn code(&self) -> ErrorCode {
311 match self {
312 Self::Io(_) => ErrorCode::Io,
313 Self::Sqlite(_) => ErrorCode::Sqlite,
314 Self::Chunk(_) => ErrorCode::ChunkRecord,
315 Self::Rejected(code, _) => *code,
316 Self::Invalid(_) => ErrorCode::Invalid,
317 }
318 }
319}
320
321impl fmt::Display for Error {
322 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
323 match self {
324 Self::Io(error) => write!(formatter, "I/O error: {error}"),
325 Self::Sqlite(error) => write!(formatter, "SQLite error: {error}"),
326 Self::Chunk(error) => error.fmt(formatter),
327 Self::Rejected(_, message) | Self::Invalid(message) => formatter.write_str(message),
328 }
329 }
330}
331
332impl std::error::Error for Error {}
333
334impl From<std::io::Error> for Error {
335 fn from(error: std::io::Error) -> Self {
336 Self::Io(error)
337 }
338}
339
340impl From<rusqlite::Error> for Error {
341 fn from(error: rusqlite::Error) -> Self {
342 Self::Sqlite(error)
343 }
344}
345
346#[cfg(test)]
347mod tests;