OXYGENHUB

Read a state index's file histories only when used 5552e69c7325

Each file's history in a state index now follows the rest of the index,
with its offset, length and its own BLAKE3 hash in the index's table.
Reading an index reads and checks its front (changes, tree, replaces and
the table); a history is read with a positioned read, checked against
its hash and decoded only when a command builds that file. Changes added
on top of an index are kept beside a history not yet read, so adding one
decodes nothing. A history that does not match its hash is a
DamagedIndex error: the workspace's state indexes are removed and the
command says to run again. Index format 4.

VS Code at 19,978 commits, under load, alternating builds: diff 85 to
63 ms, cat and blame 102 to 80 ms; log and status unchanged; output
the same.

refs #94
Author
Andy McCall @andymccall <[email protected]>
When
· 2026-10-10 03:24 UTC
Signature
✔ signed by Andy McCall
Hash
5552e69c732539b5d251a7dd1c650de7e26e4339f679ec08675af0b6867517d5
Depends on
05059d434aca 091fef80755d 170bb436e1d5 36e51aa806c6 3f1b8fe25ec9 4cce912a0953 57d8dc05c9d4 638fb21420d7 79b5a90ffd43 8fd78eb52ad2 982864068a59 a4fad5b085c7 cc7a00565161 d45dab0484ea e8d38e47e9d7 e8e6e0cac26a f6451fbbae3f f9466021286a f9575918477f ff0af1ee299d
Depended on by
c2e1d09047db cb8b8082fa73
In bookmarks
bare-hashes carried-not-removed carry-identical doctor-trust identical-sides keyless-tags main merge-remote-bookmark only-help-103 paths-for-resolutions-and-amend push-notes show-stat subrepo-pin-state switch-says
In tags
v0.10.0 v0.11.0 v0.4.3 v0.4.4 v0.4.5 v0.4.6 v0.5.0 v0.5.1 v0.6.0 v0.7.0 v0.8.0 v0.9.0
Edits
60 atoms, 0 tree operations, 5 files

Browse the tree at this change

modified CHANGELOG.md +2 −0
…
1 unchanged lines
2 2
3 3
## Unreleased
4 4
5 +
- **A state index read only as far as it is used (#94).** Each file's history in the workspace's state index is stored after the rest with a hash of its own, and is read and checked only when a command builds that file, so a diff or a one-file cat no longer reads and hashes the histories of every file (67 MB of an 84 MB index on VS Code at 20 000 commits). Changes added on top of an index no longer decode the histories they touch. A history found damaged is an error that says so, and the index is removed and rebuilt by the next command. Under load on that repository, `diff` went from 85 ms to 63 ms and `cat`/`blame` from 102 ms to 80 ms.
6 +
5 7
- **Removing a file removes every copy of it (#88).** When two lines of history each put a file back in the same place, it has two placements there and shows as one file. Removing or moving it retired only one, so the other stayed behind, and once its directory went it was an orphan: a conflict in the state. It is the one difference the Git import of VS Code had in its first 31 000 commits. Both now go together, in a commit and in a Git import.
6 8
7 9
- **A quicker log and diff on long histories (#94, first part).** `oxy log` no longer reads the working copy's metadata through the store's cache, walks the history with a fast hash and without copying each change's metadata, and works out the shortest unique id prefixes for the changes it shows rather than for every change. The state index is read without copying the file histories in it, and its change list is checked on every core. On VS Code at 20 000 commits, `log -n 20` went from 74 ms to 40 ms and loading the state index from 43 ms to 28 ms, with the same output.
…
154 unchanged lines
modified crates/oxy-core/src/objects.rs +4 −0
…
44 unchanged lines
45 45
    Cbor(#[from] cbor::CborError),
46 46
    #[error("schema: {0}")]
47 47
    Schema(String),
48 +
    /// A cache this repository derived and can derive again (a state
49 +
    /// index) does not match the hash it was written with.
50 +
    #[error("damaged index: {0}")]
51 +
    DamagedIndex(String),
48 52
}
49 53
50 54
pub(crate) fn schema<T>(msg: impl Into<String>) -> Result<T, ObjectError> {
…
1624 unchanged lines
modified crates/oxy-core/src/repo/mod.rs +98 −57
…
1094 unchanged lines
1095 1095
1096 1096
    /// Build these file graphs in a history-only state.
1097 1097
    pub fn load_graphs_of(&self, state: &mut State, nodes: &[crate::ids::NodeId]) -> Result<()> {
1098 -
        Ok(state.load_graphs(
1099 -
            nodes,
1100 -
            |refs| {
1101 -
                self.read_blobs(refs).map_err(|e| {
1102 -
                    crate::objects::ObjectError::Schema(format!("graph and text blobs: {e}"))
1103 -
                })
1104 -
            },
1105 -
            |h| self.change_bytes(h),
1106 -
        )?)
1098 +
        state
1099 +
            .load_graphs(
1100 +
                nodes,
1101 +
                |refs| {
1102 +
                    self.read_blobs(refs).map_err(|e| {
1103 +
                        crate::objects::ObjectError::Schema(format!("graph and text blobs: {e}"))
1104 +
                    })
1105 +
                },
1106 +
                |h| self.change_bytes(h),
1107 +
            )
1108 +
            .map_err(|e| self.index_damage(e))
1107 1109
    }
1108 1110
1111 +
    /// An error building graphs, and when it is a state index found
1112 +
    /// damaged (a file's history that does not match its hash), the
1113 +
    /// workspace's state indexes removed: they are rebuilt from history by
1114 +
    /// the next command.
1115 +
    fn index_damage(&self, e: crate::objects::ObjectError) -> RepoError {
1116 +
        let crate::objects::ObjectError::DamagedIndex(why) = &e else {
1117 +
            return e.into();
1118 +
        };
1119 +
        for path in self.state_indexes() {
1120 +
            let _ = fs::remove_file(path);
1121 +
        }
1122 +
        RepoError::Msg(format!(
1123 +
            "this workspace's state index was damaged ({why}) and has been removed; run the command again to rebuild it"
1124 +
        ))
1125 +
    }
1126 +
1109 1127
    /// The graphs of `nodes` of a history-only state, built beside it (see
1110 1128
    /// [`State::build_graphs_detached`]).
1111 1129
    pub fn build_graphs_detached(
…
10 unchanged lines
1122 1140
                    })
1123 1141
                },
1124 1142
                |h| self.change_bytes(h),
1125 -
            )?
1143 +
            )
1144 +
            .map_err(|e| self.index_damage(e))?
1126 1145
            .into_iter()
1127 1146
            .collect())
1128 1147
    }
…
34 unchanged lines
1163 1182
1164 1183
    /// Build every graph a history-only state still lacks, making it full.
1165 1184
    pub fn load_all_graphs(&self, state: &mut State) -> Result<()> {
1166 -
        Ok(state.load_all_graphs(
1167 -
            |refs| {
1168 -
                self.read_blobs(refs).map_err(|e| {
1169 -
                    crate::objects::ObjectError::Schema(format!("graph and text blobs: {e}"))
1170 -
                })
1171 -
            },
1172 -
            |h| self.change_bytes(h),
1173 -
        )?)
1185 +
        state
1186 +
            .load_all_graphs(
1187 +
                |refs| {
1188 +
                    self.read_blobs(refs).map_err(|e| {
1189 +
                        crate::objects::ObjectError::Schema(format!("graph and text blobs: {e}"))
1190 +
                    })
1191 +
                },
1192 +
                |h| self.change_bytes(h),
1193 +
            )
1194 +
            .map_err(|e| self.index_damage(e))
1174 1195
    }
1175 1196
1176 1197
    /// A change's encoding, for building graphs of its inline inserts. The
…
263 unchanged lines
1440 1461
            let mut have = st.maximal(deps);
1441 1462
            have.sort();
1442 1463
            if deps.iter().all(|d| st.contains(d)) && have == heads {
1464 +
                // Re-encoding reads every history: one found damaged is
1465 +
                // left to the build that reads it, which reports it.
1443 1466
                #[cfg(test)]
1444 -
                assert_eq!(
1445 -
                    st.write_index(),
1446 -
                    self.load_state_history(deps)?.write_index(),
1447 -
                    "a state index holds the state history gives"
1448 -
                );
1467 +
                if let Some(mine) = st.write_index() {
1468 +
                    assert_eq!(
1469 +
                        Some(mine),
1470 +
                        self.load_state_history(deps)?.write_index(),
1471 +
                        "a state index holds the state history gives"
1472 +
                    );
1473 +
                }
1449 1474
                return Ok(st);
1450 1475
            }
1451 1476
            // An index a few changes behind (a commit, a pull since): add
…
49 unchanged lines
1501 1526
    /// The index of `deps` if there is one, else the newest, which the
1502 1527
    /// caller may bring up to date.
1503 1528
    fn read_state_index(&self, deps: &[ChangeHash]) -> Option<(State, Vec<ChangeHash>)> {
1529 +
        use std::io::Read;
1504 1530
        let _t = crate::timing::phase("load: state index");
1505 1531
        let exact = self.state_index_path(deps);
1506 -
        let bytes = match fs::read(&exact) {
1507 -
            Ok(b) => {
1532 +
        let reading = crate::timing::phase("load:   of which reading the file");
1533 +
        let mut f = match fs::File::open(&exact) {
1534 +
            Ok(f) => {
1508 1535
                // Used now: the newest, so the last to be dropped.
1509 1536
                let _ = fs::File::options()
1510 1537
                    .write(true)
1511 1538
                    .open(&exact)
1512 1539
                    .and_then(|f| f.set_modified(std::time::SystemTime::now()));
1513 -
                b
1540 +
                f
1514 1541
            }
1515 -
            Err(_) => fs::read(self.state_indexes().first()?).ok()?,
1542 +
            Err(_) => fs::File::open(self.state_indexes().first()?).ok()?,
1516 1543
        };
1517 -
        let mut r = crate::flat::Reader::new(&bytes);
1518 -
        let mut heads = Vec::new();
1519 -
        for _ in 0..r.count().ok()? {
1520 -
            heads.push(r.object().ok()?);
1544 +
        let total = f.metadata().ok()?.len();
1545 +
        let mut word = [0u8; 4];
1546 +
        f.read_exact(&mut word).ok()?;
1547 +
        let n = u32::from_le_bytes(word) as usize;
1548 +
        let mut head = vec![0u8; 32 * n + 64 + 8];
1549 +
        f.read_exact(&mut head).ok()?;
1550 +
        let heads: Vec<ChangeHash> = head[..32 * n]
1551 +
            .chunks_exact(32)
1552 +
            .map(|c| ObjectRef(c.try_into().unwrap()))
1553 +
            .collect();
1554 +
        let (changes_sum, rest_sum) = (&head[32 * n..32 * n + 32], &head[32 * n + 32..32 * n + 64]);
1555 +
        let front_len = u64::from_le_bytes(head[32 * n + 64..].try_into().unwrap());
1556 +
        let at = (4 + head.len()) as u64;
1557 +
        if at.checked_add(front_len).is_none_or(|end| end > total) {
1558 +
            return None;
1521 1559
        }
1522 -
        let (changes_sum, rest_sum) = (r.object().ok()?, r.object().ok()?);
1523 -
        let at = 4 + 32 * heads.len() + 64;
1524 -
        let body = bytes.get(at..)?;
1525 -
        let split = 4 + r.count().ok()?;
1526 -
        // Checked whole, so a part decoded later (the tree's maps, a
1527 -
        // file's history) can trust its bytes.
1528 -
        let (changes, rest) = (body.get(..split)?, body.get(split..)?);
1560 +
        // The front only: the files' histories after it are read, each
1561 +
        // checked by its own hash, when a command builds that file.
1562 +
        let mut front = vec![0u8; front_len as usize];
1563 +
        f.read_exact(&mut front).ok()?;
1564 +
        drop(reading);
1565 +
        let checking = crate::timing::phase("load:   of which checking the file");
1566 +
        let split = 4 + u32::from_le_bytes(front.get(..4)?.try_into().unwrap()) as usize;
1567 +
        let (changes, rest) = (front.get(..split)?, front.get(split..)?);
1529 1568
        let sum = |b: &[u8]| *blake3::Hasher::new().update_rayon(b).finalize().as_bytes();
1530 -
        if sum(changes) != changes_sum.0 || sum(rest) != rest_sum.0 {
1569 +
        if sum(changes) != changes_sum || sum(rest) != rest_sum {
1531 1570
            return None;
1532 1571
        }
1533 -
        // The histories point into this buffer rather than copying it.
1534 -
        Some((
1535 -
            State::read_index_in(&std::sync::Arc::new(bytes), at).ok()?,
1536 -
            heads,
1537 -
        ))
1572 +
        drop(checking);
1573 +
        let histories = crate::state::Histories::File(
1574 +
            std::sync::Arc::new(f),
1575 +
            at + front_len,
1576 +
            total - at - front_len,
1577 +
        );
1578 +
        Some((State::read_index_front(&front, histories).ok()?, heads))
1538 1579
    }
1539 1580
1540 1581
    /// The changes of this workspace's state index (the closure of its
…
8 unchanged lines
1549 1590
        let mut word = [0u8; 4];
1550 1591
        f.read_exact(&mut word).ok()?;
1551 1592
        let n = u32::from_le_bytes(word) as usize;
1552 -
        let mut head = vec![0u8; 32 * n + 64 + 4];
1593 +
        // Heads, the two sums, the front's length, then the changes block.
1594 +
        let mut head = vec![0u8; 32 * n + 64 + 8 + 4];
1553 1595
        f.read_exact(&mut head).ok()?;
1554 1596
        let sum = &head[32 * n..32 * n + 32];
1555 -
        let len = u32::from_le_bytes(head[32 * n + 64..].try_into().unwrap()) as usize;
1597 +
        let len = u32::from_le_bytes(head[32 * n + 72..].try_into().unwrap()) as usize;
1556 1598
        let mut changes = vec![0u8; 4 + len];
1557 -
        changes[..4].copy_from_slice(&head[32 * n + 64..]);
1599 +
        changes[..4].copy_from_slice(&head[32 * n + 72..]);
1558 1600
        f.read_exact(&mut changes[4..]).ok()?;
1559 1601
        let checking = crate::timing::phase("load:   of which checking");
1560 1602
        let mut h = blake3::Hasher::new();
…
28 unchanged lines
1589 1631
        for h in &heads {
1590 1632
            w.object(h);
1591 1633
        }
1592 -
        let split = 4 + u32::from_le_bytes(body[..4].try_into().unwrap()) as usize;
1593 -
        w.fixed(
1594 -
            blake3::Hasher::new()
1595 -
                .update_rayon(&body[..split])
1596 -
                .finalize()
1597 -
                .as_bytes(),
1598 -
        );
1599 -
        let mut sum = blake3::Hasher::new();
1600 -
        sum.update_rayon(&body[split..]);
1601 -
        w.fixed(sum.finalize().as_bytes());
1634 +
        // The body is the front's length, the front, then the files'
1635 +
        // histories, which carry their own hashes: the sums cover the
1636 +
        // front's changes block and the rest of the front.
1637 +
        let front_len = u64::from_le_bytes(body[..8].try_into().unwrap()) as usize;
1638 +
        let front = &body[8..8 + front_len];
1639 +
        let split = 4 + u32::from_le_bytes(front[..4].try_into().unwrap()) as usize;
1640 +
        let sum = |b: &[u8]| *blake3::Hasher::new().update_rayon(b).finalize().as_bytes();
1641 +
        w.fixed(&sum(&front[..split]));
1642 +
        w.fixed(&sum(&front[split..]));
1602 1643
        w.fixed(&body);
1603 1644
        let path = self.state_index_path(&heads);
1604 1645
        let _ = fs::create_dir_all(self.oxy.join("index"));
…
743 unchanged lines
modified crates/oxy-core/src/repo/tests.rs +65 −0
…
8049 unchanged lines
8050 8050
    assert!(view.conflicts.is_empty(), "{:?}", view.conflicts);
8051 8051
    assert_eq!(view.entries.keys().collect::<Vec<_>>(), vec!["keep.txt"]);
8052 8052
}
8053 +
8054 +
/// oxygen#94: a state index's file histories are read, and checked by
8055 +
/// their own hashes, only when a file is built. One found damaged is an
8056 +
/// error that says so, the index is removed, and the next read rebuilds it.
8057 +
#[test]
8058 +
fn a_damaged_file_history_in_the_state_index_is_rebuilt() {
8059 +
    hermetic();
8060 +
    let dir = tempfile::tempdir().unwrap();
8061 +
    let repo = Repo::init(dir.path()).unwrap();
8062 +
    for i in 0..6 {
8063 +
        write(
8064 +
            dir.path(),
8065 +
            &format!("f{i}.txt"),
8066 +
            format!("one {i}\n").as_bytes(),
8067 +
        );
8068 +
    }
8069 +
    repo.snapshot("t").unwrap();
8070 +
    repo.commit("first", &[]).unwrap();
8071 +
    for i in 0..6 {
8072 +
        write(
8073 +
            dir.path(),
8074 +
            &format!("f{i}.txt"),
8075 +
            format!("one {i}\ntwo\n").as_bytes(),
8076 +
        );
8077 +
    }
8078 +
    repo.snapshot("t").unwrap();
8079 +
    repo.commit("second", &[]).unwrap();
8080 +
    let want: Vec<Vec<u8>> = (0..6)
8081 +
        .map(|i| format!("one {i}\ntwo\n").into_bytes())
8082 +
        .collect();
8083 +
    // The index of the working copy's base, holding every file's history
8084 +
    // (a few changes behind, an index is replayed onto, not rewritten).
8085 +
    let (_, wc) = repo.working_copy().unwrap();
8086 +
    let base = repo.load_state_history(&wc.deps).unwrap();
8087 +
    repo.write_state_index(&wc.deps, &base);
8088 +
    for (i, w) in want.iter().enumerate() {
8089 +
        assert_eq!(&repo.file_at(&format!("f{i}.txt"), None).unwrap(), w);
8090 +
    }
8091 +
    let index = repo.state_indexes();
8092 +
    assert!(!index.is_empty(), "a state index was written");
8093 +
    // The histories are last in the file: damage the last of them.
8094 +
    let mut bytes = fs::read(&index[0]).unwrap();
8095 +
    let last = bytes.len() - 1;
8096 +
    bytes[last] ^= 0x40;
8097 +
    fs::write(&index[0], &bytes).unwrap();
8098 +
8099 +
    let reads: Vec<_> = (0..6)
8100 +
        .map(|i| repo.file_at(&format!("f{i}.txt"), None))
8101 +
        .collect();
8102 +
    let damaged: Vec<String> = reads
8103 +
        .iter()
8104 +
        .filter_map(|r| r.as_ref().err().map(|e| e.to_string()))
8105 +
        .collect();
8106 +
    assert_eq!(damaged.len(), 1, "{damaged:?}");
8107 +
    assert!(
8108 +
        damaged[0].contains("state index was damaged")
8109 +
            && damaged[0].contains("run the command again"),
8110 +
        "{}",
8111 +
        damaged[0]
8112 +
    );
8113 +
    // Every file reads right again, from an index rebuilt from history.
8114 +
    for (i, w) in want.iter().enumerate() {
8115 +
        assert_eq!(&repo.file_at(&format!("f{i}.txt"), None).unwrap(), w);
8116 +
    }
8117 +
}
modified crates/oxy-core/src/state.rs +310 −147
…
119 unchanged lines
120 120
/// order they were applied (spec §11). Building it later gives the same
121 121
/// graph, since graph construction does not depend on order (§8).
122 122
///
123 -
/// Read from a state index, it stays encoded until first used: a command
124 -
/// that builds a few files decodes only their histories.
123 +
/// Read from a state index, it stays in the index, unread, until first
124 +
/// used: a command that builds a few files reads and checks only their
125 +
/// histories (oxygen#94).
125 126
#[derive(Clone, Default)]
126 127
struct Deferred {
127 -
    body: std::sync::OnceLock<DeferredBody>,
128 -
    /// The body as the index encodes it, decoded into `body` on first use.
129 -
    raw: Option<(Arc<Vec<u8>>, std::ops::Range<usize>)>,
128 +
    /// The history: `raw` read and decoded, with `more` after it. An
129 +
    /// error when `raw` does not match its hash.
130 +
    body: std::sync::OnceLock<Result<DeferredBody, String>>,
131 +
    /// The history as an index holds it, not read yet.
132 +
    raw: Option<RawHistory>,
133 +
    /// Changes added since the index was read, kept beside `raw`, so that
134 +
    /// adding one neither reads nor decodes the history.
135 +
    more: Vec<(ChangeHash, Vec<Edit>)>,
136 +
    more_texts: Vec<TextRef>,
130 137
    /// Its graph key, once worked out (or read from an index); reset by
131 138
    /// any edit added.
132 139
    key: std::sync::OnceLock<[u8; 32]>,
133 140
}
134 141
142 +
/// Where a state index's file histories are: the index file itself, read
143 +
/// on first use, or bytes in memory.
144 +
#[derive(Clone)]
145 +
pub enum Histories {
146 +
    Bytes(Arc<Vec<u8>>),
147 +
    /// The file, where its histories start, and how many bytes they take.
148 +
    File(Arc<std::fs::File>, u64, u64),
149 +
}
150 +
151 +
impl Histories {
152 +
    fn len(&self) -> u64 {
153 +
        match self {
154 +
            Histories::Bytes(b) => b.len() as u64,
155 +
            Histories::File(_, _, len) => *len,
156 +
        }
157 +
    }
158 +
159 +
    fn read(&self, at: u64, len: usize) -> std::io::Result<Vec<u8>> {
160 +
        match self {
161 +
            Histories::Bytes(b) => Ok(b[at as usize..at as usize + len].to_vec()),
162 +
            Histories::File(f, start, _) => {
163 +
                let mut buf = vec![0u8; len];
164 +
                read_exact_at(f, &mut buf, start + at)?;
165 +
                Ok(buf)
166 +
            }
167 +
        }
168 +
    }
169 +
}
170 +
171 +
#[cfg(unix)]
172 +
fn read_exact_at(f: &std::fs::File, buf: &mut [u8], at: u64) -> std::io::Result<()> {
173 +
    std::os::unix::fs::FileExt::read_exact_at(f, buf, at)
174 +
}
175 +
176 +
#[cfg(windows)]
177 +
fn read_exact_at(f: &std::fs::File, mut buf: &mut [u8], mut at: u64) -> std::io::Result<()> {
178 +
    while !buf.is_empty() {
179 +
        match std::os::windows::fs::FileExt::seek_read(f, buf, at)? {
180 +
            0 => return Err(std::io::ErrorKind::UnexpectedEof.into()),
181 +
            n => {
182 +
                buf = &mut buf[n..];
183 +
                at += n as u64;
184 +
            }
185 +
        }
186 +
    }
187 +
    Ok(())
188 +
}
189 +
190 +
/// One file's history in a state index: where it is and its hash.
191 +
#[derive(Clone)]
192 +
struct RawHistory {
193 +
    from: Histories,
194 +
    at: u64,
195 +
    len: usize,
196 +
    sum: [u8; 32],
197 +
}
198 +
199 +
impl RawHistory {
200 +
    /// The history's bytes, checked against the hash the index holds.
201 +
    fn bytes(&self) -> Result<Vec<u8>, ObjectError> {
202 +
        let damaged = |why: String| ObjectError::DamagedIndex(why);
203 +
        let b = self
204 +
            .from
205 +
            .read(self.at, self.len)
206 +
            .map_err(|e| damaged(format!("a file's history cannot be read: {e}")))?;
207 +
        if *blake3::hash(&b).as_bytes() != self.sum {
208 +
            return Err(damaged("a file's history does not match its hash".into()));
209 +
        }
210 +
        Ok(b)
211 +
    }
212 +
}
213 +
135 214
/// The closures kept, and how many hashes they hold in all.
136 215
#[derive(Default)]
137 216
struct ClosureMemo {
…
17 unchanged lines
155 234
impl Deferred {
156 235
    fn from_body(body: DeferredBody) -> Deferred {
157 236
        Deferred {
158 -
            body: std::sync::OnceLock::from(body),
237 +
            body: std::sync::OnceLock::from(Ok(body)),
159 238
            ..Deferred::default()
160 239
        }
240 +
    }
241 +
242 +
    /// The history, read and checked on first use.
243 +
    fn body(&self) -> Result<&DeferredBody, ObjectError> {
244 +
        self.body
245 +
            .get_or_init(|| {
246 +
                let mut b = match &self.raw {
247 +
                    // Matching its hash, it is what `write_index` wrote: it
248 +
                    // decodes.
249 +
                    Some(raw) => raw
250 +
                        .bytes()
251 +
                        .and_then(|b| State::read_deferred_body(&b))
252 +
                        .map_err(|e| e.to_string())?,
253 +
                    None => DeferredBody::default(),
254 +
                };
255 +
                b.edits.extend(self.more.iter().cloned());
256 +
                b.texts.extend(self.more_texts.iter().cloned());
257 +
                Ok(b)
258 +
            })
259 +
            .as_ref()
260 +
            .map_err(|e| ObjectError::DamagedIndex(e.clone()))
161 261
    }
162 262
163 -
    fn body(&self) -> &DeferredBody {
164 -
        self.body.get_or_init(|| match &self.raw {
165 -
            // The index was checked against its hash when read, and was
166 -
            // written by `write_index`: it decodes.
167 -
            Some((bytes, range)) => State::read_deferred_body(&bytes[range.clone()])
168 -
                .expect("a state index checked by its hash decodes"),
169 -
            None => DeferredBody::default(),
170 -
        })
263 +
    /// Add a change's edits to the history: beside the index's encoding
264 +
    /// while it is unread, so that nothing is read or decoded to add them.
265 +
    fn add(&mut self, h: ChangeHash, edits: Vec<Edit>, texts: Vec<TextRef>) {
266 +
        if self.raw.is_some() && !matches!(self.body.get(), Some(Ok(_))) {
267 +
            self.more.push((h, edits));
268 +
            self.more_texts.extend(texts);
269 +
            // A body found damaged stays so: what was added cannot mend it.
270 +
            return;
271 +
        }
272 +
        // No encoding to read, or decoded already: this cannot fail.
273 +
        let b = self.body_mut().expect("a history in memory");
274 +
        b.edits.push((h, edits));
275 +
        b.texts.extend(texts);
171 276
    }
172 277
173 -
    fn body_mut(&mut self) -> &mut DeferredBody {
174 -
        self.body();
278 +
    fn body_mut(&mut self) -> Result<&mut DeferredBody, ObjectError> {
279 +
        self.body()?;
175 280
        self.raw = None;
176 -
        self.body.get_mut().expect("decoded above")
281 +
        self.more.clear();
282 +
        self.more_texts.clear();
283 +
        Ok(self
284 +
            .body
285 +
            .get_mut()
286 +
            .and_then(|b| b.as_mut().ok())
287 +
            .expect("decoded above"))
177 288
    }
178 289
179 -
    fn into_body(mut self) -> DeferredBody {
180 -
        self.body();
181 -
        self.body.take().expect("decoded above")
290 +
    fn into_body(mut self) -> Result<DeferredBody, ObjectError> {
291 +
        self.body()?;
292 +
        Ok(self
293 +
            .body
294 +
            .take()
295 +
            .and_then(|b| b.ok())
296 +
            .expect("decoded above"))
182 297
    }
183 298
}
184 299
…
9 unchanged lines
194 309
/// command reads it rather than replaying history. Derived only: written
195 310
/// from a state loaded the long way, and read back as the same state.
196 311
impl State {
197 -
    const INDEX_MAGIC: &'static [u8] = b"oxy state index 3\n";
312 +
    const INDEX_MAGIC: &'static [u8] = b"oxy state index 4\n";
198 313
199 314
    /// The index of this state, or `None` when it has anything an index
200 315
    /// does not hold: a built graph, or text in memory (a change added
…
4 unchanged lines
205 320
        if !self.files.is_empty() {
206 321
            return None;
207 322
        }
208 -
        let atom = |w: &mut Writer, a: &AtomRef| match a {
209 -
            AtomRef::Start => w.u8(0),
210 -
            AtomRef::End => w.u8(1),
211 -
            AtomRef::Atom { change, index } => {
212 -
                w.u8(2);
213 -
                w.object(change);
214 -
                w.u32(*index);
215 -
            }
216 -
        };
217 323
        // The changes first, as a block a reader can take on its own (see
218 324
        // [`State::read_index_changes`]).
219 325
        let mut w = Writer::default();
…
27 unchanged lines
247 353
                w.opt_object(new);
248 354
            }
249 355
        }
250 -
        let outer = w;
251 -
        let mut w = Writer::default();
356 +
        let mut front = w;
357 +
        // The table: per file its key and where its history is, with the
358 +
        // history's hash, which is checked when it is read (oxygen#94).
359 +
        let mut table = Writer::default();
360 +
        let mut histories: Vec<u8> = Vec::new();
252 361
        let mut nodes: Vec<(&NodeId, &Arc<Deferred>)> = deferred.iter().collect();
253 362
        nodes.sort_by_key(|(n, _)| **n);
254 -
        w.count(nodes.len());
363 +
        table.count(nodes.len());
255 364
        for (n, d) in nodes {
256 -
            w.fixed(&n.0);
257 -
            let mut h = blake3::Hasher::new();
258 -
            h.update(b"oxy graph key 1\0");
259 -
            h.update(&n.0);
260 -
            w.fixed(d.key.get_or_init(|| d.graph_key(h)));
261 -
            // Unchanged since read (a change clears `raw`): copied as it is.
262 -
            if let Some((bytes, range)) = &d.raw {
263 -
                w.bytes(&bytes[range.clone()]);
264 -
                continue;
265 -
            }
266 -
            let d = d.body();
267 -
            let outer_w = std::mem::take(&mut w);
268 -
            match &d.base {
269 -
                None => w.u8(0),
270 -
                Some(DeferredBase::Blob(r)) => {
271 -
                    w.u8(1);
272 -
                    w.object(r);
365 +
            let key = match d.key.get() {
366 +
                Some(k) => *k,
367 +
                None => {
368 +
                    let mut h = blake3::Hasher::new();
369 +
                    h.update(b"oxy graph key 1\0");
370 +
                    h.update(&n.0);
371 +
                    let k = d.graph_key(h).ok()?;
372 +
                    let _ = d.key.set(k);
373 +
                    k
273 374
                }
274 -
                Some(DeferredBase::Inline(v)) => {
275 -
                    w.u8(2);
276 -
                    w.bytes(&cbor::encode(v));
277 -
                }
375 +
            };
376 +
            // Unchanged since read: its bytes as they are, checked.
377 +
            let bytes = match &d.raw {
378 +
                Some(raw) if d.more.is_empty() => raw.bytes().ok()?,
379 +
                _ => Self::write_deferred_body(d.body().ok()?)?,
380 +
            };
381 +
            table.fixed(&n.0);
382 +
            table.fixed(&key);
383 +
            table.u64(histories.len() as u64);
384 +
            table.u32(u32::try_from(bytes.len()).ok()?);
385 +
            table.fixed(blake3::hash(&bytes).as_bytes());
386 +
            histories.extend_from_slice(&bytes);
387 +
        }
388 +
        front.bytes(&table.0);
389 +
        let mut out = Writer::default();
390 +
        out.u64(front.0.len() as u64);
391 +
        out.fixed(&front.0);
392 +
        out.fixed(&histories);
393 +
        Some(out.0)
394 +
    }
395 +
396 +
    /// One file's history as an index encodes it; `None` when it holds
397 +
    /// text in memory, which an index does not.
398 +
    fn write_deferred_body(d: &DeferredBody) -> Option<Vec<u8>> {
399 +
        use crate::flat::Writer;
400 +
        let atom = |w: &mut Writer, a: &AtomRef| match a {
401 +
            AtomRef::Start => w.u8(0),
402 +
            AtomRef::End => w.u8(1),
403 +
            AtomRef::Atom { change, index } => {
404 +
                w.u8(2);
405 +
                w.object(change);
406 +
                w.u32(*index);
278 407
            }
279 -
            // Each change's edits with the texts of its inserts, sorted by
280 -
            // change: a graph does not depend on the order changes applied
281 -
            // in, and the index is then the same however history was read.
282 -
            let mut texts = d.texts.iter();
283 -
            let mut per_change: Vec<(&ChangeHash, &Vec<Edit>, Vec<&TextRef>)> = Vec::new();
284 -
            for (c, edits) in &d.edits {
285 -
                let n = edits
286 -
                    .iter()
287 -
                    .filter(|e| matches!(e, Edit::Insert { lines, .. } if lines.is_empty()))
288 -
                    .count();
289 -
                per_change.push((c, edits, texts.by_ref().take(n).collect()));
408 +
        };
409 +
        let mut w = Writer::default();
410 +
        match &d.base {
411 +
            None => w.u8(0),
412 +
            Some(DeferredBase::Blob(r)) => {
413 +
                w.u8(1);
414 +
                w.object(r);
290 415
            }
291 -
            per_change.sort_by_key(|(c, _, _)| **c);
292 -
            w.count(per_change.len());
293 -
            for (c, edits, _) in &per_change {
294 -
                w.object(c);
295 -
                w.count(edits.len());
296 -
                for e in edits.iter() {
297 -
                    match e {
298 -
                        Edit::Insert {
299 -
                            node,
300 -
                            after,
301 -
                            before,
302 -
                            lines,
303 -
                        } => {
304 -
                            if !lines.is_empty() {
305 -
                                return None;
306 -
                            }
307 -
                            w.u8(0);
308 -
                            w.fixed(&node.0);
309 -
                            atom(&mut w, after);
310 -
                            atom(&mut w, before);
311 -
                        }
312 -
                        Edit::Delete { node, atoms } => {
313 -
                            w.u8(1);
314 -
                            w.fixed(&node.0);
315 -
                            w.count(atoms.len());
316 -
                            for r in atoms {
317 -
                                w.object(&r.change);
318 -
                                w.u32(r.from);
319 -
                                w.u32(r.to);
320 -
                            }
321 -
                        }
322 -
                        Edit::Replace { .. } => return None,
323 -
                    }
324 -
                }
416 +
            Some(DeferredBase::Inline(v)) => {
417 +
                w.u8(2);
418 +
                w.bytes(&cbor::encode(v));
325 419
            }
326 -
            let texts: Vec<&TextRef> = per_change
420 +
        }
421 +
        // Each change's edits with the texts of its inserts, sorted by
422 +
        // change: a graph does not depend on the order changes applied
423 +
        // in, and the index is then the same however history was read.
424 +
        let mut texts = d.texts.iter();
425 +
        let mut per_change: Vec<(&ChangeHash, &Vec<Edit>, Vec<&TextRef>)> = Vec::new();
426 +
        for (c, edits) in &d.edits {
427 +
            let n = edits
327 428
                .iter()
328 -
                .flat_map(|(_, _, t)| t.iter().copied())
329 -
                .collect();
330 -
            w.count(texts.len());
331 -
            for t in texts {
332 -
                match t {
333 -
                    TextRef::Blob(r, count, form) => {
429 +
                .filter(|e| matches!(e, Edit::Insert { lines, .. } if lines.is_empty()))
430 +
                .count();
431 +
            per_change.push((c, edits, texts.by_ref().take(n).collect()));
432 +
        }
433 +
        per_change.sort_by_key(|(c, _, _)| **c);
434 +
        w.count(per_change.len());
435 +
        for (c, edits, _) in &per_change {
436 +
            w.object(c);
437 +
            w.count(edits.len());
438 +
            for e in edits.iter() {
439 +
                match e {
440 +
                    Edit::Insert {
441 +
                        node,
442 +
                        after,
443 +
                        before,
444 +
                        lines,
445 +
                    } => {
446 +
                        if !lines.is_empty() {
447 +
                            return None;
448 +
                        }
334 449
                        w.u8(0);
335 -
                        w.object(r);
336 -
                        w.u32(*count);
337 -
                        w.u8(match form {
338 -
                            crate::objects::TextForm::Cbor => 0,
339 -
                            crate::objects::TextForm::Raw => 1,
340 -
                            crate::objects::TextForm::CborFallback => 2,
341 -
                        });
450 +
                        w.fixed(&node.0);
451 +
                        atom(&mut w, after);
452 +
                        atom(&mut w, before);
342 453
                    }
343 -
                    TextRef::Inline { change, start, end } => {
454 +
                    Edit::Delete { node, atoms } => {
344 455
                        w.u8(1);
345 -
                        w.object(change);
346 -
                        w.u32(*start);
347 -
                        w.u32(*end);
456 +
                        w.fixed(&node.0);
457 +
                        w.count(atoms.len());
458 +
                        for r in atoms {
459 +
                            w.object(&r.change);
460 +
                            w.u32(r.from);
461 +
                            w.u32(r.to);
462 +
                        }
348 463
                    }
464 +
                    Edit::Replace { .. } => return None,
349 465
                }
350 466
            }
351 -
            let body = std::mem::replace(&mut w, outer_w);
352 -
            w.bytes(&body.0);
353 467
        }
354 -
        let mut outer = outer;
355 -
        outer.bytes(&w.0);
356 -
        Some(outer.0)
468 +
        let texts: Vec<&TextRef> = per_change
469 +
            .iter()
470 +
            .flat_map(|(_, _, t)| t.iter().copied())
471 +
            .collect();
472 +
        w.count(texts.len());
473 +
        for t in texts {
474 +
            match t {
475 +
                TextRef::Blob(r, count, form) => {
476 +
                    w.u8(0);
477 +
                    w.object(r);
478 +
                    w.u32(*count);
479 +
                    w.u8(match form {
480 +
                        crate::objects::TextForm::Cbor => 0,
481 +
                        crate::objects::TextForm::Raw => 1,
482 +
                        crate::objects::TextForm::CborFallback => 2,
483 +
                    });
484 +
                }
485 +
                TextRef::Inline { change, start, end } => {
486 +
                    w.u8(1);
487 +
                    w.object(change);
488 +
                    w.u32(*start);
489 +
                    w.u32(*end);
490 +
                }
491 +
            }
492 +
        }
493 +
        Some(w.0)
357 494
    }
358 495
359 496
    /// The state an index holds (see [`State::write_index`]).
360 497
    pub fn read_index(bytes: &[u8]) -> Result<State, ObjectError> {
361 -
        Self::read_index_in(&Arc::new(bytes.to_vec()), 0)
498 +
        let bad = || ObjectError::Schema("state index: truncated".into());
499 +
        let front_len =
500 +
            usize::try_from(crate::flat::Reader::new(bytes).u64()?).map_err(|_| bad())?;
501 +
        let front = bytes.get(8..8 + front_len).ok_or_else(bad)?;
502 +
        let histories = Histories::Bytes(Arc::new(bytes[8 + front_len..].to_vec()));
503 +
        Self::read_index_front(front, histories)
362 504
    }
363 505
364 -
    /// As [`State::read_index`], for an index at `at` in `file`, which the
365 -
    /// files' histories keep pointing into rather than copying: most of an
366 -
    /// index is histories a command never decodes.
367 -
    pub fn read_index_in(file: &Arc<Vec<u8>>, at: usize) -> Result<State, ObjectError> {
506 +
    /// An index's front, everything but the files' histories, with where
507 +
    /// those are: each is read, and checked against the hash the front
508 +
    /// holds, only when its graph is built (oxygen#94).
509 +
    pub fn read_index_front(bytes: &[u8], histories: Histories) -> Result<State, ObjectError> {
368 510
        use crate::flat::Reader;
369 511
        let bad = |what: &str| ObjectError::Schema(format!("state index: {what}"));
370 -
        let bytes = file.get(at..).ok_or_else(|| bad("truncated"))?;
371 512
        let mut r = Reader::new(bytes);
372 513
        let mut st = State::new_history_only();
373 514
        {
…
13 unchanged lines
387 528
        if !r.at_end() {
388 529
            return Err(bad("trailing bytes"));
389 530
        }
390 -
        let start = deferred_bytes.as_ptr() as usize - file.as_ptr() as usize;
391 531
        let (tree, deferred) = rayon::join(
392 532
            || TreeGraph::read_flat(&mut Reader::new(tree_bytes)),
393 -
            || Self::read_deferred(file, start..start + deferred_bytes.len()),
533 +
            || Self::read_deferred(deferred_bytes, &histories),
394 534
        );
395 535
        st.tree = tree?;
396 536
        st.deferred = Some(deferred?);
…
25 unchanged lines
422 562
    /// The deferred block: each node with its graph key and its body,
423 563
    /// left encoded (see [`Deferred`]).
424 564
    fn read_deferred(
425 -
        file: &Arc<Vec<u8>>,
426 -
        within: std::ops::Range<usize>,
565 +
        table: &[u8],
566 +
        histories: &Histories,
427 567
    ) -> Result<crate::fasthash::FastMap<NodeId, Arc<Deferred>>, ObjectError> {
428 568
        use crate::flat::Reader;
429 569
        let _t = crate::timing::phase("index: deferred");
430 -
        let mut r = Reader::new(&file[within]);
570 +
        let mut r = Reader::new(table);
431 571
        let mut deferred = crate::fasthash::FastMap::default();
432 572
        let n = r.count()?;
433 573
        deferred.reserve(n);
434 574
        for _ in 0..n {
435 575
            let id = NodeId(r.fixed::<16>()?);
436 576
            let key = r.fixed::<32>()?;
437 -
            let body = r.bytes()?;
438 -
            let start = body.as_ptr() as usize - file.as_ptr() as usize;
577 +
            // The length is of bytes in the histories, not in this table:
578 +
            // checked against the histories' size below.
579 +
            let (at, len, sum) = (r.u64()?, r.u32()? as usize, r.fixed::<32>()?);
580 +
            if at
581 +
                .checked_add(len as u64)
582 +
                .is_none_or(|end| end > histories.len())
583 +
            {
584 +
                return Err(ObjectError::Schema(
585 +
                    "state index: a file's history lies outside it".into(),
586 +
                ));
587 +
            }
439 588
            let d = Deferred {
440 -
                raw: Some((file.clone(), start..start + body.len())),
589 +
                raw: Some(RawHistory {
590 +
                    from: histories.clone(),
591 +
                    at,
592 +
                    len,
593 +
                    sum,
594 +
                }),
441 595
                key: std::sync::OnceLock::from(key),
442 596
                ..Deferred::default()
443 597
            };
…
233 unchanged lines
677 831
    load: impl FnOnce(&[ObjectRef]) -> Result<Vec<Vec<u8>>, ObjectError>,
678 832
    read_change: impl Fn(&ChangeHash) -> Result<Arc<Vec<u8>>, ObjectError>,
679 833
) -> Result<Vec<(NodeId, FileGraph)>, ObjectError> {
680 -
    let mut taken: Vec<(NodeId, DeferredBody)> =
681 -
        taken.into_iter().map(|(n, d)| (n, d.into_body())).collect();
834 +
    let mut taken: Vec<(NodeId, DeferredBody)> = taken
835 +
        .into_iter()
836 +
        .map(|(n, d)| Ok((n, d.into_body()?)))
837 +
        .collect::<Result<_, ObjectError>>()?;
682 838
    let mut refs: Vec<ObjectRef> = taken
683 839
        .iter()
684 840
        .filter_map(|(_, d)| match &d.base {
…
279 unchanged lines
964 1120
        if self.files.contains_key(node) {
965 1121
            return None;
966 1122
        }
967 -
        let d = self.deferred.as_ref()?.get(node)?.body();
1123 +
        // A history that cannot be read is no single text: building its
1124 +
        // graph reports why.
1125 +
        let d = self.deferred.as_ref()?.get(node)?.body().ok()?;
968 1126
        let [(_, edits)] = d.edits.as_slice() else {
969 1127
            return None;
970 1128
        };
…
33 unchanged lines
1004 1162
            h.update(&[0]);
1005 1163
            return Some(*h.finalize().as_bytes());
1006 1164
        };
1007 -
        Some(*d.key.get_or_init(|| d.graph_key(h)))
1165 +
        // A history that cannot be read has no key: the file is read.
1166 +
        if let Some(k) = d.key.get() {
1167 +
            return Some(*k);
1168 +
        }
1169 +
        let k = d.graph_key(h).ok()?;
1170 +
        Some(*d.key.get_or_init(|| k))
1008 1171
    }
1009 1172
1010 1173
    pub fn len(&self) -> usize {
…
55 unchanged lines
1066 1229
        let mut total = 0;
1067 1230
        let mut most = 0;
1068 1231
        for v in d.values() {
1069 -
            let n: usize = v.body().edits.iter().map(|(_, e)| e.len()).sum();
1232 +
            let n: usize = v
1233 +
                .body()
1234 +
                .map_or(0, |b| b.edits.iter().map(|(_, e)| e.len()).sum());
1070 1235
            total += n;
1071 1236
            most = most.max(n);
1072 1237
        }
…
3 unchanged lines
1076 1241
1077 1242
impl Deferred {
1078 1243
    /// See [`State::graph_key`]: `h` has taken the node already.
1079 -
    fn graph_key(&self, mut h: blake3::Hasher) -> [u8; 32] {
1080 -
        let d = self.body();
1244 +
    fn graph_key(&self, mut h: blake3::Hasher) -> Result<[u8; 32], ObjectError> {
1245 +
        let d = self.body()?;
1081 1246
        match &d.base {
1082 1247
            None => h.update(&[0]),
1083 1248
            Some(DeferredBase::Blob(r)) => h.update(&[1]).update(&r.0),
…
6 unchanged lines
1090 1255
        for c in changes {
1091 1256
            h.update(&c.0);
1092 1257
        }
1093 -
        *h.finalize().as_bytes()
1258 +
        Ok(*h.finalize().as_bytes())
1094 1259
    }
1095 1260
}
1096 1261
…
458 unchanged lines
1555 1720
                    }
1556 1721
                    None => {
1557 1722
                        let d = Arc::make_mut(deferred.entry(node).or_default());
1558 -
                        let b = d.body_mut();
1559 -
                        b.edits.push((h, edits));
1560 -
                        b.texts.extend(node_texts);
1723 +
                        d.add(h, edits, node_texts);
1561 1724
                        d.key = Default::default();
1562 1725
                    }
1563 1726
                }
…
1202 unchanged lines