revault_lockbox_api/lockbox/
symlinks.rs1use 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 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 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 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}