Skip to main content

objects/
legacy.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Migration-only decoders for removed durable formats.
3
4use serde::{Deserialize, Serialize};
5use sley::{ObjectFormat as GitObjectFormat, ObjectId as GitObjectId};
6
7use crate::{
8    error::{HeddleError, Result},
9    object::{ContentHash, Tree, TreeEntry},
10};
11
12const LEGACY_GITLINK_BLOB_PREFIX: &str = "heddle-submodule:";
13
14pub fn decode_gitlink_blob_marker(content: &[u8]) -> Option<GitObjectId> {
15    let text = std::str::from_utf8(content).ok()?.trim();
16    let oid = text.strip_prefix(LEGACY_GITLINK_BLOB_PREFIX)?.trim();
17    GitObjectId::from_hex(GitObjectFormat::Sha1, oid).ok()
18}
19
20/// Decode the removed V1 tree schema.
21///
22/// This intentionally lives outside `Tree`'s `Deserialize` impl: normal
23/// runtime readers accept only the current versioned tree envelope, while
24/// migrations may call this one-shot decoder and immediately rewrite a V2 tree
25/// body at the same semantic tree hash.
26pub fn decode_legacy_tree_v1(data: &[u8]) -> Result<Tree> {
27    let legacy: LegacyTreeV1 = rmp_serde::from_slice(data)?;
28    legacy.into_tree()
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
32struct LegacyTreeV1 {
33    entries: Vec<LegacyTreeEntryV1>,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
37struct LegacyTreeEntryV1 {
38    name: String,
39    mode: LegacyFileMode,
40    entry_type: LegacyEntryType,
41    hash: ContentHash,
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
45enum LegacyFileMode {
46    Normal,
47    Executable,
48    Symlink,
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
52enum LegacyEntryType {
53    Blob,
54    Tree,
55    Symlink,
56}
57
58impl LegacyTreeV1 {
59    fn into_tree(self) -> Result<Tree> {
60        let mut entries = Vec::with_capacity(self.entries.len());
61        for entry in self.entries {
62            entries.push(entry.into_tree_entry()?);
63        }
64        let tree = Tree::from_entries(entries);
65        tree.validate()?;
66        Ok(tree)
67    }
68}
69
70impl LegacyTreeEntryV1 {
71    fn into_tree_entry(self) -> Result<TreeEntry> {
72        match (self.entry_type, self.mode) {
73            (LegacyEntryType::Blob, LegacyFileMode::Normal) => {
74                Ok(TreeEntry::file(self.name, self.hash, false)?)
75            }
76            (LegacyEntryType::Blob, LegacyFileMode::Executable) => {
77                Ok(TreeEntry::file(self.name, self.hash, true)?)
78            }
79            (LegacyEntryType::Tree, LegacyFileMode::Normal) => {
80                Ok(TreeEntry::directory(self.name, self.hash)?)
81            }
82            (LegacyEntryType::Symlink, LegacyFileMode::Symlink) => {
83                Ok(TreeEntry::symlink(self.name, self.hash)?)
84            }
85            (entry_type, mode) => Err(HeddleError::InvalidObject(format!(
86                "invalid legacy tree entry '{}': {entry_type:?} cannot use mode {mode:?}",
87                self.name
88            ))),
89        }
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    #[test]
98    fn decodes_legacy_gitlink_blob_marker() {
99        let oid = decode_gitlink_blob_marker(
100            b"heddle-submodule: 0808080808080808080808080808080808080808",
101        )
102        .expect("legacy marker decodes");
103
104        assert_eq!(oid.to_string(), "0808080808080808080808080808080808080808");
105    }
106
107    #[test]
108    fn ignores_ordinary_blob_content() {
109        assert!(decode_gitlink_blob_marker(b"not a gitlink").is_none());
110    }
111
112    #[test]
113    fn decodes_legacy_tree_v1_without_sniffing_marker_blobs() {
114        let blob_hash =
115            ContentHash::compute(b"heddle-submodule: 0808080808080808080808080808080808080808");
116        let raw = rmp_serde::to_vec(&LegacyTreeV1 {
117            entries: vec![LegacyTreeEntryV1 {
118                name: "vendor".to_string(),
119                mode: LegacyFileMode::Normal,
120                entry_type: LegacyEntryType::Blob,
121                hash: blob_hash,
122            }],
123        })
124        .expect("legacy tree serializes");
125
126        let decoded = decode_legacy_tree_v1(&raw).expect("legacy tree decodes");
127        let entry = decoded.get("vendor").expect("entry exists");
128
129        assert!(entry.is_blob());
130        assert_eq!(entry.blob_hash(), Some(blob_hash));
131        assert!(entry.gitlink_target().is_none());
132    }
133}