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 |