Skip to main content

rig_memory/
rewrite.rs

1//! Rewriting a session's saved history in place ([`rewrite`]) through a
2//! [`Rewrite`]: [`Strings`] maps every string, [`Messages`] only messages.
3
4use std::collections::{BTreeSet, HashMap};
5use std::io;
6use std::sync::Arc;
7
8use rig_core::completion::Message;
9use rig_core::conversation::{Aside, Author, Change, Conversation, Lineage};
10use serde::de::DeserializeOwned;
11use serde::{Deserialize, Serialize};
12use serde_json::Value;
13
14use crate::blobs::{BLOBS, blob_names, named_blobs, put_blob, read_blob, store_change};
15use crate::format::{Body, Header, LogLines, LogRewrite, Meta, parse, write_change, write_meta};
16use crate::session::{Fold, Folded, fold_lines, read_header, read_logs};
17use crate::writer::{refused, set_aside_tail};
18use crate::{Files, MemoryStore, SessionError, Stamp, Store};
19use rig_core::now_ms;
20
21/// Where a value being rewritten is kept.
22#[derive(Clone, Copy, Debug)]
23pub enum Place<'a> {
24    /// In a record of the log `log` (an agent's id or
25    /// [`SESSION`](crate::SESSION)), stamped `stamp`, made by `by`.
26    Record {
27        /// The log.
28        log: &'a str,
29        /// The record's stamp.
30        stamp: Stamp,
31        /// Who made the record.
32        by: Option<&'a Author>,
33    },
34    /// In the file `name`: one of the store's, one a host keeps outside it,
35    /// or a copy of either held in memory.
36    File {
37        /// The file.
38        name: &'a str,
39    },
40}
41
42/// A change applied to every value a session keeps. Each hook edits its
43/// argument in place and returns whether it changed anything. Every hook
44/// defaults to no change, except [`file`](Self::file), which sends JSON
45/// files to [`value`](Self::value), JSON-lines files line by line, and
46/// other UTF-8 to [`text`](Self::text). A hook cannot add, drop or re-kind
47/// a record. Shared between owners, so it takes `&self`.
48pub trait Rewrite: Send + Sync {
49    /// A message of a push, a splice or a reset. Images are as stored:
50    /// `blob:<sha256>.<ext>` in a push or a splice, inline in a reset;
51    /// inline data put in a push or a splice is stored as a blob.
52    fn message(&self, at: &Place<'_>, message: &mut Message) -> bool {
53        let _ = (at, message);
54        false
55    }
56
57    /// An aside put or replaced.
58    fn aside(&self, at: &Place<'_>, aside: &mut Aside) -> bool {
59        let _ = (at, aside);
60        false
61    }
62
63    /// A saved value under `key`, or a JSON value of a file (`key` is its
64    /// name). A value made `null` is removed.
65    fn value(&self, at: &Place<'_>, key: &str, value: &mut Value) -> bool {
66        let _ = (at, key, value);
67        false
68    }
69
70    /// Text of a file that is not JSON.
71    fn text(&self, at: &Place<'_>, text: &mut String) -> bool {
72        let _ = (at, text);
73        false
74    }
75
76    /// The file `name`, whole.
77    fn file(&self, name: &str, bytes: &mut Vec<u8>) -> bool {
78        map_file(self, name, bytes)
79    }
80}
81
82/// Every string, keys included, of every value: messages and asides through
83/// their serde form, saved values, text and every file. Redaction and path
84/// remapping are this. A string that is part of a message's or an aside's
85/// form, such as a field name or a tag, is left as it is where changing it
86/// would make the value unreadable.
87///
88/// ```
89/// use rig_core::conversation::Author;
90/// use rig_core::message::Message;
91/// use rig_memory::{MemoryStore, Session, Strings, load, rewrite, save};
92///
93/// let store = MemoryStore::default();
94/// let mut conversation = load(&store, "main")?;
95/// conversation.push(Message::user("my key is sk-123"));
96/// save(&store, "main", &mut conversation)?;
97/// let redact = Strings(|text: &mut String| {
98///     let hit = text.contains("sk-123");
99///     if hit {
100///         *text = text.replace("sk-123", "[REDACTED]");
101///     }
102///     hit
103/// });
104/// let rewritten = rewrite(&store, Author::plugin("redact"), &redact)?;
105/// assert_eq!(rewritten.logs.len(), 1);
106/// let main = Session::read(&store)?.agents.remove(0);
107/// assert_eq!(main.conversation.messages(), [Message::user("my key is [REDACTED]").into()]);
108/// # Ok::<(), rig_memory::SessionError>(())
109/// ```
110pub struct Strings<F: Fn(&mut String) -> bool + Send + Sync>(pub F);
111
112impl<F: Fn(&mut String) -> bool + Send + Sync> Rewrite for Strings<F> {
113    fn message(&self, _: &Place<'_>, message: &mut Message) -> bool {
114        map_serde(message, &self.0)
115    }
116
117    fn aside(&self, _: &Place<'_>, aside: &mut Aside) -> bool {
118        map_serde(aside, &self.0)
119    }
120
121    fn value(&self, _: &Place<'_>, _: &str, value: &mut Value) -> bool {
122        map_strings(value, &self.0)
123    }
124
125    fn text(&self, _: &Place<'_>, text: &mut String) -> bool {
126        (self.0)(text)
127    }
128}
129
130/// Only messages; files are left.
131pub struct Messages<F: Fn(&mut Message) -> bool + Send + Sync>(pub F);
132
133impl<F: Fn(&mut Message) -> bool + Send + Sync> Rewrite for Messages<F> {
134    fn message(&self, _: &Place<'_>, message: &mut Message) -> bool {
135        (self.0)(message)
136    }
137
138    fn file(&self, _: &str, _: &mut Vec<u8>) -> bool {
139        false
140    }
141}
142
143/// Maps every string (keys included) of `value` through `f`; whether any
144/// changed.
145pub fn map_strings(value: &mut Value, f: &dyn Fn(&mut String) -> bool) -> bool {
146    match value {
147        Value::String(text) => f(text),
148        Value::Array(items) => items
149            .iter_mut()
150            .fold(false, |changed, item| map_strings(item, f) | changed),
151        Value::Object(map) => {
152            let mut changed = map
153                .values_mut()
154                .fold(false, |changed, item| map_strings(item, f) | changed);
155            let renamed: Vec<(String, String)> = map
156                .keys()
157                .filter_map(|key| {
158                    let mut new = key.clone();
159                    f(&mut new).then(|| (key.clone(), new))
160                })
161                .collect();
162            if !renamed.is_empty() {
163                let renamed: HashMap<String, String> = renamed.into_iter().collect();
164                *map = std::mem::take(map)
165                    .into_iter()
166                    .map(|(key, item)| match renamed.get(&key) {
167                        Some(new) => (new.clone(), item),
168                        None => (key, item),
169                    })
170                    .collect();
171                changed = true;
172            }
173            changed
174        }
175        Value::Null | Value::Bool(_) | Value::Number(_) => false,
176    }
177}
178
179/// Maps every string of `item`'s serde form through `f`, keys included,
180/// and reads it back; whether it changed. When the whole map does not read
181/// back (it changed a field name or a tag), each string is mapped on its
182/// own and kept where it reads back.
183fn map_serde<T: Serialize + DeserializeOwned>(
184    item: &mut T,
185    f: &dyn Fn(&mut String) -> bool,
186) -> bool {
187    let Ok(mut value) = serde_json::to_value(&*item) else {
188        return false;
189    };
190    if !map_strings(&mut value, f) {
191        return false;
192    }
193    if let Ok(read) = T::deserialize(&value) {
194        *item = read;
195        return true;
196    }
197    let Ok(mut kept) = serde_json::to_value(&*item) else {
198        return false;
199    };
200    let mut leaves = Vec::new();
201    strings_of(&kept, &mut Vec::new(), f, &mut leaves);
202    let mut changed = false;
203    for leaf in leaves {
204        let mut trial = kept.clone();
205        if map_leaf(&mut trial, &leaf, f)
206            && let Ok(read) = T::deserialize(&trial)
207        {
208            *item = read;
209            kept = trial;
210            changed = true;
211        }
212    }
213    changed
214}
215
216/// A step from a JSON value to one inside it.
217#[derive(Clone)]
218enum Step {
219    Index(usize),
220    Key(String),
221}
222
223/// A string of a JSON value: the value at `path`, or with `key`, the name
224/// of the last step of `path`.
225struct Leaf {
226    path: Vec<Step>,
227    key: bool,
228}
229
230/// Every string of `value` that `f` changes, each after the strings inside
231/// it, so changing one leaves the paths of the rest.
232fn strings_of(
233    value: &Value,
234    path: &mut Vec<Step>,
235    f: &dyn Fn(&mut String) -> bool,
236    leaves: &mut Vec<Leaf>,
237) {
238    let changes = |text: &str| f(&mut text.to_owned());
239    match value {
240        Value::String(text) if changes(text) => leaves.push(Leaf {
241            path: path.clone(),
242            key: false,
243        }),
244        Value::Array(items) => {
245            for (at, item) in items.iter().enumerate() {
246                path.push(Step::Index(at));
247                strings_of(item, path, f, leaves);
248                path.pop();
249            }
250        }
251        Value::Object(map) => {
252            for (key, item) in map {
253                path.push(Step::Key(key.clone()));
254                strings_of(item, path, f, leaves);
255                if changes(key) {
256                    leaves.push(Leaf {
257                        path: path.clone(),
258                        key: true,
259                    });
260                }
261                path.pop();
262            }
263        }
264        _ => {}
265    }
266}
267
268/// Maps the string `leaf` names in `value` through `f`; whether it changed.
269fn map_leaf(value: &mut Value, leaf: &Leaf, f: &dyn Fn(&mut String) -> bool) -> bool {
270    let (last, parent) = match leaf.path.split_last() {
271        Some((last, parent)) if leaf.key => (Some(last), parent),
272        _ => (None, leaf.path.as_slice()),
273    };
274    let mut at = value;
275    for step in parent {
276        let next = match step {
277            Step::Index(index) => at.get_mut(*index),
278            Step::Key(key) => at.get_mut(key.as_str()),
279        };
280        let Some(next) = next else {
281            return false;
282        };
283        at = next;
284    }
285    match (last, at) {
286        (None, Value::String(text)) => f(text),
287        (Some(Step::Key(key)), Value::Object(map)) => {
288            let mut new = key.clone();
289            if !f(&mut new) || map.contains_key(&new) || !map.contains_key(key) {
290                return false;
291            }
292            *map = std::mem::take(map)
293                .into_iter()
294                .map(|(old, item)| match old == *key {
295                    true => (new.clone(), item),
296                    false => (old, item),
297                })
298                .collect();
299            true
300        }
301        _ => false,
302    }
303}
304
305/// `item` mapped through `with` by its serde form, as a JSON value of a
306/// file is ([`Rewrite::value`] at `at`, under `key`): whether it changed.
307/// How a host or a plugin maps what it keeps of a rewritten session, such
308/// as a copy of a tool call held in memory.
309///
310/// # Errors
311///
312/// When the mapped form no longer reads as a `T`; `item` is left as it was,
313/// and the owner drops it or keeps it unreadable to no one.
314pub fn rewrite_value<T: Serialize + DeserializeOwned>(
315    with: &dyn Rewrite,
316    at: &Place<'_>,
317    key: &str,
318    item: &mut T,
319) -> Result<bool, serde_json::Error> {
320    let mut value = serde_json::to_value(&*item)?;
321    if !with.value(at, key, &mut value) {
322        return Ok(false);
323    }
324    *item = T::deserialize(value)?;
325    Ok(true)
326}
327
328/// The file `name` of `files` through [`Rewrite::file`], written back
329/// whole ([`Files::write_file`], atomic) when it changed: whether it did.
330/// A file that is not there is left. How a host or a plugin maps a file it
331/// keeps from the session outside the session's store, such as a history
332/// of prompts.
333///
334/// # Errors
335///
336/// When the file cannot be read or written.
337pub fn rewrite_file(files: &dyn Files, name: &str, with: &dyn Rewrite) -> io::Result<bool> {
338    let mut bytes = match files.read_file(name) {
339        Ok(bytes) => bytes,
340        Err(failure) if failure.kind() == io::ErrorKind::NotFound => return Ok(false),
341        Err(failure) => return Err(failure),
342    };
343    if !with.file(name, &mut bytes) {
344        return Ok(false);
345    }
346    files.write_file(name, &bytes)?;
347    Ok(true)
348}
349
350/// The generic file pass of [`Rewrite::file`].
351fn map_file<R: Rewrite + ?Sized>(with: &R, name: &str, bytes: &mut Vec<u8>) -> bool {
352    let at = Place::File { name };
353    if let Ok(mut value) = serde_json::from_slice::<Value>(bytes) {
354        if !with.value(&at, name, &mut value) {
355            return false;
356        }
357        let pretty = bytes.trim_ascii_end().contains(&b'\n');
358        let written = match pretty {
359            true => serde_json::to_vec_pretty(&value),
360            false => serde_json::to_vec(&value),
361        };
362        let Ok(mut written) = written else {
363            return false;
364        };
365        if bytes.ends_with(b"\n") {
366            written.push(b'\n');
367        }
368        *bytes = written;
369        return true;
370    }
371    let Ok(text) = std::str::from_utf8(bytes) else {
372        return false;
373    };
374    let lines: Vec<(&str, &str, Option<Value>)> = text
375        .split_inclusive('\n')
376        .map(|line| {
377            let (body, end) = line
378                .strip_suffix('\n')
379                .map_or((line, ""), |body| (body, "\n"));
380            (body, end, serde_json::from_str(body).ok())
381        })
382        .collect();
383    let complete: Vec<_> = lines
384        .iter()
385        .filter(|(body, end, _)| !end.is_empty() && !body.trim().is_empty())
386        .collect();
387    let json_lines = !complete.is_empty() && complete.iter().all(|line| line.2.is_some());
388    if !json_lines {
389        let mut text = text.to_owned();
390        let changed = with.text(&at, &mut text);
391        if changed {
392            *bytes = text.into_bytes();
393        }
394        return changed;
395    }
396    let mut out = String::with_capacity(text.len());
397    let mut changed = false;
398    for (body, end, value) in lines {
399        match value {
400            Some(mut value) => match with.value(&at, name, &mut value) {
401                true => {
402                    match serde_json::to_string(&value) {
403                        Ok(line) => out.push_str(&line),
404                        Err(_) => out.push_str(body),
405                    }
406                    changed = true;
407                }
408                false => out.push_str(body),
409            },
410            None => {
411                let mut line = body.to_owned();
412                changed |= with.text(&at, &mut line);
413                out.push_str(&line);
414            }
415        }
416        out.push_str(end);
417    }
418    if changed {
419        *bytes = out.into_bytes();
420    }
421    changed
422}
423
424/// What [`rewrite`] did.
425#[derive(Clone, Debug, Default)]
426pub struct Rewritten {
427    /// Each log a record of which changed, by name.
428    pub logs: Vec<RewrittenLog>,
429    /// The store files the rewrite changed.
430    pub files: Vec<String>,
431    /// The blobs no record named any more, removed.
432    pub blobs_removed: Vec<String>,
433}
434
435/// A log [`rewrite`] changed.
436#[derive(Clone, Debug)]
437pub struct RewrittenLog {
438    /// Its name: an agent's id, or [`SESSION`](crate::SESSION).
439    pub log: String,
440    /// How many of its records changed.
441    pub records: usize,
442}
443
444/// A log's records after its header, mapped through a [`Rewrite`].
445pub(crate) struct Mapped {
446    /// Its header and the header's stamp.
447    pub header: (Stamp, Header),
448    /// Each record after the header, a line each: those unchanged as they
449    /// were, byte for byte.
450    pub body: Vec<u8>,
451    /// How many records changed.
452    pub records: usize,
453}
454
455/// Maps the records of `lines`, the log `name`, through `with`. Images put
456/// inline in a push or a splice are stored in `blobs`. `None` when it holds
457/// no line.
458pub(crate) fn map_log(
459    name: &str,
460    lines: &LogLines,
461    with: &dyn Rewrite,
462    blobs: &dyn Store,
463) -> Result<Option<Mapped>, SessionError> {
464    let refuse = |line, why| SessionError::Record {
465        log: name.to_owned(),
466        line,
467        why,
468    };
469    let Some((_, _, first)) = lines.iter().next() else {
470        return Ok(None);
471    };
472    let (stamp, header) = read_header(name, first)?;
473    let mut mapped = Mapped {
474        header: (stamp, header),
475        body: Vec::new(),
476        records: 0,
477    };
478    for (line, _, bytes) in lines.iter().skip(1) {
479        let (stamp, body) = parse(bytes).map_err(|why| refuse(line, why))?;
480        match map_record(name, stamp, body, with, blobs).map_err(|why| refuse(line, why))? {
481            Some(record) => {
482                mapped.body.extend_from_slice(&record);
483                mapped.records += 1;
484            }
485            None => {
486                mapped.body.extend_from_slice(bytes);
487                mapped.body.push(b'\n');
488            }
489        }
490    }
491    Ok(Some(mapped))
492}
493
494/// The record `body` of the log `log`, stamped `stamp`, mapped through
495/// `with`, as a line; `None` when it did not change.
496pub(crate) fn map_record(
497    log: &str,
498    stamp: Stamp,
499    body: Body,
500    with: &dyn Rewrite,
501    blobs: &dyn Store,
502) -> Result<Option<Vec<u8>>, String> {
503    let mut line = Vec::new();
504    match body {
505        Body::Meta(Meta::State {
506            key,
507            value: Some(mut value),
508        }) if !value.is_null() => {
509            let at = Place::Record {
510                log,
511                stamp,
512                by: None,
513            };
514            if !with.value(&at, &key, &mut value) {
515                return Ok(None);
516            }
517            let state = Meta::State {
518                key,
519                value: Some(value),
520            };
521            write_meta(&mut line, stamp, &state).map_err(|failure| failure.to_string())?;
522        }
523        Body::Meta(_) => return Ok(None),
524        Body::Change(mut recorded) => {
525            let at = Place::Record {
526                log,
527                stamp,
528                by: recorded.by.as_ref(),
529            };
530            if !map_change(&mut recorded.change, &at, with)? {
531                return Ok(None);
532            }
533            let put = &mut |name: String, bytes: Vec<u8>| put_blob(blobs, &name, &bytes);
534            let stored = store_change(&recorded.change, put)
535                .map_err(|failure| format!("storing an image failed: {failure}"))?;
536            let change = stored.as_ref().unwrap_or(&recorded.change);
537            write_change(&mut line, stamp, recorded.by.as_ref(), change)
538                .map_err(|failure| failure.to_string())?;
539        }
540    }
541    Ok(Some(line))
542}
543
544/// Maps the messages and asides of `change` through `with`; whether any
545/// changed.
546pub(crate) fn map_change(
547    change: &mut Change,
548    at: &Place<'_>,
549    with: &dyn Rewrite,
550) -> Result<bool, String> {
551    Ok(match change {
552        Change::Push { entries } | Change::Splice { with: entries, .. } => {
553            entries.iter_mut().fold(false, |changed, entry| {
554                with.message(at, Arc::make_mut(&mut entry.message)) | changed
555            })
556        }
557        Change::Aside {
558            aside: Some(aside), ..
559        } => with.aside(at, aside),
560        Change::Reset { state } => map_state(state, at, with)?,
561        Change::Aside { aside: None, .. } | Change::Halt { .. } => false,
562    })
563}
564
565/// Maps the messages and saved asides of a reset's `state` through `with`,
566/// by its serde form; whether any changed.
567fn map_state(state: &mut Conversation, at: &Place<'_>, with: &dyn Rewrite) -> Result<bool, String> {
568    let fail = |failure: serde_json::Error| failure.to_string();
569    let mut value = serde_json::to_value(&*state).map_err(fail)?;
570    let mut changed = false;
571    if let Some(Value::Array(messages)) = value.get_mut("messages") {
572        for message in messages {
573            let mut typed = Message::deserialize(&*message).map_err(fail)?;
574            if with.message(at, &mut typed) {
575                *message = serde_json::to_value(&typed).map_err(fail)?;
576                changed = true;
577            }
578        }
579    }
580    if let Some(Value::Array(asides)) = value.get_mut("asides") {
581        for aside in asides.iter_mut().filter_map(|pair| pair.get_mut(1)) {
582            let mut typed = Aside::deserialize(&*aside).map_err(fail)?;
583            if with.aside(at, &mut typed) {
584                *aside = serde_json::to_value(&typed).map_err(fail)?;
585                changed = true;
586            }
587        }
588    }
589    if changed {
590        *state = Conversation::deserialize(&value).map_err(fail)?;
591    }
592    Ok(changed)
593}
594
595/// Rewrites the session in `store` through `with`, as done by `by`, in
596/// place: every record stays where it is with its stamp, and every value
597/// in it goes through `with` (see [`Rewrite`]). Each log a record of which
598/// changed gets a fresh lineage and `{t, by}` added to its header's
599/// `rewrites`.
600///
601/// Every log is read strictly, and every
602/// changed log is encoded and folded strictly in memory, before anything
603/// is written. Then torn tails
604/// are set aside (as [`Writer::open`](crate::Writer::open) does), new
605/// blobs are stored, each changed log is replaced
606/// ([`Store::replace_log`]), every file of the store goes through
607/// [`Rewrite::file`] (each written atomically), and blobs no record names
608/// any more are removed last.
609///
610/// What a reader sees: each log is the old one or the new one, whole;
611/// across logs a reader during a rewrite may see some rewritten and some
612/// not, and each always reads. A crash midway leaves some logs and files
613/// rewritten; running the same map again finishes it. A conversation
614/// read before the rewrite no longer continues a changed log: [`save`] of
615/// it gives [`SessionError::Changed`], and the caller reads it again.
616///
617/// Not for a session a running [`Writer`](crate::Writer) logs to: no
618/// process may rewrite a running session; rig-memory cannot tell, so a
619/// program rewriting a rig session first checks that no rig runs it.
620/// Files kept outside the store are the program's to map with the same
621/// `with` ([`rewrite_value`], [`rewrite_file`]).
622///
623/// # Errors
624///
625/// As [`Session::read`](crate::Session::read) (a session of another format, or broken, is
626/// refused, nothing written), or when a rewritten log would not read
627/// (nothing written), or a write fails.
628///
629/// [`save`]: crate::save
630pub fn rewrite(
631    store: &dyn Store,
632    by: Author,
633    with: &dyn Rewrite,
634) -> Result<Rewritten, SessionError> {
635    let mut logs: Vec<(String, LogLines, Folded)> = Vec::new();
636    for (name, lines) in read_logs(store)? {
637        if let Some(folded) = fold_lines(&name, &lines, Fold::default(), &|_| {
638            Err(io::ErrorKind::NotFound.into())
639        })? {
640            logs.push((name, lines, folded));
641        }
642    }
643    logs.sort_by(|a, b| a.0.cmp(&b.0));
644    let t = now_ms();
645    let blobs = MemoryStore::default();
646    let blob = |name: &str| read_blob(&blobs, name).or_else(|_| read_blob(store, name));
647    let mut rewritten = Rewritten::default();
648    let mut replaced: Vec<(u64, Vec<u8>)> = Vec::new();
649    for (name, lines, _) in &logs {
650        let Some(mapped) = map_log(name, lines, with, &blobs)? else {
651            continue;
652        };
653        if mapped.records == 0 {
654            continue;
655        }
656        let (stamp, mut header) = mapped.header;
657        header.lineage = Lineage::fresh();
658        header.rewrites.push(LogRewrite { t, by: by.clone() });
659        let mut bytes = Vec::with_capacity(mapped.body.len() + 256);
660        write_meta(&mut bytes, stamp, &Meta::Header(header)).map_err(io::Error::from)?;
661        bytes.extend_from_slice(&mapped.body);
662        let new = LogLines::parse(bytes);
663        if fold_lines(name, &new, Fold::WHOLE, &blob)?.is_none() {
664            return Err(SessionError::Record {
665                log: name.clone(),
666                line: 1,
667                why: "the rewritten log is empty".to_owned(),
668            });
669        }
670        rewritten.logs.push(RewrittenLog {
671            log: name.clone(),
672            records: mapped.records,
673        });
674        replaced.push((lines.len(), new.prefix(u64::MAX).to_vec()));
675    }
676
677    // Nothing was written so far.
678    for (name, _, old) in &logs {
679        if let Some((kept, cut)) = old.torn {
680            set_aside_tail(store, name, kept, cut)?;
681        }
682    }
683    for name in blob_names(&blobs)? {
684        put_blob(store, &name, &read_blob(&blobs, &name)?)?;
685    }
686    for (log, (len, bytes)) in rewritten.logs.iter().zip(&replaced) {
687        refused(&log.log, store.replace_log(&log.log, *len, bytes))?;
688    }
689    for name in store.files()? {
690        if !name.starts_with(BLOBS) && rewrite_file(store, &name, with)? {
691            rewritten.files.push(name);
692        }
693    }
694    rewritten.files.sort();
695    let mut named = BTreeSet::new();
696    for (name, lines, _) in &logs {
697        let log = rewritten.logs.iter().position(|log| &log.log == name);
698        let bytes = match log.and_then(|at| replaced.get(at)) {
699            Some((_, bytes)) => bytes.as_slice(),
700            None => lines.prefix(u64::MAX),
701        };
702        named_blobs(bytes, &mut named);
703    }
704    let mut stored = blob_names(store)?;
705    stored.sort();
706    for name in stored {
707        if !named.contains(&name) {
708            store.remove_file(&format!("{BLOBS}{name}"))?;
709            rewritten.blobs_removed.push(name);
710        }
711    }
712    Ok(rewritten)
713}
714
715#[cfg(test)]
716mod tests;