Skip to main content

revault_lockbox_api/lockbox/
symlinks.rs

1use super::Lockbox;
2use crate::constants::DEFAULT_SYMLINK_PERMISSIONS;
3use crate::file_format::{decode_symlink_payload, encode_symlink_payload};
4use crate::lockbox_path::LockboxPath;
5use crate::node_kind::NodeKind;
6use crate::page::{
7    encoded_object_len, page_size_for_encoded_objects, uncompressed_objects_fit, PageObject,
8    PageObjectKind, DEFAULT_METADATA_PAGE_BYTES,
9};
10use crate::toc_entry::TocEntry;
11use crate::{Error, Result};
12
13impl<State> Lockbox<State> {
14    /// Add or replace a symbolic link.
15    ///
16    /// When `replace` is `false`, returns `Error::AlreadyExists` if `path`
17    /// already names an existing file or symlink. When `replace` is `true`,
18    /// returns `Error::NotFound` if there is no existing entry to replace. Returns
19    /// `Error::InvalidPath` for directory-only or unsafe lockbox paths and
20    /// propagates storage errors from the write.
21    pub fn add_symlink(
22        &mut self,
23        path: &LockboxPath,
24        target: &LockboxPath,
25        replace: bool,
26    ) -> Result<()>
27    where
28        State: crate::WritableLockboxState,
29    {
30        let path = path.file_path()?;
31        let target = target.file_path()?;
32        self.ensure_parent_directory(&path)?;
33        self.validate_replace_intent(&path, replace)?;
34        if self.should_discard_file_pages_after_flush()
35            && self.pending_small_files.contains_key(path.as_str())
36        {
37            self.flush_bulk_small_file_packer()?;
38        }
39        self.remove_pending_small_file(&path);
40
41        if let Some(old) = self.toc_entries.get(path.as_str()) {
42            self.free_entry_slots(old.clone())?;
43        }
44
45        self.pending_symlinks.insert(path.clone(), target.clone());
46        let entry = TocEntry {
47            path: path.clone(),
48            len: 0,
49            record_offset: 0,
50            record_len: 0,
51            record_object_id: 0,
52            deleted: false,
53            node_kind: NodeKind::Symlink,
54            permissions: DEFAULT_SYMLINK_PERMISSIONS,
55            chunks: Vec::new(),
56        };
57        self.toc_entries.insert(path.clone(), entry);
58        self.mark_toc_dirty(&path);
59        Ok(())
60    }
61
62    pub(crate) fn flush_pending_symlinks(&mut self) -> Result<()> {
63        if self.pending_symlinks.is_empty() {
64            return Ok(());
65        }
66
67        let pending = std::mem::take(&mut self.pending_symlinks);
68        let mut pending_objects = Vec::new();
69        let mut stream_len = 4usize;
70        for (path, target) in pending {
71            self.sequence += 1;
72            let object = PageObject::new(
73                PageObjectKind::Symlink,
74                self.sequence,
75                encode_symlink_payload(&path, &target),
76            );
77            let object_len = encoded_object_len(&object)?;
78            if !pending_objects.is_empty()
79                && !uncompressed_objects_fit(DEFAULT_METADATA_PAGE_BYTES, stream_len + object_len)
80            {
81                self.write_symlink_recovery_page(std::mem::take(&mut pending_objects))?;
82                stream_len = 4;
83            }
84            if !uncompressed_objects_fit(DEFAULT_METADATA_PAGE_BYTES, 4 + object_len) {
85                return Err(Error::SecurityLimitExceeded(
86                    "symlink payload exceeds metadata page size".to_string(),
87                ));
88            }
89            stream_len += object_len;
90            pending_objects.push(PendingSymlinkObject { path, object });
91        }
92        if !pending_objects.is_empty() {
93            self.write_symlink_recovery_page(pending_objects)?;
94        }
95        Ok(())
96    }
97
98    fn write_symlink_recovery_page(&mut self, pending: Vec<PendingSymlinkObject>) -> Result<()> {
99        let objects = pending
100            .iter()
101            .map(|pending| pending.object.clone())
102            .collect::<Vec<_>>();
103        let page_size = page_size_for_encoded_objects(&objects)?;
104        let page_offset = self.allocate_page_offset(page_size as u64)?;
105        self.write_decoded_page_at(page_offset, self.sequence, objects)?;
106        for pending in pending {
107            if let Some(entry) = self.toc_entries.get_mut(pending.path.as_str()) {
108                entry.record_offset = page_offset;
109                entry.record_len = page_size as u64;
110                entry.record_object_id = pending.object.id;
111                self.dirty_toc_paths.insert(entry.path.clone());
112            }
113        }
114        Ok(())
115    }
116
117    /// Return the target path for a symbolic link.
118    ///
119    /// Returns `Error::InvalidPath` for directory-only paths,
120    /// `Error::NotFound` if `path` is absent or not a symlink, and
121    /// `Error::CorruptRecord` if the stored symlink metadata is inconsistent.
122    pub fn get_symlink_target(&self, path: &LockboxPath) -> Result<LockboxPath> {
123        let path = path.as_file_path()?;
124        let entry = self
125            .toc_entries
126            .get(path)
127            .filter(|entry| !entry.deleted && entry.node_kind == NodeKind::Symlink)
128            .ok_or_else(|| Error::NotFound(path.to_string()))?;
129        self.symlink_target_for_entry(entry)
130    }
131
132    pub(crate) fn symlink_target_for_entry(&self, entry: &TocEntry) -> Result<LockboxPath> {
133        if let Some(target) = self.pending_symlinks.get(entry.path.as_str()) {
134            return Ok(target.clone());
135        }
136        if entry.record_offset == 0 || entry.record_object_id == 0 {
137            return Err(Error::CorruptRecord);
138        }
139        self.with_page_object(entry.record_offset, entry.record_object_id, |object| {
140            if object.kind != PageObjectKind::Symlink {
141                return Err(Error::CorruptRecord);
142            }
143            let (path, target) = object.with_payload(decode_symlink_payload)??;
144            if path != entry.path {
145                return Err(Error::CorruptRecord);
146            }
147            Ok(target)
148        })
149    }
150
151    /// Return true when the logical path is a symbolic link.
152    pub fn is_symlink(&self, path: &LockboxPath) -> bool {
153        let Ok(path) = path.as_file_path() else {
154            return false;
155        };
156        self.toc_entries
157            .get(path)
158            .filter(|entry| !entry.deleted)
159            .map(|entry| entry.node_kind == NodeKind::Symlink)
160            .unwrap_or(false)
161    }
162}
163
164struct PendingSymlinkObject {
165    path: LockboxPath,
166    object: PageObject,
167}