1use super::intervals::{Interval, union};
3use super::{Media, Policy, PolicySnapshot};
4use serde::{Deserialize, Serialize};
5use std::{
6 collections::BTreeMap,
7 io::{Read, Write},
8 path::{Path, PathBuf},
9};
10
11const FORMAT_VERSION: u32 = 1;
12const MAX_BYTES: usize = 8 * 1024 * 1024;
13const MAX_REVISION: u64 = (1 << 53) - 1;
14pub(super) const RETAIN_MS: u64 = 32 * 86_400_000;
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
17#[serde(deny_unknown_fields)]
18pub(super) struct Reservation {
19 pub(super) id: String,
20 pub(super) media: Media,
21 pub(super) interval: Interval,
22 pub(super) consumed_ms: u64,
23}
24#[derive(Debug, Clone, Default, Serialize, Deserialize)]
25#[serde(deny_unknown_fields)]
26pub(super) struct Owner {
27 pub(super) revision: u64,
28 #[serde(default, skip_serializing_if = "Option::is_none")]
29 pub(super) locked_by: Option<String>,
30 pub(super) policy: Policy,
31 pub(super) settled: Vec<Interval>,
32 pub(super) reservations: Vec<Reservation>,
33}
34#[derive(Debug, Clone, Serialize, Deserialize)]
36#[serde(deny_unknown_fields)]
37pub struct ParentalStore {
38 format_version: u32,
39 #[serde(deserialize_with = "unique_owners")]
40 pub(super) owners: BTreeMap<String, Owner>,
41}
42impl Default for ParentalStore {
43 fn default() -> Self {
44 Self {
45 format_version: FORMAT_VERSION,
46 owners: BTreeMap::new(),
47 }
48 }
49}
50pub fn path(data_dir: &Path) -> PathBuf {
52 data_dir.join("parental.json")
53}
54pub(super) fn valid_owner(owner: &str) -> bool {
55 !owner.is_empty() && owner.len() <= 256 && !owner.chars().any(char::is_control)
56}
57pub(super) fn valid_lease(id: &str) -> bool {
58 id.len() == 64 && id.bytes().all(|byte| byte.is_ascii_hexdigit())
59}
60impl ParentalStore {
61 pub fn load(path: &Path) -> Result<Self, String> {
63 Self::load_optional(path).map(Option::unwrap_or_default)
64 }
65 pub fn load_optional(path: &Path) -> Result<Option<Self>, String> {
68 let inspected = match std::fs::symlink_metadata(path) {
69 Ok(metadata) => metadata,
70 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
71 return Ok(None);
72 }
73 Err(_) => return Err("parental policy could not be read".into()),
74 };
75 crate::accounts::check_private_file(path)
76 .map_err(|_| "parental store must be a private regular file")?;
77 let mut file =
78 std::fs::File::open(path).map_err(|_| "parental policy could not be opened")?;
79 let opened = file
80 .metadata()
81 .map_err(|_| "parental policy metadata is unavailable")?;
82 crate::accounts::validate_private_read(&inspected, &opened)
83 .map_err(|_| "parental policy changed while opening")?;
84 if opened.len() > MAX_BYTES as u64 {
85 return Err("parental store exceeds 8 MiB".into());
86 }
87 let mut bytes = Vec::new();
88 (&mut file)
89 .take(MAX_BYTES as u64 + 1)
90 .read_to_end(&mut bytes)
91 .map_err(|_| "parental policy read failed")?;
92 if bytes.len() > MAX_BYTES {
93 return Err("parental store exceeds 8 MiB".into());
94 }
95 let after = file
96 .metadata()
97 .map_err(|_| "parental policy metadata is unavailable")?;
98 let current =
99 std::fs::symlink_metadata(path).map_err(|_| "parental policy changed while reading")?;
100 crate::accounts::validate_private_read(&opened, &after)
101 .map_err(|_| "parental policy changed while reading")?;
102 crate::accounts::validate_private_read(&opened, ¤t)
103 .map_err(|_| "parental policy changed while reading")?;
104 if opened.len() != after.len()
105 || opened.modified().ok() != after.modified().ok()
106 || opened.len() != current.len()
107 || opened.modified().ok() != current.modified().ok()
108 {
109 return Err("parental policy changed while reading".into());
110 }
111 let store: Self = serde_json::from_slice(&bytes)
112 .map_err(|_| "parental store is unreadable or unsupported")?;
113 store.validate()?;
114 Ok(Some(store))
115 }
116 pub fn save(&self, path: &Path) -> Result<(), String> {
118 self.validate()?;
119 crate::accounts::check_private_file(path)
120 .map_err(|_| "parental store must remain private")?;
121 let mut bytes = Limited(Vec::new());
122 serde_json::to_writer(&mut bytes, self)
123 .map_err(|_| "parental store exceeds its serialization budget")?;
124 crate::atomic_file::write(path, &bytes.0)
125 .map_err(|_| "parental store could not be saved".to_owned())
126 }
127 pub fn policy(&self, owner: &str) -> PolicySnapshot {
129 let value = self.owners.get(owner);
130 PolicySnapshot {
131 owner: owner.into(),
132 revision: value.map_or(0, |value| value.revision),
133 locked_by: value.and_then(|value| value.locked_by.clone()),
134 policy: value.map_or_else(Policy::default, |value| value.policy.clone()),
135 }
136 }
137 pub fn set_policy(
139 &mut self,
140 owner: &str,
141 policy: Policy,
142 expected_revision: u64,
143 ) -> Result<PolicySnapshot, String> {
144 self.set_policy_by(owner, None, policy, expected_revision)
145 }
146 pub fn set_policy_by(
149 &mut self,
150 owner: &str,
151 administrator: Option<&str>,
152 policy: Policy,
153 expected_revision: u64,
154 ) -> Result<PolicySnapshot, String> {
155 if !valid_owner(owner) {
156 return Err("invalid parental owner".into());
157 }
158 let policy = policy.canonicalize()?;
159 let old = self.owners.get(owner);
160 if administrator.is_some_and(|value| !valid_owner(value) || value == owner)
161 || old
162 .and_then(|value| value.locked_by.as_deref())
163 .is_some_and(|locked| Some(locked) != administrator)
164 {
165 return Err("parental policy is locked by another administrator".into());
166 }
167 let locked_by = policy
168 .enabled
169 .then_some(administrator)
170 .flatten()
171 .map(str::to_owned);
172 if old.map_or(0, |value| value.revision) != expected_revision {
173 return Err("parental policy revision conflict".into());
174 }
175 if old.is_some_and(|value| value.policy == policy && value.locked_by == locked_by) {
176 return Ok(self.policy(owner));
177 }
178 let mut candidate = self.clone();
179 let row = candidate.owners.entry(owner.into()).or_default();
180 row.revision = row
181 .revision
182 .checked_add(1)
183 .filter(|value| *value <= MAX_REVISION)
184 .ok_or("parental policy revision exhausted")?;
185 row.policy = policy;
186 row.locked_by = locked_by;
187 candidate.validate()?;
188 *self = candidate;
189 Ok(self.policy(owner))
190 }
191 pub fn supervises(&self, administrator: &str) -> bool {
193 self.owners
194 .values()
195 .any(|owner| owner.locked_by.as_deref() == Some(administrator))
196 }
197 pub(crate) fn validate(&self) -> Result<(), String> {
198 if self.format_version != FORMAT_VERSION || self.owners.len() > 256 {
199 return Err("unsupported or oversized parental store".into());
200 }
201 let mut intervals = 0_usize;
202 let mut text_bytes = 0_usize;
203 for (name, owner) in &self.owners {
204 if !valid_owner(name)
205 || owner
206 .locked_by
207 .as_deref()
208 .is_some_and(|id| !valid_owner(id) || id == name || !owner.policy.enabled)
209 || owner.revision > MAX_REVISION
210 || owner.reservations.len() > 8
211 || owner.settled.len() > 8192
212 {
213 return Err("invalid parental owner or ledger bounds".into());
214 }
215 owner.policy.validate()?;
216 text_bytes = text_bytes.saturating_add(
217 owner
218 .policy
219 .blocked_artists
220 .iter()
221 .chain(&owner.policy.blocked_podcasts)
222 .map(String::len)
223 .sum::<usize>(),
224 );
225 intervals += owner.settled.len();
226 let mut previous_end = 0;
227 for value in &owner.settled {
228 if value.start_ms >= value.end_ms
229 || value.end_ms > 253_402_300_799_999
230 || value.end_ms - value.start_ms > RETAIN_MS
231 || value.start_ms < previous_end
232 {
233 return Err("invalid parental usage interval".into());
234 }
235 previous_end = value.end_ms;
236 }
237 let mut ids = std::collections::BTreeSet::new();
238 for value in &owner.reservations {
239 if !valid_lease(&value.id)
240 || !ids.insert(&value.id)
241 || value.interval.start_ms >= value.interval.end_ms
242 || value.interval.end_ms > 253_402_300_799_999
243 || value.interval.end_ms - value.interval.start_ms > 10_000
244 || value.consumed_ms > value.interval.end_ms - value.interval.start_ms
245 {
246 return Err("invalid parental listening reservation".into());
247 }
248 if let Media::Podcast { feed } = &value.media {
249 super::canonical_feed(feed)?;
250 }
251 }
252 }
253 if intervals > 65_536 || text_bytes > 2 * 1024 * 1024 {
254 return Err("parental store exceeds its global budget".into());
255 }
256 Ok(())
257 }
258}
259impl Owner {
260 pub(super) fn intervals(&self) -> Vec<Interval> {
261 union(
262 self.settled
263 .iter()
264 .copied()
265 .chain(self.reservations.iter().map(|value| value.interval))
266 .collect(),
267 )
268 }
269 pub(super) fn prune(&mut self, now_ms: u64) {
270 let horizon = self
274 .settled
275 .iter()
276 .map(|interval| interval.end_ms)
277 .max()
278 .unwrap_or(now_ms)
279 .max(now_ms)
280 .min(now_ms.saturating_add(10_000));
281 let cutoff = horizon.saturating_sub(RETAIN_MS);
282 self.reservations
285 .retain(|reservation| reservation.interval.end_ms > cutoff);
286 self.settled = union(
287 self.settled
288 .iter()
289 .filter_map(|value| {
290 (value.end_ms > cutoff).then_some(Interval {
291 start_ms: value.start_ms.max(cutoff),
292 end_ms: value.end_ms,
293 })
294 })
295 .collect(),
296 );
297 }
298 pub(super) fn make_room(&mut self, now_ms: u64) {
299 if self.reservations.len() < 8 {
300 return;
301 }
302 if let Some(index) = self
303 .reservations
304 .iter()
305 .enumerate()
306 .filter(|(_, value)| value.interval.end_ms <= now_ms)
307 .min_by_key(|(_, value)| value.interval.end_ms)
308 .map(|(index, _)| index)
309 {
310 let expired = self.reservations.remove(index);
311 self.settled.push(expired.interval);
312 self.prune(now_ms);
313 }
314 }
315}
316struct Limited(Vec<u8>);
317impl Write for Limited {
318 fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
319 if self.0.len().saturating_add(bytes.len()) > MAX_BYTES {
320 return Err(std::io::Error::other("parental store byte budget exceeded"));
321 }
322 self.0.extend_from_slice(bytes);
323 Ok(bytes.len())
324 }
325 fn flush(&mut self) -> std::io::Result<()> {
326 Ok(())
327 }
328}
329#[cfg(test)]
330#[path = "storage_tests.rs"]
331mod tests;
332
333fn unique_owners<'de, D: serde::Deserializer<'de>>(
334 deserializer: D,
335) -> Result<BTreeMap<String, Owner>, D::Error> {
336 struct Owners;
337 impl<'de> serde::de::Visitor<'de> for Owners {
338 type Value = BTreeMap<String, Owner>;
339 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
340 formatter.write_str("at most 256 unique parental owners")
341 }
342 fn visit_map<A: serde::de::MapAccess<'de>>(
343 self,
344 mut input: A,
345 ) -> Result<Self::Value, A::Error> {
346 let mut values = BTreeMap::new();
347 while let Some((owner, value)) = input.next_entry::<String, Owner>()? {
348 if values.len() >= 256 || values.insert(owner, value).is_some() {
349 return Err(serde::de::Error::custom(
350 "duplicate or excessive parental owners",
351 ));
352 }
353 }
354 Ok(values)
355 }
356 }
357 deserializer.deserialize_map(Owners)
358}