1use 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
20pub 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}