Skip to main content

apt_swarm/
sync.rs

1use crate::db::{Database, DatabaseClient};
2use crate::errors::*;
3use crate::keyring::Keyring;
4use crate::p2p::peerdb;
5use crate::signed::Signed;
6use base16ct::HexDisplay;
7use bstr::BStr;
8use futures::StreamExt;
9use indexmap::{IndexMap, IndexSet};
10use sequoia_openpgp::Fingerprint;
11use sha2::{Digest, Sha256};
12use std::borrow::Cow;
13use std::collections::{BTreeMap, VecDeque};
14use std::fmt;
15use std::str;
16use std::str::FromStr;
17use std::time::Duration;
18use tokio::io;
19use tokio::io::{AsyncBufReadExt, AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
20use tokio::time;
21
22pub const MAX_LINE_LENGTH: u64 = 512;
23
24pub const SYNC_INDEX_TIMEOUT: Duration = Duration::from_secs(120);
25pub const SYNC_READ_TIMEOUT: Duration = Duration::from_secs(30);
26
27// We expect entries from 0-f
28pub const BATCH_INDEX_MAX_SIZE: usize = 16;
29
30/// If the number of entries is greater than zero, but <= this threshold, send a dump instead of an index
31pub const SPILL_THRESHOLD: usize = 1;
32
33/// Stop announcing peers we couldn't handshake with recently
34const PEX_MAX_SUCCESS_AGE: Duration = Duration::from_secs(3600 * 24 * 5);
35
36#[derive(Debug, Clone)]
37pub enum Query {
38    Tree(TreeQuery),
39    Pex,
40    Unknown(String),
41}
42
43impl Query {
44    pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
45        let line = bytes.strip_suffix(b"\n").unwrap_or(bytes);
46        let line = str::from_utf8(line).context("Query contains invalid utf8")?;
47        let query = line
48            .parse()
49            .with_context(|| anyhow!("Failed to parse input as query: {line:?}"))?;
50        Ok(query)
51    }
52}
53
54impl FromStr for Query {
55    type Err = Error;
56
57    fn from_str(query: &str) -> Result<Self> {
58        if let Some(cmd) = query.strip_prefix("//") {
59            if cmd == "pex" {
60                Ok(Query::Pex)
61            } else {
62                Ok(Query::Unknown(cmd.to_string()))
63            }
64        } else {
65            let query = query
66                .parse()
67                .context("Failed to parse input as tree-query")?;
68            Ok(Query::Tree(query))
69        }
70    }
71}
72
73#[derive(Debug, Clone)]
74pub struct TreeQuery {
75    pub fp: Fingerprint,
76    pub hash_algo: String,
77    pub prefix: Option<String>,
78}
79
80impl TreeQuery {
81    pub fn new<I: Into<String>>(fp: Fingerprint, hash_algo: I) -> Self {
82        TreeQuery {
83            fp: fp.clone(),
84            hash_algo: hash_algo.into(),
85            prefix: None,
86        }
87    }
88
89    pub async fn write_to<W: AsyncWrite + Unpin>(&self, mut tx: W) -> Result<()> {
90        let mut out = format!("{:X} {}", self.fp, self.hash_algo);
91        if let Some(prefix) = &self.prefix {
92            out.push(' ');
93            out.push_str(prefix);
94        }
95        out.push('\n');
96        tx.write_all(out.as_bytes()).await?;
97        Ok(())
98    }
99
100    /// Switch to the next shard
101    pub fn increment(&mut self) -> bool {
102        if let Some(prefix) = &mut self.prefix {
103            if prefix.ends_with('f') {
104                prefix.pop();
105                true
106            } else if let Some(c) = prefix.pop() {
107                let c = match c {
108                    '0'..='8' | 'a'..='e' => (c as u8 + 1) as char,
109                    '9' => 'a',
110                    _ => c,
111                };
112                prefix.push(c);
113                false
114            } else {
115                // prefix is empty, keyspace has been traversed
116                false
117            }
118        } else {
119            debug!("Peers are already in sync, nothing to do here");
120            self.prefix = Some(String::new());
121            false
122        }
123    }
124
125    /// Traverse into the first entry in this shard
126    pub fn enter(&mut self) {
127        if let Some(prefix) = &mut self.prefix {
128            prefix.push('0');
129        } else {
130            self.prefix = Some("0".to_string());
131        }
132    }
133}
134
135impl fmt::Display for TreeQuery {
136    fn fmt(&self, w: &mut fmt::Formatter) -> fmt::Result {
137        let prefix = self.prefix.as_deref().unwrap_or("");
138        write!(w, "{:X}/{}:{}", self.fp, self.hash_algo, prefix)?;
139        Ok(())
140    }
141}
142
143impl FromStr for TreeQuery {
144    type Err = Error;
145
146    fn from_str(s: &str) -> Result<Self> {
147        let mut s = s.split(' ');
148        let fp = Fingerprint::from_str(s.next().context("Missing fingerprint")?)
149            .context("Invalid fingerprint")?;
150
151        let hash_algo = s.next().context("Missing hash algo")?;
152        if hash_algo != "sha256" {
153            bail!("Only sha256 is supported at the moment");
154        }
155
156        let prefix = s.next().map(String::from);
157
158        if let Some(garbage) = s.next() {
159            bail!("Detected trailing data, rejecting as invalid: {garbage:?}");
160        }
161
162        Ok(TreeQuery {
163            fp,
164            hash_algo: hash_algo.to_string(),
165            prefix,
166        })
167    }
168}
169
170#[derive(Debug, Default)]
171pub struct BatchIndex {
172    index: IndexMap<String, (String, usize)>,
173}
174
175impl BatchIndex {
176    pub fn new() -> Self {
177        BatchIndex::default()
178    }
179
180    pub fn add(&mut self, index: String, prefix: String, count: usize) -> Result<()> {
181        if self.index.len() < BATCH_INDEX_MAX_SIZE {
182            self.index.insert(prefix, (index, count));
183            Ok(())
184        } else {
185            bail!(
186                "Batch index is already at max capacity: {:?}",
187                self.index.len()
188            )
189        }
190    }
191
192    pub async fn write_to<W: AsyncWrite + Unpin>(&self, mut sink: W) -> Result<()> {
193        for (prefix, (index, count)) in &self.index {
194            sink.write_all(format!("{index} {prefix} {count}\n").as_bytes())
195                .await?;
196        }
197        Ok(())
198    }
199
200    pub fn parse_line(&mut self, line: &[u8]) -> Result<()> {
201        let line = line.strip_suffix(b"\n").unwrap_or(line);
202        let line = str::from_utf8(line).context("Response contains invalid utf8")?;
203
204        let mut s = line.split(' ');
205        let index = s.next().context("Missing index from response")?;
206        let prefix = s.next().context("Failed to get prefix for index")?;
207        let count = s.next().context("Failed to get number of children")?;
208        let count = count
209            .parse()
210            .context("Number of children is not a number")?;
211
212        self.add(index.to_string(), prefix.to_string(), count)
213    }
214
215    pub fn get(&self, key: &str) -> Option<&(String, usize)> {
216        self.index.get(key)
217    }
218
219    pub fn keys(&self) -> indexmap::map::Keys<'_, String, (String, usize)> {
220        self.index.keys()
221    }
222
223    pub fn clear(&mut self) {
224        self.index.clear();
225    }
226}
227
228pub async fn index_from_scan(db: &Database, query: &TreeQuery) -> Result<(String, usize)> {
229    let prefix = query.to_string();
230
231    let mut counter = 0;
232    let mut hasher = Sha256::new();
233
234    let stream = db.scan_keys(prefix.as_bytes());
235    tokio::pin!(stream);
236    while let Some(item) = stream.next().await {
237        let hash = item.context("Failed to read from database (index_from_scan)")?;
238        hasher.update(&hash);
239        hasher.update(b"\n");
240        counter += 1;
241    }
242
243    let result = HexDisplay(&hasher.finalize());
244    Ok((format!("sha256:{result:x}"), counter))
245}
246
247pub async fn sync_yield<
248    D: DatabaseClient + Sync + Send,
249    R: AsyncRead + Unpin,
250    W: AsyncWrite + Unpin,
251>(
252    db: &mut D,
253    peerdb: Option<peerdb::Client>,
254    rx: R,
255    mut tx: W,
256    timeout: Option<Duration>,
257) -> Result<()> {
258    let mut rx = io::BufReader::new(rx);
259    loop {
260        let mut line = Vec::new();
261        let mut rrx = (&mut rx).take(MAX_LINE_LENGTH);
262        let read = rrx.read_until(b'\n', &mut line);
263
264        let n = if let Some(timeout) = timeout {
265            if let Ok(n) = time::timeout(timeout, read).await {
266                n
267            } else {
268                break;
269            }
270        } else {
271            read.await
272        }?;
273
274        if n == 0 {
275            break;
276        }
277
278        if !line.ends_with(b"\n") {
279            bail!(
280                "Client sent invalid request, exceeding size limit: {:?}",
281                BStr::new(&line)
282            );
283        }
284
285        let query = Query::from_bytes(&line)?;
286        trace!("Received query: {:?}", query);
287        match query {
288            Query::Tree(mut query) => {
289                let (index, total) = db.batch_index_from_scan(&mut query).await?;
290
291                if total > 0 && total <= SPILL_THRESHOLD {
292                    let prefix = query.to_string();
293                    for (hash, data) in db.spill(prefix.as_bytes()).await? {
294                        trace!("Sending data packet to client: {:?}", BStr::new(&hash));
295                        tx.write_all(format!(":{:x}\n", data.len()).as_bytes())
296                            .await?;
297                        tx.write_all(&data).await?;
298                    }
299                    tx.write_all(b":0\n").await?;
300                } else {
301                    index.write_to(&mut tx).await?;
302                    tx.write_all(b"\n").await?;
303                }
304            }
305            Query::Pex => {
306                let mut buf = String::new();
307                if let Some(peerdb) = &peerdb {
308                    for addr in peerdb.sample(Some(PEX_MAX_SUCCESS_AGE)).await? {
309                        buf += &format!("{addr}\n");
310                    }
311                }
312                tx.write_all(format!(":{}\n", buf.len()).as_bytes()).await?;
313                tx.write_all(buf.as_bytes()).await?;
314            }
315            Query::Unknown(data) => {
316                debug!("Received unknown command from network: {data:?}");
317                tx.write_all(b":0\n").await?;
318            }
319        }
320    }
321
322    Ok(())
323}
324
325#[derive(Debug, Default)]
326pub struct SyncQueue {
327    queues: BTreeMap<usize, VecDeque<Option<String>>>,
328}
329
330impl SyncQueue {
331    pub fn push(&mut self, key: Option<String>) {
332        let len = key.as_ref().map(|s| s.len()).unwrap_or(0);
333        let queue = self.queues.entry(len).or_default();
334        queue.push_back(key);
335    }
336
337    pub fn pop_next(&mut self) -> Option<Option<String>> {
338        loop {
339            let mut entry = self.queues.last_entry()?;
340            let queue = entry.get_mut();
341            if let Some(item) = queue.pop_front() {
342                return Some(item);
343            } else {
344                entry.remove_entry();
345            }
346        }
347    }
348}
349
350pub async fn sync_pull_key<
351    D: DatabaseClient + Sync + Send,
352    R: AsyncRead + Unpin,
353    W: AsyncWrite + Unpin,
354>(
355    db: &mut D,
356    keyring: &Keyring,
357    fp: &Fingerprint,
358    dry_run: bool,
359    mut tx: W,
360    rx: &mut io::BufReader<R>,
361) -> Result<()> {
362    let mut query = TreeQuery {
363        fp: fp.clone(),
364        hash_algo: "sha256".to_string(),
365        prefix: None,
366    };
367
368    let mut queue = SyncQueue::default();
369    queue.push(None);
370
371    while let Some(item) = queue.pop_next() {
372        query.prefix = item;
373        info!("Requesting index for: {:?}", query.to_string());
374        query.write_to(&mut tx).await?;
375
376        let (our_index, _our_count) = db.batch_index_from_scan(&mut query).await?;
377        trace!("Our index: {our_index:?}");
378
379        let mut line = Vec::new();
380        let mut their_index = BatchIndex::new();
381
382        loop {
383            line.clear();
384
385            let mut rrx = rx.take(MAX_LINE_LENGTH);
386            let read = rrx.read_until(b'\n', &mut line);
387            let n = time::timeout(SYNC_INDEX_TIMEOUT, read)
388                .await
389                .context("Request for index timed out")?
390                .context("Failed to receive response from peer")?;
391
392            if n == 0 {
393                bail!("Reached unexpected eof while enumerating service");
394            }
395
396            if !line.ends_with(b"\n") {
397                bail!(
398                    "Server sent invalid line, exceeding size limit: {:?}",
399                    BStr::new(&line)
400                );
401            }
402
403            if line == b"\n" {
404                let keys = their_index
405                    .keys()
406                    .chain(our_index.keys())
407                    .collect::<IndexSet<_>>();
408
409                for key in keys {
410                    match (their_index.get(key), our_index.get(key)) {
411                        (Some(theirs), Some(ours)) => {
412                            trace!("Comparing index shards for key={key:?}, theirs={theirs:?}, ours={ours:?}");
413
414                            if theirs.1 == 0 {
415                                trace!(
416                                    "No children in this shard (key={key:?}), moving to next one"
417                                );
418                            } else if theirs == ours {
419                                trace!("These shards are already in sync (key={key:?}), moving to next one");
420                            } else {
421                                trace!("Data to be found here (key={key:?}), trying to enumerate");
422                                queue.push(Some(key.to_owned()));
423                            }
424                        }
425                        _ => bail!("Some index shards are omitted, this is currently unsupported"),
426                    }
427                }
428
429                their_index.clear();
430                break;
431            } else if let Some(line) = line.strip_prefix(b":") {
432                let line = line.strip_suffix(b"\n").unwrap_or(line);
433                let line = str::from_utf8(line).context("Length tag has invalid utf8")?;
434                trace!("Received len tag: {:?}", line);
435                let len = usize::from_str_radix(line, 16)
436                    .with_context(|| anyhow!("Length tag is invalid number: {line:?}"))?;
437
438                if len == 0 {
439                    trace!("Received all releases from shard, moving to next one");
440                    while query.increment() {
441                        trace!("Reached last entry in shard, returning to parent");
442                    }
443                    break;
444                }
445
446                // TODO: check this tag doesn't OOM us
447                info!("Reading data packet from remote: {len:?} bytes");
448
449                let mut remaining = len;
450                let mut buf = vec![0u8; len];
451                while remaining > 0 {
452                    let read = rx.read(&mut buf[len - remaining..]);
453
454                    let n = time::timeout(SYNC_READ_TIMEOUT, read)
455                        .await
456                        .context("Read from remote timed out")?
457                        .context("Failed to receive data from peer")?;
458
459                    if n == 0 {
460                        bail!("Unexpected end of file");
461                    }
462
463                    remaining -= n;
464                    trace!("Read {}/{} bytes from remote", len - remaining, len);
465                }
466                trace!("Finished reading data packet: {:?}", buf.len());
467
468                let mut bytes = &buf[..];
469                while !bytes.is_empty() {
470                    let (signed, remaining) =
471                        Signed::from_bytes(bytes).context("Failed to parse release file")?;
472
473                    for (fp, variant) in signed.canonicalize(Some(keyring))? {
474                        let fp = fp.context(
475                            "Signature can't be imported because the signature is unverified",
476                        )?;
477                        if dry_run {
478                            debug!("Skipping insert due to dry-run");
479                        } else {
480                            db.add_release(&fp, &variant).await?;
481                        }
482                    }
483
484                    bytes = remaining;
485                }
486            } else {
487                their_index
488                    .parse_line(&line)
489                    .with_context(|| anyhow!("Failed to parse line of batch index: {line:?}"))?;
490            }
491        }
492    }
493
494    Ok(())
495}
496
497pub async fn sync_pull<
498    D: DatabaseClient + Sync + Send,
499    R: AsyncRead + Unpin,
500    W: AsyncWrite + Unpin,
501>(
502    db: &mut D,
503    keyring: &Keyring,
504    selected_keys: &[Fingerprint],
505    dry_run: bool,
506    mut tx: W,
507    rx: R,
508) -> Result<()> {
509    let selected_keys = if !selected_keys.is_empty() {
510        Cow::Borrowed(selected_keys)
511    } else {
512        Cow::Owned(keyring.all_fingerprints())
513    };
514
515    let mut rx = io::BufReader::new(rx);
516    for fp in selected_keys.iter() {
517        sync_pull_key(db, keyring, fp, dry_run, &mut tx, &mut rx).await?;
518    }
519
520    Ok(())
521}
522
523#[cfg(test)]
524mod tests {
525    use super::*;
526    use crate::db::AccessMode;
527    use futures::TryStreamExt;
528
529    fn init() {
530        let _ = env_logger::builder().is_test(true).try_init();
531    }
532
533    const EXAMPLE_KEYRING: &[u8] = include_bytes!("../contrib/signal-desktop-keyring.pgp");
534    const EMPTY_HASH: &str =
535        "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
536
537    fn example_tree_query() -> TreeQuery {
538        let fp = "DBA36B5181D0C816F630E889D980A17457F6FB06".parse().unwrap();
539        TreeQuery::new(fp, "sha256")
540    }
541
542    fn empty_index() -> (String, usize) {
543        (EMPTY_HASH.to_string(), 0)
544    }
545
546    async fn open_temp_dbs() -> Result<(tempfile::TempDir, Database, Database)> {
547        let dir = tempfile::tempdir()?;
548        let db_a = Database::open_at(dir.path().join("a"), AccessMode::Exclusive).await?;
549        let db_b = Database::open_at(dir.path().join("b"), AccessMode::Exclusive).await?;
550        Ok((dir, db_a, db_b))
551    }
552
553    async fn run_sync(keyring: &Keyring, db_a: &mut Database, db_b: &mut Database) -> Result<()> {
554        let (client, server) = tokio::io::duplex(64);
555        let (client_rx, client_tx) = tokio::io::split(client);
556        let (server_rx, server_tx) = tokio::io::split(server);
557        let task_yield = sync_yield(db_a, None, server_rx, server_tx, None);
558        let task_pull = sync_pull(db_b, keyring, &[], false, client_tx, client_rx);
559
560        tokio::select! {
561            ret = task_pull => ret?,
562            ret = task_yield => bail!("Yield task was not expected to return: {ret:?}"),
563        }
564
565        Ok(())
566    }
567
568    #[test]
569    fn test_tree_query_display() {
570        let query = example_tree_query();
571        assert_eq!(
572            query.to_string(),
573            "DBA36B5181D0C816F630E889D980A17457F6FB06/sha256:"
574        );
575    }
576
577    #[test]
578    fn test_example_tree_query_is_example_keyring() {
579        let keyring = Keyring::new(EXAMPLE_KEYRING).unwrap();
580
581        // Check the example query matches this keyring
582        let tree_query = example_tree_query();
583        assert_eq!(keyring.all_fingerprints(), &[tree_query.fp]);
584    }
585
586    #[tokio::test]
587    async fn test_sync_both_empty() -> Result<()> {
588        init();
589
590        let keyring = Keyring::new(EXAMPLE_KEYRING).unwrap();
591
592        // Run the sync on two empty db's
593        let (_, mut db_a, mut db_b) = open_temp_dbs().await.unwrap();
594        run_sync(&keyring, &mut db_a, &mut db_b).await.unwrap();
595
596        // Calculate the sync index
597        let tree_query = example_tree_query();
598        assert_eq!(
599            index_from_scan(&db_a, &tree_query).await.unwrap(),
600            empty_index()
601        );
602        assert_eq!(
603            index_from_scan(&db_b, &tree_query).await.unwrap(),
604            empty_index()
605        );
606
607        Ok(())
608    }
609
610    #[tokio::test]
611    async fn test_sync_full() -> Result<()> {
612        init();
613
614        let keyring = Keyring::new(EXAMPLE_KEYRING).unwrap();
615        let (_, mut db_a, mut db_b) = open_temp_dbs().await.unwrap();
616
617        let data = [
618        b"-----BEGIN PGP SIGNED MESSAGE-----
619
620Origin: . xenial
621Label: . xenial
622Suite: xenial
623Codename: xenial
624Date: Thu, 23 Feb 2023 01:55:04 UTC
625Architectures: amd64
626Components: main
627Description: Generated by aptly
628MD5Sum:
629 cdb20787f1556bb38ae3b6017ef51327   132984 main/binary-amd64/Packages
630 001fc41d6c21eb85a43a13133584cbae    21567 main/binary-amd64/Packages.gz
631 3fb4f1a0169c3b2fff2c43c2a2277b51    17923 main/binary-amd64/Packages.bz2
632 c911b1bc4adf556f6fbd17c0c9cd8315     4794 main/Contents-amd64.gz
633 d8c35b55bc8e48e267b9ccdaf383976d       85 main/binary-amd64/Release
634SHA1:
635 455673b692a697ad3ada91a875096365f8da1524   132984 main/binary-amd64/Packages
636 e6a940039dcfc7f93f4a5501f15e75d1427b9464    21567 main/binary-amd64/Packages.gz
637 b8447294816bb063c10d0ba35f48dd5d1980f795    17923 main/binary-amd64/Packages.bz2
638 b6fd643edc8846c0914b44f3182dfc086877d944     4794 main/Contents-amd64.gz
639 992cb9cd8a0af2d9ad81d2b45342656d41157202       85 main/binary-amd64/Release
640SHA256:
641 989c22244106e44d789400d4da33d2ed64228ce94f48d1c2c37493118c992384   132984 main/binary-amd64/Packages
642 c46198172d00d4e01388832b61186a888da47e2c119c1e9dd7378fea206b1237    21567 main/binary-amd64/Packages.gz
643 b368e24d5c137448095f8940e3b371bff83e3e56159df6c58d4be83732a85554    17923 main/binary-amd64/Packages.bz2
644 bb347cbc00e02d73fef513965f1cd9f9e73100cd34097c43fdd1414668ec8ed8     4794 main/Contents-amd64.gz
645 e593f5bb98e0b6dbf5d0636ebff298b905b98a00402e2b20173fdb5da85c46d9       85 main/binary-amd64/Release
646-----BEGIN PGP SIGNATURE-----
647
648wsFcBAEBCAAGBQJj9sd6AAoJENmAoXRX9vsGOQ0P/3S63ctKl7QyxmRQ4UVJl70S
649hTxA90FbWp236nrEWw4EO/eVWiR/VbgFPacp/dyBpSmtTFl5cpOeyf2SYj5qfg5L
650cemYgUbaxRl+PBFGm7A14y82Ym0MUzF9cNWVK8bDXH9BKSljKKerXr4giOwjTkgh
651z2LoLxnrbhGkIWnSNiT0YvQrkxkSC5BjOInRiy/4Dr7LFAX/7KBzyPVwiDPxWQca
652dwtmI6EoZQP+zHDTR6RwnYOB7oME8aYIruwF9Vhu/unfdC4LpbNJDGL7VwQKUp8h
653ICupSwnRmHPV2raNBq58K6OunGvFO0oFaYUIQqbvGzu/5859YWhrdd7gBd9Fj4zI
654Ff7fHC+ZigCNCk7op4LykJ/3uJF8NvFlNxiagO+1tRko3V4tNbeSrXEKDhr5RQJz
655p/VdL1TXI/pVIobxbF5D/Lo8dCs5LjJsJ5rFlPgzjlREFn0hwKcDwB7M+rbPhuHV
6561R3lgdhW01ZghwOdTMiX1cShQwE7bvGtskn2WIHyIhEawpotGNpBFG2K5TdxfXA1
657m+wu4PLxfxOSb+VoQlH1enyDcR7m7XNtt692l++6nw3rq6Wv2zNc9DHRE+HNavJg
658zwlfH3L9OOoGfPMfRxrKqFzcob2gnKjptlHt3XpUx5ZwS4hcKB2lETT9ORVxe1NI
659rK5KKL67o5aLviVqo98l
660=MI63
661-----END PGP SIGNATURE-----
662",
663b"-----BEGIN PGP SIGNED MESSAGE-----
664
665Origin: . xenial
666Label: . xenial
667Suite: xenial
668Codename: xenial
669Date: Wed, 15 Feb 2023 23:18:08 UTC
670Architectures: amd64
671Components: main
672Description: Generated by aptly
673MD5Sum:
674 bea5a0f7c99209504f22d8faf10125fd   132287 main/binary-amd64/Packages
675 1aa4c130945a3a076a9f16546ca17a83    21467 main/binary-amd64/Packages.gz
676 f18c7f0779161104fed2aec72d9a44e2    17922 main/binary-amd64/Packages.bz2
677 fdb168fb0b8f575585d917ca0ffd98bb     4783 main/Contents-amd64.gz
678 d8c35b55bc8e48e267b9ccdaf383976d       85 main/binary-amd64/Release
679SHA1:
680 d01c164d99cf7c867b5f115770f58e4916d7a15f   132287 main/binary-amd64/Packages
681 41e78c4c558567f4936d9952eb928a32911cc56c    21467 main/binary-amd64/Packages.gz
682 8837bf2f3e2bad7c73712d003c1510c6171c53ee    17922 main/binary-amd64/Packages.bz2
683 e800a4c83e8d9ef564f5869ad838962550799c5e     4783 main/Contents-amd64.gz
684 992cb9cd8a0af2d9ad81d2b45342656d41157202       85 main/binary-amd64/Release
685SHA256:
686 9f9178b66c4d1d31d7b2b741f0835c2140552cac68861beaf2ecc55f0364c620   132287 main/binary-amd64/Packages
687 57386742060a0913236bafd5e8eb3b3334284e0d2ab8362a7c22c78175e9d89b    21467 main/binary-amd64/Packages.gz
688 ec097b64b5e3a39760a9b5ea6b02e91d0401994464dd0ce3de2a0a26a62230e2    17922 main/binary-amd64/Packages.bz2
689 e04d5fd71915c3003d55f3927e5af71a4831e30ffbb0efec6dceb36cd1b054fa     4783 main/Contents-amd64.gz
690 e593f5bb98e0b6dbf5d0636ebff298b905b98a00402e2b20173fdb5da85c46d9       85 main/binary-amd64/Release
691-----BEGIN PGP SIGNATURE-----
692
693wsFcBAABCAAGBQJj7WgwAAoJENmAoXRX9vsG69AQAJmvducnhHqCXQIsqjXrDMjU
694QUAw56MRunn7rHTFpJY0ZPLgQ5gVBibouNZ9x78wuJ784Sl+MIHC7RWdQYBEbWQ5
695haKjiI00BzDeXx4sUet1E+Ce5dhjK/UvoZIOy+ed5nv/HM7QFrvoxdADSDnYGy2o
696djFUVWR5kzkb5Tv7bcjJQWWf6JvY1Z12CgsG85ECYv2PE+tGgQjSwbxRDvFFzY1O
697Xy1EkjT+YDG6hy5CiKSZL7qPsjsLHeuRvat3oSlBWiFRnSuLOlsDozqzYMFqNx93
698GPQiFNiYEmkxDxiKLOcds7+Plz2FjQdQwv2msllJ4jA9PxYRiEbfH14/ELk+/snE
69966XID9dv91JbrwaI3NOoJZZmN+QYZ7WaAj3Uxl3cYnCGuIIt6z4KB2CYeyRa3f3K
700HbPq0mBchPPmavaQEfaNDQ+dzMuazR0VMoKfHGEp44r+XU+JH/lNzlxgQEMgVv43
7010B++zb4MYgheGUhu7Xdgd6XSQdZGxt4GieXLwIAXA0nmAFlZB7EAJcyHqz0hVo6m
702Q/m8Ja8hBw6lmyM5uCduF61BhnQDfuDQetLgGzrvOp3m2qfTag3QGtEijwhH8L2O
7033xuMqMjtJutTa557go0p+PLjAhMVQ0S7z+3aLn/368qnqlxSflDCPe4GMcaXmOCz
704RdMJMk9txqB8GM5F2sO3
705=gtrA
706-----END PGP SIGNATURE-----
707",
708b"-----BEGIN PGP SIGNED MESSAGE-----
709
710Origin: . xenial
711Label: . xenial
712Suite: xenial
713Codename: xenial
714Date: Fri, 10 Feb 2023 21:24:49 UTC
715Architectures: amd64
716Components: main
717Description: Generated by aptly
718MD5Sum:
719 1044a9316b629fb7ea4b964ecaf1ccf3    21255 main/binary-amd64/Packages.gz
720 6ee4dcbdb0c0e98e416b94f542f6cc1b    17584 main/binary-amd64/Packages.bz2
721 fdb168fb0b8f575585d917ca0ffd98bb     4783 main/Contents-amd64.gz
722 d8c35b55bc8e48e267b9ccdaf383976d       85 main/binary-amd64/Release
723 b2f1f73fabd4acfaca43d05bee1debca   130864 main/binary-amd64/Packages
724SHA1:
725 1bcb7cd08c94a3519b2dce77f3f5f5e16c312067    21255 main/binary-amd64/Packages.gz
726 61c0f6b35c7bd3a5f59094511ccd593dcb2b8c96    17584 main/binary-amd64/Packages.bz2
727 e800a4c83e8d9ef564f5869ad838962550799c5e     4783 main/Contents-amd64.gz
728 992cb9cd8a0af2d9ad81d2b45342656d41157202       85 main/binary-amd64/Release
729 4a7c86cb1c0caa36c92e1af844ebf0b7e2bf4cea   130864 main/binary-amd64/Packages
730SHA256:
731 481c1bf74f609fbf71eed01da98a05cbe884acc2efd6d0e2c1c65f9e72ddc2e6    21255 main/binary-amd64/Packages.gz
732 d9f8cc2cc5b2aa854c509caf96d9e1457e6cb0fd55597ac49408a96afb8a727b    17584 main/binary-amd64/Packages.bz2
733 e04d5fd71915c3003d55f3927e5af71a4831e30ffbb0efec6dceb36cd1b054fa     4783 main/Contents-amd64.gz
734 e593f5bb98e0b6dbf5d0636ebff298b905b98a00402e2b20173fdb5da85c46d9       85 main/binary-amd64/Release
735 d0bec4d8f926383f3d61dc79b8d5d352f71adcf8befb59e8e02aeabe8c19eeba   130864 main/binary-amd64/Packages
736-----BEGIN PGP SIGNATURE-----
737
738wsFcBAEBCAAGBQJj5rYjAAoJENmAoXRX9vsG3wUP/2X7ufCo5nJkyHhzOtTEI4Pq
739rz6P94r2S/OA7v99mVkKNyOYZ8hKMNccYumvkWaXBF+WkLemCPeJxaBbRUrulu3c
740GXNPHht8dusQYIxS2VQVYbHgXfwQ+Y3+P1wVLPNT+9Ka0POkUT4YiM1G8Zx3fwTq
741zUeCpV1TKgkrVQ4CF5DX8i9tcVmYUq8B+BouwQAFJxElM1cuYqGybG19H/od77nH
742tkv3n43P0TCZ9KR48ZXWXF+6v26SRse2YkergbNOtJwRfdMHzvc8d/nb7T/Iv3jM
743WmqUs6Ob4EioUTWYwi2H3y+LnzAPeSVEklfCS61LzlyFGelpxHGuTjaaMtCI2Bkb
744f3XyNjeVwUYmnGWrBMCI38CUnY0J0oXLrVUxYZoT0O9SSO2bpql64T2Flqn10Djk
745W8j7V9a5gNO69PkNEHWUylwolFvF/H8Zmc6QZbnnbFSpC4pMEeRhoI1v1CqPSMn8
746APOGWa1xHN9hj9g4AZfXvO56BDveo9lbNOmFs2EAmBEEj2hCiroRtuCxxDmwerq3
747MLtCJIkir3JdbefexXcbIoP5+tjl573nvKU+Kb4KhCJTBDEY6+6qZKTSBDESKTvq
748T2L60YwfXZsj6WCS9roTz9llmze3YjURbHNZpf4BO3zONwNNeqFZw3qYWNCyzRS+
749R4AjBHbzlyIGpU5BGNn3
750=KMXz
751-----END PGP SIGNATURE-----
752"
753];
754        for data in data {
755            let (signed, _) = Signed::from_bytes(data).unwrap();
756            db_a.add_release(
757                &"DBA36B5181D0C816F630E889D980A17457F6FB06".parse()?,
758                &signed,
759            )
760            .await
761            .unwrap();
762        }
763
764        // Calculate the sync index
765        let tree_query = example_tree_query();
766        assert_eq!(
767            index_from_scan(&db_a, &tree_query).await.unwrap(),
768            (
769                "sha256:8cfc7ff62a9b8c8159788f8238a4fabab5c839ae63c20572ae0a6db912f666fb"
770                    .to_string(),
771                3
772            ),
773        );
774        assert_eq!(
775            index_from_scan(&db_b, &tree_query).await.unwrap(),
776            empty_index()
777        );
778
779        // Check initial database content
780        let keys_a = db_a.scan_keys(b"").try_collect::<Vec<_>>().await.unwrap();
781        let keys_b = db_b.scan_keys(b"").try_collect::<Vec<_>>().await.unwrap();
782        assert_eq!(keys_a.len(), 3);
783        assert_eq!(keys_b.len(), 0);
784
785        // Run sync
786        run_sync(&keyring, &mut db_a, &mut db_b).await.unwrap();
787
788        // Check database content afterwards
789        let keys_a = db_a.scan_keys(b"").try_collect::<Vec<_>>().await.unwrap();
790        let keys_b = db_b.scan_keys(b"").try_collect::<Vec<_>>().await.unwrap();
791        assert_eq!(keys_a.len(), 3);
792        assert_eq!(keys_b.len(), 3);
793
794        // Calculate the sync index
795        let tree_query = example_tree_query();
796        assert_eq!(
797            index_from_scan(&db_a, &tree_query).await.unwrap(),
798            (
799                "sha256:8cfc7ff62a9b8c8159788f8238a4fabab5c839ae63c20572ae0a6db912f666fb"
800                    .to_string(),
801                3
802            ),
803        );
804        assert_eq!(
805            index_from_scan(&db_b, &tree_query).await.unwrap(),
806            (
807                "sha256:8cfc7ff62a9b8c8159788f8238a4fabab5c839ae63c20572ae0a6db912f666fb"
808                    .to_string(),
809                3
810            ),
811        );
812
813        Ok(())
814    }
815
816    #[tokio::test]
817    async fn test_sync_from_partial() -> Result<()> {
818        init();
819
820        let keyring = Keyring::new(EXAMPLE_KEYRING).unwrap();
821        let (_, mut db_a, mut db_b) = open_temp_dbs().await.unwrap();
822
823        let data = [
824        b"-----BEGIN PGP SIGNED MESSAGE-----
825
826Origin: . xenial
827Label: . xenial
828Suite: xenial
829Codename: xenial
830Date: Thu, 23 Feb 2023 01:55:04 UTC
831Architectures: amd64
832Components: main
833Description: Generated by aptly
834MD5Sum:
835 cdb20787f1556bb38ae3b6017ef51327   132984 main/binary-amd64/Packages
836 001fc41d6c21eb85a43a13133584cbae    21567 main/binary-amd64/Packages.gz
837 3fb4f1a0169c3b2fff2c43c2a2277b51    17923 main/binary-amd64/Packages.bz2
838 c911b1bc4adf556f6fbd17c0c9cd8315     4794 main/Contents-amd64.gz
839 d8c35b55bc8e48e267b9ccdaf383976d       85 main/binary-amd64/Release
840SHA1:
841 455673b692a697ad3ada91a875096365f8da1524   132984 main/binary-amd64/Packages
842 e6a940039dcfc7f93f4a5501f15e75d1427b9464    21567 main/binary-amd64/Packages.gz
843 b8447294816bb063c10d0ba35f48dd5d1980f795    17923 main/binary-amd64/Packages.bz2
844 b6fd643edc8846c0914b44f3182dfc086877d944     4794 main/Contents-amd64.gz
845 992cb9cd8a0af2d9ad81d2b45342656d41157202       85 main/binary-amd64/Release
846SHA256:
847 989c22244106e44d789400d4da33d2ed64228ce94f48d1c2c37493118c992384   132984 main/binary-amd64/Packages
848 c46198172d00d4e01388832b61186a888da47e2c119c1e9dd7378fea206b1237    21567 main/binary-amd64/Packages.gz
849 b368e24d5c137448095f8940e3b371bff83e3e56159df6c58d4be83732a85554    17923 main/binary-amd64/Packages.bz2
850 bb347cbc00e02d73fef513965f1cd9f9e73100cd34097c43fdd1414668ec8ed8     4794 main/Contents-amd64.gz
851 e593f5bb98e0b6dbf5d0636ebff298b905b98a00402e2b20173fdb5da85c46d9       85 main/binary-amd64/Release
852-----BEGIN PGP SIGNATURE-----
853
854wsFcBAEBCAAGBQJj9sd6AAoJENmAoXRX9vsGOQ0P/3S63ctKl7QyxmRQ4UVJl70S
855hTxA90FbWp236nrEWw4EO/eVWiR/VbgFPacp/dyBpSmtTFl5cpOeyf2SYj5qfg5L
856cemYgUbaxRl+PBFGm7A14y82Ym0MUzF9cNWVK8bDXH9BKSljKKerXr4giOwjTkgh
857z2LoLxnrbhGkIWnSNiT0YvQrkxkSC5BjOInRiy/4Dr7LFAX/7KBzyPVwiDPxWQca
858dwtmI6EoZQP+zHDTR6RwnYOB7oME8aYIruwF9Vhu/unfdC4LpbNJDGL7VwQKUp8h
859ICupSwnRmHPV2raNBq58K6OunGvFO0oFaYUIQqbvGzu/5859YWhrdd7gBd9Fj4zI
860Ff7fHC+ZigCNCk7op4LykJ/3uJF8NvFlNxiagO+1tRko3V4tNbeSrXEKDhr5RQJz
861p/VdL1TXI/pVIobxbF5D/Lo8dCs5LjJsJ5rFlPgzjlREFn0hwKcDwB7M+rbPhuHV
8621R3lgdhW01ZghwOdTMiX1cShQwE7bvGtskn2WIHyIhEawpotGNpBFG2K5TdxfXA1
863m+wu4PLxfxOSb+VoQlH1enyDcR7m7XNtt692l++6nw3rq6Wv2zNc9DHRE+HNavJg
864zwlfH3L9OOoGfPMfRxrKqFzcob2gnKjptlHt3XpUx5ZwS4hcKB2lETT9ORVxe1NI
865rK5KKL67o5aLviVqo98l
866=MI63
867-----END PGP SIGNATURE-----
868",
869b"-----BEGIN PGP SIGNED MESSAGE-----
870
871Origin: . xenial
872Label: . xenial
873Suite: xenial
874Codename: xenial
875Date: Wed, 15 Feb 2023 23:18:08 UTC
876Architectures: amd64
877Components: main
878Description: Generated by aptly
879MD5Sum:
880 bea5a0f7c99209504f22d8faf10125fd   132287 main/binary-amd64/Packages
881 1aa4c130945a3a076a9f16546ca17a83    21467 main/binary-amd64/Packages.gz
882 f18c7f0779161104fed2aec72d9a44e2    17922 main/binary-amd64/Packages.bz2
883 fdb168fb0b8f575585d917ca0ffd98bb     4783 main/Contents-amd64.gz
884 d8c35b55bc8e48e267b9ccdaf383976d       85 main/binary-amd64/Release
885SHA1:
886 d01c164d99cf7c867b5f115770f58e4916d7a15f   132287 main/binary-amd64/Packages
887 41e78c4c558567f4936d9952eb928a32911cc56c    21467 main/binary-amd64/Packages.gz
888 8837bf2f3e2bad7c73712d003c1510c6171c53ee    17922 main/binary-amd64/Packages.bz2
889 e800a4c83e8d9ef564f5869ad838962550799c5e     4783 main/Contents-amd64.gz
890 992cb9cd8a0af2d9ad81d2b45342656d41157202       85 main/binary-amd64/Release
891SHA256:
892 9f9178b66c4d1d31d7b2b741f0835c2140552cac68861beaf2ecc55f0364c620   132287 main/binary-amd64/Packages
893 57386742060a0913236bafd5e8eb3b3334284e0d2ab8362a7c22c78175e9d89b    21467 main/binary-amd64/Packages.gz
894 ec097b64b5e3a39760a9b5ea6b02e91d0401994464dd0ce3de2a0a26a62230e2    17922 main/binary-amd64/Packages.bz2
895 e04d5fd71915c3003d55f3927e5af71a4831e30ffbb0efec6dceb36cd1b054fa     4783 main/Contents-amd64.gz
896 e593f5bb98e0b6dbf5d0636ebff298b905b98a00402e2b20173fdb5da85c46d9       85 main/binary-amd64/Release
897-----BEGIN PGP SIGNATURE-----
898
899wsFcBAABCAAGBQJj7WgwAAoJENmAoXRX9vsG69AQAJmvducnhHqCXQIsqjXrDMjU
900QUAw56MRunn7rHTFpJY0ZPLgQ5gVBibouNZ9x78wuJ784Sl+MIHC7RWdQYBEbWQ5
901haKjiI00BzDeXx4sUet1E+Ce5dhjK/UvoZIOy+ed5nv/HM7QFrvoxdADSDnYGy2o
902djFUVWR5kzkb5Tv7bcjJQWWf6JvY1Z12CgsG85ECYv2PE+tGgQjSwbxRDvFFzY1O
903Xy1EkjT+YDG6hy5CiKSZL7qPsjsLHeuRvat3oSlBWiFRnSuLOlsDozqzYMFqNx93
904GPQiFNiYEmkxDxiKLOcds7+Plz2FjQdQwv2msllJ4jA9PxYRiEbfH14/ELk+/snE
90566XID9dv91JbrwaI3NOoJZZmN+QYZ7WaAj3Uxl3cYnCGuIIt6z4KB2CYeyRa3f3K
906HbPq0mBchPPmavaQEfaNDQ+dzMuazR0VMoKfHGEp44r+XU+JH/lNzlxgQEMgVv43
9070B++zb4MYgheGUhu7Xdgd6XSQdZGxt4GieXLwIAXA0nmAFlZB7EAJcyHqz0hVo6m
908Q/m8Ja8hBw6lmyM5uCduF61BhnQDfuDQetLgGzrvOp3m2qfTag3QGtEijwhH8L2O
9093xuMqMjtJutTa557go0p+PLjAhMVQ0S7z+3aLn/368qnqlxSflDCPe4GMcaXmOCz
910RdMJMk9txqB8GM5F2sO3
911=gtrA
912-----END PGP SIGNATURE-----
913",
914b"-----BEGIN PGP SIGNED MESSAGE-----
915
916Origin: . xenial
917Label: . xenial
918Suite: xenial
919Codename: xenial
920Date: Fri, 10 Feb 2023 21:24:49 UTC
921Architectures: amd64
922Components: main
923Description: Generated by aptly
924MD5Sum:
925 1044a9316b629fb7ea4b964ecaf1ccf3    21255 main/binary-amd64/Packages.gz
926 6ee4dcbdb0c0e98e416b94f542f6cc1b    17584 main/binary-amd64/Packages.bz2
927 fdb168fb0b8f575585d917ca0ffd98bb     4783 main/Contents-amd64.gz
928 d8c35b55bc8e48e267b9ccdaf383976d       85 main/binary-amd64/Release
929 b2f1f73fabd4acfaca43d05bee1debca   130864 main/binary-amd64/Packages
930SHA1:
931 1bcb7cd08c94a3519b2dce77f3f5f5e16c312067    21255 main/binary-amd64/Packages.gz
932 61c0f6b35c7bd3a5f59094511ccd593dcb2b8c96    17584 main/binary-amd64/Packages.bz2
933 e800a4c83e8d9ef564f5869ad838962550799c5e     4783 main/Contents-amd64.gz
934 992cb9cd8a0af2d9ad81d2b45342656d41157202       85 main/binary-amd64/Release
935 4a7c86cb1c0caa36c92e1af844ebf0b7e2bf4cea   130864 main/binary-amd64/Packages
936SHA256:
937 481c1bf74f609fbf71eed01da98a05cbe884acc2efd6d0e2c1c65f9e72ddc2e6    21255 main/binary-amd64/Packages.gz
938 d9f8cc2cc5b2aa854c509caf96d9e1457e6cb0fd55597ac49408a96afb8a727b    17584 main/binary-amd64/Packages.bz2
939 e04d5fd71915c3003d55f3927e5af71a4831e30ffbb0efec6dceb36cd1b054fa     4783 main/Contents-amd64.gz
940 e593f5bb98e0b6dbf5d0636ebff298b905b98a00402e2b20173fdb5da85c46d9       85 main/binary-amd64/Release
941 d0bec4d8f926383f3d61dc79b8d5d352f71adcf8befb59e8e02aeabe8c19eeba   130864 main/binary-amd64/Packages
942-----BEGIN PGP SIGNATURE-----
943
944wsFcBAEBCAAGBQJj5rYjAAoJENmAoXRX9vsG3wUP/2X7ufCo5nJkyHhzOtTEI4Pq
945rz6P94r2S/OA7v99mVkKNyOYZ8hKMNccYumvkWaXBF+WkLemCPeJxaBbRUrulu3c
946GXNPHht8dusQYIxS2VQVYbHgXfwQ+Y3+P1wVLPNT+9Ka0POkUT4YiM1G8Zx3fwTq
947zUeCpV1TKgkrVQ4CF5DX8i9tcVmYUq8B+BouwQAFJxElM1cuYqGybG19H/od77nH
948tkv3n43P0TCZ9KR48ZXWXF+6v26SRse2YkergbNOtJwRfdMHzvc8d/nb7T/Iv3jM
949WmqUs6Ob4EioUTWYwi2H3y+LnzAPeSVEklfCS61LzlyFGelpxHGuTjaaMtCI2Bkb
950f3XyNjeVwUYmnGWrBMCI38CUnY0J0oXLrVUxYZoT0O9SSO2bpql64T2Flqn10Djk
951W8j7V9a5gNO69PkNEHWUylwolFvF/H8Zmc6QZbnnbFSpC4pMEeRhoI1v1CqPSMn8
952APOGWa1xHN9hj9g4AZfXvO56BDveo9lbNOmFs2EAmBEEj2hCiroRtuCxxDmwerq3
953MLtCJIkir3JdbefexXcbIoP5+tjl573nvKU+Kb4KhCJTBDEY6+6qZKTSBDESKTvq
954T2L60YwfXZsj6WCS9roTz9llmze3YjURbHNZpf4BO3zONwNNeqFZw3qYWNCyzRS+
955R4AjBHbzlyIGpU5BGNn3
956=KMXz
957-----END PGP SIGNATURE-----
958"
959];
960        for data in data {
961            let (signed, _) = Signed::from_bytes(data).unwrap();
962            db_a.add_release(
963                &"DBA36B5181D0C816F630E889D980A17457F6FB06".parse()?,
964                &signed,
965            )
966            .await
967            .unwrap();
968        }
969
970        let (signed, _) = Signed::from_bytes(b"-----BEGIN PGP SIGNED MESSAGE-----
971
972Origin: . xenial
973Label: . xenial
974Suite: xenial
975Codename: xenial
976Date: Wed, 15 Feb 2023 23:18:08 UTC
977Architectures: amd64
978Components: main
979Description: Generated by aptly
980MD5Sum:
981 bea5a0f7c99209504f22d8faf10125fd   132287 main/binary-amd64/Packages
982 1aa4c130945a3a076a9f16546ca17a83    21467 main/binary-amd64/Packages.gz
983 f18c7f0779161104fed2aec72d9a44e2    17922 main/binary-amd64/Packages.bz2
984 fdb168fb0b8f575585d917ca0ffd98bb     4783 main/Contents-amd64.gz
985 d8c35b55bc8e48e267b9ccdaf383976d       85 main/binary-amd64/Release
986SHA1:
987 d01c164d99cf7c867b5f115770f58e4916d7a15f   132287 main/binary-amd64/Packages
988 41e78c4c558567f4936d9952eb928a32911cc56c    21467 main/binary-amd64/Packages.gz
989 8837bf2f3e2bad7c73712d003c1510c6171c53ee    17922 main/binary-amd64/Packages.bz2
990 e800a4c83e8d9ef564f5869ad838962550799c5e     4783 main/Contents-amd64.gz
991 992cb9cd8a0af2d9ad81d2b45342656d41157202       85 main/binary-amd64/Release
992SHA256:
993 9f9178b66c4d1d31d7b2b741f0835c2140552cac68861beaf2ecc55f0364c620   132287 main/binary-amd64/Packages
994 57386742060a0913236bafd5e8eb3b3334284e0d2ab8362a7c22c78175e9d89b    21467 main/binary-amd64/Packages.gz
995 ec097b64b5e3a39760a9b5ea6b02e91d0401994464dd0ce3de2a0a26a62230e2    17922 main/binary-amd64/Packages.bz2
996 e04d5fd71915c3003d55f3927e5af71a4831e30ffbb0efec6dceb36cd1b054fa     4783 main/Contents-amd64.gz
997 e593f5bb98e0b6dbf5d0636ebff298b905b98a00402e2b20173fdb5da85c46d9       85 main/binary-amd64/Release
998-----BEGIN PGP SIGNATURE-----
999
1000wsFcBAABCAAGBQJj7WgwAAoJENmAoXRX9vsG69AQAJmvducnhHqCXQIsqjXrDMjU
1001QUAw56MRunn7rHTFpJY0ZPLgQ5gVBibouNZ9x78wuJ784Sl+MIHC7RWdQYBEbWQ5
1002haKjiI00BzDeXx4sUet1E+Ce5dhjK/UvoZIOy+ed5nv/HM7QFrvoxdADSDnYGy2o
1003djFUVWR5kzkb5Tv7bcjJQWWf6JvY1Z12CgsG85ECYv2PE+tGgQjSwbxRDvFFzY1O
1004Xy1EkjT+YDG6hy5CiKSZL7qPsjsLHeuRvat3oSlBWiFRnSuLOlsDozqzYMFqNx93
1005GPQiFNiYEmkxDxiKLOcds7+Plz2FjQdQwv2msllJ4jA9PxYRiEbfH14/ELk+/snE
100666XID9dv91JbrwaI3NOoJZZmN+QYZ7WaAj3Uxl3cYnCGuIIt6z4KB2CYeyRa3f3K
1007HbPq0mBchPPmavaQEfaNDQ+dzMuazR0VMoKfHGEp44r+XU+JH/lNzlxgQEMgVv43
10080B++zb4MYgheGUhu7Xdgd6XSQdZGxt4GieXLwIAXA0nmAFlZB7EAJcyHqz0hVo6m
1009Q/m8Ja8hBw6lmyM5uCduF61BhnQDfuDQetLgGzrvOp3m2qfTag3QGtEijwhH8L2O
10103xuMqMjtJutTa557go0p+PLjAhMVQ0S7z+3aLn/368qnqlxSflDCPe4GMcaXmOCz
1011RdMJMk9txqB8GM5F2sO3
1012=gtrA
1013-----END PGP SIGNATURE-----
1014").unwrap();
1015
1016        db_b.add_release(
1017            &"DBA36B5181D0C816F630E889D980A17457F6FB06".parse()?,
1018            &signed,
1019        )
1020        .await
1021        .unwrap();
1022
1023        // Calculate the sync index
1024        let tree_query = example_tree_query();
1025        assert_eq!(
1026            index_from_scan(&db_a, &tree_query).await.unwrap(),
1027            (
1028                "sha256:8cfc7ff62a9b8c8159788f8238a4fabab5c839ae63c20572ae0a6db912f666fb"
1029                    .to_string(),
1030                3
1031            ),
1032        );
1033        assert_eq!(
1034            index_from_scan(&db_b, &tree_query).await.unwrap(),
1035            (
1036                "sha256:ec661134c817590c2b39c6e004bcab527a3b9d83702c62fbb7d5a1546a639df4"
1037                    .to_string(),
1038                1
1039            ),
1040        );
1041
1042        // Check initial database content
1043        let keys_a = db_a.scan_keys(b"").try_collect::<Vec<_>>().await.unwrap();
1044        let keys_b = db_b.scan_keys(b"").try_collect::<Vec<_>>().await.unwrap();
1045        assert_eq!(keys_a.len(), 3);
1046        assert_eq!(keys_b.len(), 1);
1047
1048        // Run sync
1049        run_sync(&keyring, &mut db_a, &mut db_b).await.unwrap();
1050
1051        // Check database content afterwards
1052        let keys_a = db_a.scan_keys(b"").try_collect::<Vec<_>>().await.unwrap();
1053        let keys_b = db_b.scan_keys(b"").try_collect::<Vec<_>>().await.unwrap();
1054        assert_eq!(keys_a.len(), 3);
1055        assert_eq!(keys_b.len(), 3);
1056
1057        // Calculate the sync index
1058        let tree_query = example_tree_query();
1059        assert_eq!(
1060            index_from_scan(&db_a, &tree_query).await.unwrap(),
1061            (
1062                "sha256:8cfc7ff62a9b8c8159788f8238a4fabab5c839ae63c20572ae0a6db912f666fb"
1063                    .to_string(),
1064                3
1065            ),
1066        );
1067        assert_eq!(
1068            index_from_scan(&db_b, &tree_query).await.unwrap(),
1069            (
1070                "sha256:8cfc7ff62a9b8c8159788f8238a4fabab5c839ae63c20572ae0a6db912f666fb"
1071                    .to_string(),
1072                3
1073            ),
1074        );
1075
1076        Ok(())
1077    }
1078}