Skip to main content

assetpack_core/sqlite/
common.rs

1use crate::{Codec, Error, Hash32, ObjectKind, ObjectRecord, Result, VerifiedObject, compress, decompress};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
4pub enum MerkleMode {
5  Disabled,
6  #[default]
7  Enabled,
8}
9
10#[derive(Debug, Clone, Default)]
11pub struct SqliteStoreOptions {
12  pub namespace: Option<String>,
13  pub merkle: MerkleMode,
14}
15
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct MerkleProof {
18  pub leaf_pos: u64,
19  pub siblings: Vec<Hash32>,
20}
21
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct MerkleSummary {
24  pub root: Hash32,
25  pub leaf_count: u64,
26}
27
28#[derive(Debug, Clone)]
29pub(in crate::sqlite) struct SqliteLayout {
30  pub namespace: Option<String>,
31  pub objects: String,
32  pub file_recipe_cache: String,
33  pub merkle_leaves: String,
34  pub merkle_nodes: String,
35  pub merkle_meta: String,
36  pub merkle_leaves_pos_index: String,
37}
38
39impl SqliteLayout {
40  pub fn new(namespace: Option<&str>) -> Result<Self> {
41    let namespace = normalize_namespace(namespace)?;
42    let prefix = namespace.as_ref().map(|value| format!("{value}_")).unwrap_or_default();
43    let index_prefix = namespace
44      .as_ref()
45      .map(|value| format!("idx_{value}_"))
46      .unwrap_or_else(|| "idx_".into());
47    Ok(Self {
48      namespace,
49      objects: format!("{prefix}objects"),
50      file_recipe_cache: format!("{prefix}file_recipe_cache"),
51      merkle_leaves: format!("{prefix}merkle_leaves"),
52      merkle_nodes: format!("{prefix}merkle_nodes"),
53      merkle_meta: format!("{prefix}merkle_meta"),
54      merkle_leaves_pos_index: format!("{index_prefix}merkle_leaves_pos"),
55    })
56  }
57}
58
59fn normalize_namespace(namespace: Option<&str>) -> Result<Option<String>> {
60  let Some(namespace) = namespace else {
61    return Ok(None);
62  };
63  if namespace.is_empty() {
64    return Err(Error::InvalidConfig("store namespace cannot be empty".into()));
65  }
66  if namespace.chars().all(|ch| ch.is_ascii_alphanumeric() || ch == '_') {
67    Ok(Some(namespace.to_string()))
68  } else {
69    Err(Error::InvalidConfig(format!(
70      "store namespace must contain only ASCII letters, digits, or underscores: {namespace}"
71    )))
72  }
73}
74
75pub(in crate::sqlite) fn schema_statements(layout: &SqliteLayout, merkle: MerkleMode) -> Vec<String> {
76  let mut statements = vec![
77    format!(
78      "CREATE TABLE IF NOT EXISTS {} (hash BLOB PRIMARY KEY, kind INTEGER NOT NULL, size INTEGER NOT NULL, stored_size INTEGER NOT NULL, \
79       codec TEXT NOT NULL, content BLOB NOT NULL, created_at INTEGER NOT NULL);",
80      layout.objects
81    ),
82    format!(
83      "CREATE TABLE IF NOT EXISTS {} (file_hash BLOB PRIMARY KEY, recipe_hash BLOB NOT NULL, updated_at INTEGER NOT NULL);",
84      layout.file_recipe_cache
85    ),
86  ];
87  if merkle == MerkleMode::Enabled {
88    statements.extend([
89      format!(
90        "CREATE TABLE IF NOT EXISTS {} (hash BLOB PRIMARY KEY, pos INTEGER NOT NULL);",
91        layout.merkle_leaves
92      ),
93      format!(
94        "CREATE UNIQUE INDEX IF NOT EXISTS {} ON {}(pos);",
95        layout.merkle_leaves_pos_index, layout.merkle_leaves
96      ),
97      format!(
98        "CREATE TABLE IF NOT EXISTS {} (level INTEGER NOT NULL, pos INTEGER NOT NULL, hash BLOB NOT NULL, PRIMARY KEY(level, pos));",
99        layout.merkle_nodes
100      ),
101      format!(
102        "CREATE TABLE IF NOT EXISTS {} (key TEXT PRIMARY KEY, value TEXT NOT NULL);",
103        layout.merkle_meta
104      ),
105    ]);
106  }
107  statements
108}
109
110pub(in crate::sqlite) struct StoredObjectRow {
111  pub hash: Hash32,
112  pub kind: i64,
113  pub decoded_len: i64,
114  pub codec: String,
115  pub stored_bytes: Vec<u8>,
116}
117
118pub(in crate::sqlite) fn prepare_object(hash: Hash32, kind: ObjectKind, content: &[u8], codec: Codec) -> Result<ObjectRecord> {
119  if Hash32::sha3_256(content) != hash {
120    return Err(Error::HashMismatch);
121  }
122  let mut selected = codec;
123  let stored_bytes = if codec == Codec::Raw {
124    content.to_vec()
125  } else {
126    let candidate = compress(codec, content)?;
127    if candidate.len() as f64 / content.len().max(1) as f64 >= 0.98 {
128      selected = Codec::Raw;
129      content.to_vec()
130    } else {
131      candidate
132    }
133  };
134  Ok(ObjectRecord {
135    hash,
136    kind,
137    decoded_len: content.len() as u64,
138    codec: selected,
139    stored_bytes,
140  })
141}
142
143pub(in crate::sqlite) fn verified_object_from_row(row: StoredObjectRow) -> Result<VerifiedObject> {
144  let kind = ObjectKind::from_i64(row.kind).ok_or_else(|| Error::Integrity("invalid kind".into()))?;
145  let expected = u64::try_from(row.decoded_len).map_err(|_| Error::Integrity("negative object size".into()))?;
146  let bytes = decompress(codec_from_str(&row.codec)?, &row.stored_bytes)?;
147  let actual = bytes.len() as u64;
148  if actual != expected {
149    return Err(Error::ObjectLengthMismatch { expected, actual });
150  }
151  let actual_hash = Hash32::sha3_256(&bytes);
152  if actual_hash != row.hash {
153    return Err(Error::ObjectHashMismatch {
154      expected: row.hash,
155      actual: actual_hash,
156    });
157  }
158  Ok(VerifiedObject {
159    hash: row.hash,
160    kind,
161    bytes,
162  })
163}
164
165pub(in crate::sqlite) fn object_record_from_row(row: StoredObjectRow) -> Result<ObjectRecord> {
166  Ok(ObjectRecord {
167    hash: row.hash,
168    kind: ObjectKind::from_i64(row.kind).ok_or_else(|| Error::Integrity("invalid kind".into()))?,
169    decoded_len: u64::try_from(row.decoded_len).map_err(|_| Error::Integrity("negative object size".into()))?,
170    codec: codec_from_str(&row.codec)?,
171    stored_bytes: row.stored_bytes,
172  })
173}
174
175pub(in crate::sqlite) fn codec_to_str(codec: Codec) -> &'static str {
176  match codec {
177    Codec::Raw => "raw",
178    Codec::Zstd => "zstd",
179    Codec::Brotli => "brotli",
180  }
181}
182
183pub(in crate::sqlite) fn codec_from_str(value: &str) -> Result<Codec> {
184  match value {
185    "raw" => Ok(Codec::Raw),
186    "zstd" => Ok(Codec::Zstd),
187    "brotli" => Ok(Codec::Brotli),
188    other => Err(Error::InvalidCodec(other.to_string())),
189  }
190}
191
192pub(in crate::sqlite) fn hash_pair(left: &Hash32, right: &Hash32) -> Hash32 {
193  let mut bytes = Vec::with_capacity(64);
194  bytes.extend_from_slice(left.as_bytes());
195  bytes.extend_from_slice(right.as_bytes());
196  Hash32::sha3_256(bytes)
197}
198
199pub(in crate::sqlite) fn merkle_root_from_hashes(hashes: &[Hash32]) -> MerkleSummary {
200  if hashes.is_empty() {
201    return MerkleSummary {
202      root: Hash32::sha3_256([]),
203      leaf_count: 0,
204    };
205  }
206  let mut current = hashes.to_vec();
207  while current.len() > 1 {
208    current = current
209      .chunks(2)
210      .map(|pair| hash_pair(&pair[0], pair.get(1).unwrap_or(&pair[0])))
211      .collect();
212  }
213  MerkleSummary {
214    root: current[0],
215    leaf_count: hashes.len() as u64,
216  }
217}