ipfs_unixfs/
symlink.rs

1//! UnixFS symlink support. UnixFS symlinks are UnixFS messages similar to single block files, but
2//! the link name or target path is encoded in the UnixFS::Data field. This means that the target
3//! path could be in any encoding, however it is always treated as an utf8 Unix path. Could be that
4//! this is wrong.
5
6use crate::pb::{FlatUnixFs, UnixFs, UnixFsType};
7use alloc::borrow::Cow;
8use quick_protobuf::{MessageWrite, Writer};
9
10/// Appends a dag-pb block for for a symlink to the given target_path. It is expected that the
11/// `target_path` is valid relative unix path relative to the place in which this is used but
12/// targets validity cannot really be judged.
13pub fn serialize_symlink_block(target_path: &str, block_buffer: &mut Vec<u8>) {
14    // should this fail or not? protobuf encoding cannot fail here, however we might create a too
15    // large block but what's the limit?
16    //
17    // why not return a (Cid, Vec<u8>) like usually with cidv0? well...
18
19    let node = FlatUnixFs {
20        links: Vec::new(),
21        data: UnixFs {
22            Type: UnixFsType::Symlink,
23            Data: Some(Cow::Borrowed(target_path.as_bytes())),
24            ..Default::default()
25        },
26    };
27
28    let mut writer = Writer::new(block_buffer);
29    node.write_message(&mut writer).expect("unexpected failure");
30}
31
32#[cfg(test)]
33mod tests {
34    use super::serialize_symlink_block;
35    use cid::Cid;
36    use core::convert::TryFrom;
37    use sha2::{Digest, Sha256};
38
39    #[test]
40    fn simple_symlink() {
41        let mut buf = Vec::new();
42
43        // this symlink just points to a "b" at the same level, used in `symlinks_in_trees` to
44        // create the `foo_directory/a` which links to sibling `b` or the directory
45        // `foo_directory/b`.
46        serialize_symlink_block("b", &mut buf);
47
48        let mh = multihash::wrap(multihash::Code::Sha2_256, &Sha256::digest(&buf));
49        let cid = Cid::new_v0(mh).expect("sha2_256 is the correct multihash for cidv0");
50
51        assert_eq!(
52            cid.to_string(),
53            "QmfLJN6HLyREnWr7QQNmgmuNziUhcbwUopkHQ8gD3pMfp6"
54        );
55    }
56
57    #[test]
58    fn symlinks_in_trees_rooted() {
59        use crate::dir::builder::BufferingTreeBuilder;
60
61        let mut tree = BufferingTreeBuilder::default();
62
63        tree.put_link(
64            "foo_directory/b/car",
65            Cid::try_from("QmNYVgoDXh3dqC1jjCuYqQ9w4XfiocehPZjEPiQiCVYv33").unwrap(),
66            12,
67        )
68        .unwrap();
69
70        tree.put_link(
71            "foo_directory/a",
72            Cid::try_from("QmfLJN6HLyREnWr7QQNmgmuNziUhcbwUopkHQ8gD3pMfp6").unwrap(),
73            7,
74        )
75        .unwrap();
76
77        let otn = tree.build().last().unwrap().unwrap();
78
79        assert_eq!(
80            otn.cid.to_string(),
81            "QmZDVQHwjHwA4SyzEDtJLNxmZeJVK1W8BWFAHV61x2Rs19"
82        );
83    }
84
85    #[test]
86    fn symlinks_in_trees_wrapped() {
87        use crate::dir::builder::{BufferingTreeBuilder, TreeOptions};
88
89        // note regarding the root directory; now we can add the paths without the first component
90        // `foo_directory` and still get the same result as in `symlinks_in_trees_rooted`.
91        let mut opts = TreeOptions::default();
92        opts.wrap_with_directory();
93
94        let mut tree = BufferingTreeBuilder::new(opts);
95
96        tree.put_link(
97            "b/car",
98            Cid::try_from("QmNYVgoDXh3dqC1jjCuYqQ9w4XfiocehPZjEPiQiCVYv33").unwrap(),
99            12,
100        )
101        .unwrap();
102
103        tree.put_link(
104            "a",
105            Cid::try_from("QmfLJN6HLyREnWr7QQNmgmuNziUhcbwUopkHQ8gD3pMfp6").unwrap(),
106            7,
107        )
108        .unwrap();
109
110        let otn = tree.build().last().unwrap().unwrap();
111
112        assert_eq!(
113            otn.cid.to_string(),
114            "QmZDVQHwjHwA4SyzEDtJLNxmZeJVK1W8BWFAHV61x2Rs19"
115        );
116    }
117
118    #[test]
119    fn walking_symlink_containing_tree() {
120        use crate::walk::{ContinuedWalk, Walker};
121        use hex_literal::hex;
122        use std::path::PathBuf;
123
124        // while this case or similar should be repeated in the walker tests, the topic of symlinks
125        // and how the target path names are handled (esp. on windows) is curious enough to warrant
126        // duplicating these three cases here.
127
128        let mut fake = crate::test_support::FakeBlockstore::default();
129
130        // if `simple_symlink` and `symlinks_in_trees_*` passed, they would had created these
131        // blocks, which we now take for granted.
132
133        let tree_blocks: &[(&'static str, &'static [u8])] = &[
134            ("QmZDVQHwjHwA4SyzEDtJLNxmZeJVK1W8BWFAHV61x2Rs19", &hex!("12290a221220fc7fac69ddb44e39686ecfd1ecc6c52ab653f4227e533ee74a2e238f8b2143d3120161180712290a221220b924ddb19181d159c29eec7c98ec506976a76d40241ccd203b226849ce6e0b72120162183d0a020801")),
135            ("QmfLJN6HLyREnWr7QQNmgmuNziUhcbwUopkHQ8gD3pMfp6", &hex!("0a050804120162")),
136            ("QmaoNjmCQ9774sR6H4DzgGPafXyuVVTCyBeXLaxueKYRLm", &hex!("122b0a2212200308c49252eb61966f802baf45074e074f3b3b90619766e0589c1445261a1a221203636172180c0a020801")),
137            ("QmNYVgoDXh3dqC1jjCuYqQ9w4XfiocehPZjEPiQiCVYv33", &hex!("0a0a080212046361720a1804")),
138        ];
139
140        for (expected, bytes) in tree_blocks {
141            assert_eq!(*expected, fake.insert_v0(bytes).to_string());
142        }
143
144        let mut walker = Walker::new(
145            // note: this matches the `symlinks_in_trees` root cid (the last cid produced)
146            Cid::try_from(tree_blocks[0].0).unwrap(),
147            String::default(),
148        );
149
150        #[derive(Debug, PartialEq, Eq)]
151        enum Entry {
152            Dir(PathBuf),
153            Symlink(PathBuf, String),
154            File(PathBuf),
155        }
156
157        let mut actual = Vec::new();
158
159        while walker.should_continue() {
160            let (next, _) = walker.pending_links();
161            let next = fake.get_by_cid(next);
162
163            match walker.next(next, &mut None).unwrap() {
164                ContinuedWalk::File(_fs, _cid, path, _metadata, _total_size) => {
165                    actual.push(Entry::File(path.into()));
166                }
167                ContinuedWalk::RootDirectory(_cid, path, _metadata)
168                | ContinuedWalk::Directory(_cid, path, _metadata) => {
169                    actual.push(Entry::Dir(path.into()));
170                }
171                ContinuedWalk::Bucket(..) => { /* ignore */ }
172                ContinuedWalk::Symlink(link_name, _cid, path, _metadata) => {
173                    actual.push(Entry::Symlink(
174                        path.into(),
175                        core::str::from_utf8(link_name).unwrap().to_owned(),
176                    ));
177                }
178            };
179        }
180
181        // possibly noteworthy: compare these paths to the ones used when creating; there was
182        // non-empty root component `foo_directory`, refer to `symlinks_in_trees_*` variants for
183        // more.
184        let expected = &[
185            Entry::Dir(PathBuf::from("")),
186            Entry::Symlink(PathBuf::from("a"), String::from("b")),
187            Entry::Dir(PathBuf::from("b")),
188            Entry::File({
189                let mut p = PathBuf::from("b");
190                p.push("car");
191                p
192            }),
193        ];
194
195        assert_eq!(expected, actual.as_slice());
196    }
197}