1use std::{
2 num::{NonZeroU64, NonZeroUsize},
3 path::Path,
4 sync::Arc,
5};
6
7use serde::Serialize;
8use thiserror::Error;
9
10pub(crate) use super::path::PortableLogicalPath;
11use super::{
12 ContentValidationCode, ContentValidationError, ContentValidationErrors, LogicalAssetPath,
13 LogicalContentPath, LogicalTreePathError, PostCollection, PostSource, PublicationSource,
14 ValidatedContent, validate_content,
15};
16
17#[cfg(target_os = "linux")]
18mod linux;
19
20#[cfg(test)]
21mod tests;
22
23const DEFAULT_PUBLICATION_BYTES: u64 = 256 * 1024;
24pub(crate) const DEFAULT_POST_BYTES: u64 = 4 * 1024 * 1024;
25const DEFAULT_ASSET_BYTES: u64 = 32 * 1024 * 1024;
26const DEFAULT_TREE_BYTES: u64 = 256 * 1024 * 1024;
27const DEFAULT_ENTRIES: usize = 10_000;
28const DEFAULT_DEPTH: usize = 16;
29const DEFAULT_PATH_BYTES: usize = 1_024;
30
31macro_rules! content_u64_limit {
32 ($name:ident) => {
33 #[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
34 #[serde(transparent)]
35 pub struct $name(NonZeroU64);
36
37 impl $name {
38 pub const fn new(value: u64) -> Option<Self> {
39 match NonZeroU64::new(value) {
40 Some(value) if value.get() < u64::MAX => Some(Self(value)),
41 _ => None,
42 }
43 }
44
45 pub const fn get(self) -> u64 {
46 self.0.get()
47 }
48
49 fn default_value(value: u64) -> Self {
50 Self(NonZeroU64::new(value).unwrap_or(NonZeroU64::MIN))
51 }
52 }
53 };
54}
55
56macro_rules! content_usize_limit {
57 ($name:ident) => {
58 #[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
59 #[serde(transparent)]
60 pub struct $name(NonZeroUsize);
61
62 impl $name {
63 pub const fn new(value: usize) -> Option<Self> {
64 match NonZeroUsize::new(value) {
65 Some(value) => Some(Self(value)),
66 None => None,
67 }
68 }
69
70 pub const fn get(self) -> usize {
71 self.0.get()
72 }
73
74 fn default_value(value: usize) -> Self {
75 Self(NonZeroUsize::new(value).unwrap_or(NonZeroUsize::MIN))
76 }
77 }
78 };
79}
80
81content_u64_limit!(ContentFileByteLimit);
82content_u64_limit!(ContentTreeByteLimit);
83content_usize_limit!(ContentEntryLimit);
84content_usize_limit!(ContentDepthLimit);
85content_usize_limit!(ContentPathByteLimit);
86
87#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
88pub struct ContentTreeLimits {
89 pub publication_file_bytes: ContentFileByteLimit,
90 pub post_file_bytes: ContentFileByteLimit,
91 pub asset_file_bytes: ContentFileByteLimit,
92 pub total_tree_bytes: ContentTreeByteLimit,
93 pub entries: ContentEntryLimit,
94 pub depth: ContentDepthLimit,
95 pub path_bytes: ContentPathByteLimit,
96}
97
98impl ContentTreeLimits {
99 pub fn new(
100 publication_file_bytes: ContentFileByteLimit,
101 post_file_bytes: ContentFileByteLimit,
102 asset_file_bytes: ContentFileByteLimit,
103 total_tree_bytes: ContentTreeByteLimit,
104 entries: ContentEntryLimit,
105 depth: ContentDepthLimit,
106 path_bytes: ContentPathByteLimit,
107 ) -> Result<Self, ContentTreeLimitsError> {
108 if [publication_file_bytes, post_file_bytes, asset_file_bytes]
109 .into_iter()
110 .any(|limit| limit.get() > total_tree_bytes.get())
111 {
112 return Err(ContentTreeLimitsError);
113 }
114 Ok(Self {
115 publication_file_bytes,
116 post_file_bytes,
117 asset_file_bytes,
118 total_tree_bytes,
119 entries,
120 depth,
121 path_bytes,
122 })
123 }
124
125 pub(crate) const fn file_limit(self, kind: ContentFileKind) -> ContentFileByteLimit {
126 match kind {
127 ContentFileKind::Publication => self.publication_file_bytes,
128 ContentFileKind::Post(_) => self.post_file_bytes,
129 ContentFileKind::Asset => self.asset_file_bytes,
130 }
131 }
132}
133
134impl Default for ContentTreeLimits {
135 fn default() -> Self {
136 Self {
137 publication_file_bytes: ContentFileByteLimit::default_value(DEFAULT_PUBLICATION_BYTES),
138 post_file_bytes: ContentFileByteLimit::default_value(DEFAULT_POST_BYTES),
139 asset_file_bytes: ContentFileByteLimit::default_value(DEFAULT_ASSET_BYTES),
140 total_tree_bytes: ContentTreeByteLimit::default_value(DEFAULT_TREE_BYTES),
141 entries: ContentEntryLimit::default_value(DEFAULT_ENTRIES),
142 depth: ContentDepthLimit::default_value(DEFAULT_DEPTH),
143 path_bytes: ContentPathByteLimit::default_value(DEFAULT_PATH_BYTES),
144 }
145 }
146}
147
148#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
149#[error("each file limit must not exceed the complete tree limit")]
150pub struct ContentTreeLimitsError;
151
152#[derive(Clone, Debug, Eq, PartialEq)]
153pub struct DiscoveredPublication {
154 pub path: LogicalContentPath,
155 pub source: Box<str>,
156}
157
158#[derive(Clone, Debug, Eq, PartialEq)]
159pub struct DiscoveredPost {
160 pub path: LogicalContentPath,
161 pub collection: PostCollection,
162 pub source: Box<str>,
163}
164
165#[derive(Clone, Debug, Eq, PartialEq)]
166pub struct DiscoveredAsset {
167 pub path: LogicalAssetPath,
168 pub bytes: Arc<[u8]>,
169}
170
171#[derive(Clone, Debug, Eq, PartialEq)]
172pub struct DiscoveredContentTree {
173 pub publication: DiscoveredPublication,
174 pub posts: Vec<DiscoveredPost>,
175 pub assets: Vec<DiscoveredAsset>,
176 pub total_bytes: u64,
177}
178
179impl DiscoveredContentTree {
180 pub fn validate(&self) -> Result<ValidatedContent, ContentValidationErrors> {
181 validate_content(
182 PublicationSource::new(self.publication.path.as_str(), &self.publication.source),
183 self.posts.iter().map(|post| PostSource {
184 path: post.path.clone(),
185 contents: &post.source,
186 collection: post.collection,
187 }),
188 )
189 }
190
191 pub(crate) fn new(
192 publication: DiscoveredPublication,
193 posts: Vec<DiscoveredPost>,
194 assets: Vec<DiscoveredAsset>,
195 total_bytes: u64,
196 ) -> Self {
197 Self {
198 publication,
199 posts,
200 assets,
201 total_bytes,
202 }
203 }
204}
205
206pub fn discover_content_tree(
207 root: &Path,
208 limits: ContentTreeLimits,
209) -> Result<DiscoveredContentTree, ContentValidationErrors> {
210 discover_content_tree_with_hooks(root, limits, || {}, |_| {}, |_| {})
211}
212
213#[cfg(test)]
214fn discover_content_tree_with_hook(
215 root: &Path,
216 limits: ContentTreeLimits,
217 before_read: impl FnOnce(),
218) -> Result<DiscoveredContentTree, ContentValidationErrors> {
219 discover_content_tree_with_hooks(root, limits, before_read, |_| {}, |_| {})
220}
221
222#[cfg(target_os = "linux")]
223fn discover_content_tree_with_hooks(
224 root: &Path,
225 limits: ContentTreeLimits,
226 before_read: impl FnOnce(),
227 before_file_read: impl FnMut(&str),
228 after_file_read: impl FnMut(&str),
229) -> Result<DiscoveredContentTree, ContentValidationErrors> {
230 linux::discover(root, limits, before_read, before_file_read, after_file_read)
231}
232
233#[cfg(not(target_os = "linux"))]
234fn discover_content_tree_with_hooks(
235 _root: &Path,
236 _limits: ContentTreeLimits,
237 _before_read: impl FnOnce(),
238 _before_file_read: impl FnMut(&str),
239 _after_file_read: impl FnMut(&str),
240) -> Result<DiscoveredContentTree, ContentValidationErrors> {
241 let mut diagnostics = super::DiagnosticCollector::default();
242 diagnostics.push(ContentValidationError::new(
243 LogicalContentPath::new("<content-root>"),
244 "$path",
245 ContentValidationCode::ContentPlatformUnsupported,
246 "safe content discovery requires the supported Linux filesystem boundary",
247 ));
248 Err(diagnostics.finish())
249}
250
251#[derive(Clone, Copy, Debug, Eq, PartialEq)]
252pub(crate) enum ContentFileKind {
253 Publication,
254 Post(PostCollection),
255 Asset,
256}
257
258pub(crate) fn tree_error(
259 path: impl Into<String>,
260 code: ContentValidationCode,
261 message: impl Into<String>,
262) -> ContentValidationError {
263 ContentValidationError::new(LogicalContentPath::new(path), "$path", code, message)
264}
265
266pub(crate) fn publication(path: impl Into<String>, source: String) -> DiscoveredPublication {
267 DiscoveredPublication {
268 path: LogicalContentPath::new(path),
269 source: source.into_boxed_str(),
270 }
271}
272
273pub(crate) fn post(
274 path: impl Into<String>,
275 collection: PostCollection,
276 source: String,
277) -> DiscoveredPost {
278 DiscoveredPost {
279 path: LogicalContentPath::new(path),
280 collection,
281 source: source.into_boxed_str(),
282 }
283}
284
285pub(crate) fn asset(path: LogicalAssetPath, bytes: Vec<u8>) -> DiscoveredAsset {
286 DiscoveredAsset {
287 path,
288 bytes: Arc::from(bytes),
289 }
290}