This repository has no description
11 kB
317 lines
1use std::collections::BTreeSet;
2use std::path::Path;
3
4use rusqlite::{Connection, OpenFlags, OptionalExtension, Row};
5
6#[derive(Debug, thiserror::Error)]
7pub enum SourceError {
8 #[error("source database query failed: {0}")]
9 Sqlite(#[from] rusqlite::Error),
10 #[error(
11 "source database predates DID-keyed repos. Upgrade tangled-knot to its latest release, let it finish its own migrations, then run knot-migrate again."
12 )]
13 PreDid,
14 #[error("source database has no acl table for the casbin cross-check")]
15 MissingAcl,
16 #[error(
17 "source database has a collaborators table but no knot_members table. Upgrade tangled-knot to its latest release, let it finish its own migrations, then run knot-migrate again."
18 )]
19 CollaboratorsWithoutMembers,
20 #[error(
21 "source table `{table}` is missing expected columns {}. This tangled-knot predates the schema knot-migrate reads. Upgrade tangled-knot to its latest release, let it finish its own migrations, then run knot-migrate again.",
22 .missing.join(", ")
23 )]
24 SchemaMismatch { table: String, missing: Vec<String> },
25}
26
27knot_types::text_newtype! {
28 pub struct SourceRepoDid(String) => verbatim as from_column;
29 pub struct SourceDid(String) => verbatim as from_column;
30 pub struct SourceRkey(String) => verbatim as from_column;
31 pub struct SourceRepoName(String) => verbatim as from_column;
32 pub struct SourceRepoObject(String) => verbatim as from_column;
33 pub struct SourceTimestamp(String) => verbatim as from_column;
34}
35
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub enum SourceKeyType {
38 K256,
39 Other(String),
40}
41
42impl SourceKeyType {
43 pub fn from_column(value: impl Into<String>) -> Self {
44 let value = value.into();
45 match value.as_str() {
46 "k256" => Self::K256,
47 _ => Self::Other(value),
48 }
49 }
50
51 pub fn is_k256(&self) -> bool {
52 matches!(self, Self::K256)
53 }
54}
55
56impl ::std::fmt::Display for SourceKeyType {
57 fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
58 match self {
59 Self::K256 => f.write_str("k256"),
60 Self::Other(value) => f.write_str(value),
61 }
62 }
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub enum SourceSchema {
67 Tables,
68 PreFlip,
69}
70
71#[derive(Clone, PartialEq, Eq, zeroize::Zeroize, zeroize::ZeroizeOnDrop)]
72// Every repo's private key, straight outta the old knot's db.
73// `Debug` doesn't prints any bytes on purpose,
74// as to not compromise a migration.
75pub struct SourceSigningKey(Vec<u8>);
76
77impl SourceSigningKey {
78 pub fn from_column(bytes: Vec<u8>) -> Self {
79 Self(bytes)
80 }
81
82 pub fn as_bytes(&self) -> &[u8] {
83 &self.0
84 }
85}
86
87impl std::fmt::Debug for SourceSigningKey {
88 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89 f.debug_tuple("SourceSigningKey").finish_non_exhaustive()
90 }
91}
92
93#[derive(Debug, Clone, PartialEq, Eq)]
94pub struct RepoRow {
95 pub repo_did: SourceRepoDid,
96 pub owner_did: SourceDid,
97 pub repo_name: SourceRepoName,
98 pub signing_key: SourceSigningKey,
99 pub key_type: SourceKeyType,
100 pub created_at: SourceTimestamp,
101}
102
103#[derive(Debug, Clone, PartialEq, Eq)]
104pub struct MemberRow {
105 pub did: SourceDid,
106 pub subject: SourceDid,
107 pub created: SourceTimestamp,
108}
109
110#[derive(Debug, Clone, PartialEq, Eq)]
111pub struct CollabRow {
112 pub repo_did: SourceRepoDid,
113 pub subject_did: SourceDid,
114 pub added_by_did: SourceDid,
115 pub created: SourceTimestamp,
116}
117
118#[derive(Debug, Clone, PartialEq, Eq)]
119pub struct AclRow {
120 pub p_type: String,
121 pub v0: String,
122 pub v1: String,
123 pub v2: String,
124 pub v3: String,
125}
126
127pub struct SourceDb {
128 conn: Connection,
129}
130
131impl SourceDb {
132 pub fn open(path: &Path) -> Result<Self, SourceError> {
133 let conn = Connection::open_with_flags(path, OpenFlags::SQLITE_OPEN_READ_ONLY)?;
134 Ok(Self { conn })
135 }
136
137 pub fn schema(&self) -> Result<SourceSchema, SourceError> {
138 let variant = match (
139 self.has_table("repo_keys")?,
140 self.has_table("repo_aliases")?,
141 self.has_table("knot_members")?,
142 self.has_table("collaborators")?,
143 ) {
144 (true, true, true, true) => SourceSchema::Tables,
145 (true, true, false, true) => return Err(SourceError::CollaboratorsWithoutMembers),
146 (true, true, _, false) => SourceSchema::PreFlip,
147 _ => return Err(SourceError::PreDid),
148 };
149 let checks: Vec<(&str, &[&str])> = [
150 Some((
151 "repo_keys",
152 &[
153 "repo_did",
154 "owner_did",
155 "repo_name",
156 "signing_key",
157 "key_type",
158 "created_at",
159 ][..],
160 )),
161 Some(("repo_aliases", &["rkey", "repo_did", "rev"][..])),
162 self.has_table("acl")?
163 .then_some(("acl", &["p_type", "v0", "v1", "v2", "v3"][..])),
164 self.has_table("knot_members")?
165 .then_some(("knot_members", &["id", "did", "subject", "created"][..])),
166 (variant == SourceSchema::Tables).then_some((
167 "collaborators",
168 &["id", "repo_did", "subject_did", "added_by_did", "created"][..],
169 )),
170 ]
171 .into_iter()
172 .flatten()
173 .collect();
174 checks
175 .into_iter()
176 .try_for_each(|(table, columns)| self.require_columns(table, columns))?;
177 Ok(variant)
178 }
179
180 fn has_table(&self, name: &str) -> Result<bool, SourceError> {
181 let count: i64 = self.conn.query_row(
182 "select count(*) from sqlite_master where type = 'table' and name = ?1",
183 [name],
184 |row| row.get(0),
185 )?;
186 Ok(count > 0)
187 }
188
189 fn require_columns(&self, table: &str, required: &[&str]) -> Result<(), SourceError> {
190 let present: BTreeSet<String> = self
191 .conn
192 .prepare("select name from pragma_table_info(?1)")?
193 .query_map([table], |row| row.get::<_, String>(0))?
194 .collect::<rusqlite::Result<_>>()?;
195 let missing: Vec<String> = required
196 .iter()
197 .filter(|column| !present.contains(**column))
198 .map(|column| (*column).to_string())
199 .collect();
200 missing
201 .is_empty()
202 .then_some(())
203 .ok_or(SourceError::SchemaMismatch {
204 table: table.to_string(),
205 missing,
206 })
207 }
208
209 pub fn repos(&self) -> Result<Vec<RepoRow>, SourceError> {
210 self.collect(
211 "select repo_did, owner_did, repo_name, signing_key, key_type, created_at
212 from repo_keys order by created_at, repo_did",
213 |row| {
214 Ok(RepoRow {
215 repo_did: SourceRepoDid::from_column(row.get::<_, String>(0)?),
216 owner_did: SourceDid::from_column(row.get::<_, String>(1)?),
217 repo_name: SourceRepoName::from_column(row.get::<_, String>(2)?),
218 signing_key: SourceSigningKey::from_column(row.get(3)?),
219 key_type: SourceKeyType::from_column(row.get::<_, String>(4)?),
220 created_at: SourceTimestamp::from_column(row.get::<_, String>(5)?),
221 })
222 },
223 )
224 }
225
226 pub fn members(&self) -> Result<Vec<MemberRow>, SourceError> {
227 if !self.has_table("knot_members")? {
228 return Ok(Vec::new());
229 }
230 self.collect(
231 "select did, subject, created from knot_members
232 where id in (select min(id) from knot_members group by subject)
233 order by id",
234 |row| {
235 Ok(MemberRow {
236 did: SourceDid::from_column(row.get::<_, String>(0)?),
237 subject: SourceDid::from_column(row.get::<_, String>(1)?),
238 created: SourceTimestamp::from_column(row.get::<_, String>(2)?),
239 })
240 },
241 )
242 }
243
244 pub fn collaborators(&self) -> Result<Vec<CollabRow>, SourceError> {
245 if !self.has_table("collaborators")? {
246 return Ok(Vec::new());
247 }
248 self.collect(
249 "select repo_did, subject_did, added_by_did, created from collaborators order by id",
250 |row| {
251 Ok(CollabRow {
252 repo_did: SourceRepoDid::from_column(row.get::<_, String>(0)?),
253 subject_did: SourceDid::from_column(row.get::<_, String>(1)?),
254 added_by_did: SourceDid::from_column(row.get::<_, String>(2)?),
255 created: SourceTimestamp::from_column(row.get::<_, String>(3)?),
256 })
257 },
258 )
259 }
260
261 pub fn acl(&self) -> Result<Vec<AclRow>, SourceError> {
262 if !self.has_table("acl")? {
263 return Err(SourceError::MissingAcl);
264 }
265 self.collect(
266 "select p_type, v0, v1, v2, v3 from acl order by rowid",
267 |row| {
268 Ok(AclRow {
269 p_type: row.get(0)?,
270 v0: row.get(1)?,
271 v1: row.get(2)?,
272 v2: row.get(3)?,
273 v3: row.get(4)?,
274 })
275 },
276 )
277 }
278
279 pub fn current_rkey(
280 &self,
281 repo_did: &SourceRepoDid,
282 ) -> Result<Option<SourceRkey>, SourceError> {
283 self.conn
284 .query_row(
285 "select rkey from repo_aliases
286 where repo_did = ?
287 order by rev desc
288 limit 1",
289 [repo_did.as_str()],
290 |row| row.get::<_, String>(0).map(SourceRkey::from_column),
291 )
292 .optional()
293 .map_err(Into::into)
294 }
295
296 pub fn orphan_alias_count(&self) -> Result<u64, SourceError> {
297 let count: i64 = self.conn.query_row(
298 "select count(*) from repo_aliases ra
299 where not exists (select 1 from repo_keys rk where rk.repo_did = ra.repo_did)",
300 [],
301 |row| row.get(0),
302 )?;
303 Ok(count as u64)
304 }
305
306 fn collect<T>(
307 &self,
308 sql: &str,
309 map: impl Fn(&Row<'_>) -> rusqlite::Result<T>,
310 ) -> Result<Vec<T>, SourceError> {
311 let mut statement = self.conn.prepare(sql)?;
312 let rows = statement
313 .query_map([], map)?
314 .collect::<rusqlite::Result<Vec<T>>>()?;
315 Ok(rows)
316 }
317}