Skip to main content

xdg_thumbnail/
namespace.rs

1// SPDX-FileCopyrightText: 2026 KIM Hyunjae
2// SPDX-License-Identifier: MPL-2.0
3
4use std::fmt;
5use std::path::{Path, PathBuf};
6use std::str::FromStr;
7
8use crate::{Result, ThumbnailError};
9
10static THUMBNAIL_SIZES: [ThumbnailSize; 4] = [
11    ThumbnailSize::Normal,
12    ThumbnailSize::Large,
13    ThumbnailSize::XLarge,
14    ThumbnailSize::XxLarge,
15];
16
17/// A successful-thumbnail size namespace or a program failure namespace.
18#[derive(Clone, Debug, Eq, Hash, PartialEq)]
19#[non_exhaustive]
20pub enum CacheNamespace {
21    /// A successful thumbnail size directory.
22    Size(ThumbnailSize),
23    /// A failure-entry namespace under `fail/`.
24    Failure(FailureNamespace),
25}
26
27impl CacheNamespace {
28    pub(crate) fn join_under(&self, root: &Path, filename: &str) -> PathBuf {
29        match self {
30            Self::Size(size) => root.join(size.directory_name()).join(filename),
31            Self::Failure(namespace) => root.join("fail").join(namespace.as_str()).join(filename),
32        }
33    }
34
35    /// Returns the relative cache directory for this namespace.
36    #[must_use]
37    pub fn relative_directory(&self) -> String {
38        match self {
39            Self::Size(size) => size.directory_name().to_owned(),
40            Self::Failure(namespace) => format!("fail/{}", namespace.as_str()),
41        }
42    }
43}
44
45impl fmt::Display for CacheNamespace {
46    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47        f.write_str(&self.relative_directory())
48    }
49}
50
51/// A validated direct directory name for failure entries.
52#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
53pub struct FailureNamespace {
54    value: String,
55}
56
57impl FailureNamespace {
58    /// Creates a failure namespace from an ASCII direct directory name.
59    ///
60    /// # Errors
61    ///
62    /// Returns an error when the value is empty, is `.` or `..`, or contains bytes outside ASCII
63    /// letters, digits, `.`, `_`, `+`, and `-`.
64    pub fn new(value: impl Into<String>) -> Result<Self> {
65        let value = value.into();
66        if value.is_empty() || value == "." || value == ".." {
67            return Err(ThumbnailError::invalid_namespace(
68                "failure namespace must be a non-empty direct name",
69            ));
70        }
71        if !value
72            .bytes()
73            .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'+' | b'-'))
74        {
75            return Err(ThumbnailError::invalid_namespace(
76                "failure namespace contains an invalid character",
77            ));
78        }
79        Ok(Self { value })
80    }
81
82    /// Returns the namespace directory name.
83    #[must_use]
84    pub fn as_str(&self) -> &str {
85        &self.value
86    }
87}
88
89impl fmt::Display for FailureNamespace {
90    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91        f.write_str(&self.value)
92    }
93}
94
95impl AsRef<str> for FailureNamespace {
96    fn as_ref(&self) -> &str {
97        self.as_str()
98    }
99}
100
101impl FromStr for FailureNamespace {
102    type Err = ThumbnailError;
103
104    fn from_str(value: &str) -> Result<Self> {
105        Self::new(value)
106    }
107}
108
109/// A standard Freedesktop thumbnail size directory.
110#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
111#[non_exhaustive]
112pub enum ThumbnailSize {
113    /// 128px thumbnail cache directory.
114    Normal,
115    /// 256px thumbnail cache directory.
116    Large,
117    /// 512px thumbnail cache directory.
118    XLarge,
119    /// 1024px thumbnail cache directory.
120    XxLarge,
121}
122
123impl ThumbnailSize {
124    /// Returns the standard cache directory name for this size.
125    #[must_use]
126    pub const fn directory_name(self) -> &'static str {
127        match self {
128            Self::Normal => "normal",
129            Self::Large => "large",
130            Self::XLarge => "x-large",
131            Self::XxLarge => "xx-large",
132        }
133    }
134
135    /// Returns the maximum width and height for this namespace in pixels.
136    #[must_use]
137    pub const fn max_dimension(self) -> u32 {
138        match self {
139            Self::Normal => 128,
140            Self::Large => 256,
141            Self::XLarge => 512,
142            Self::XxLarge => 1024,
143        }
144    }
145
146    /// Returns all standard thumbnail sizes in cache scan order.
147    #[must_use]
148    pub const fn all() -> &'static [Self] {
149        &THUMBNAIL_SIZES
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use super::{CacheNamespace, FailureNamespace, ThumbnailSize};
156
157    #[test]
158    fn thumbnail_size_directory_names_match_standard() {
159        assert_eq!(ThumbnailSize::Normal.directory_name(), "normal");
160        assert_eq!(ThumbnailSize::Large.directory_name(), "large");
161        assert_eq!(ThumbnailSize::XLarge.directory_name(), "x-large");
162        assert_eq!(ThumbnailSize::XxLarge.directory_name(), "xx-large");
163    }
164
165    #[test]
166    fn all_thumbnail_sizes_are_in_scan_order() {
167        let names = ThumbnailSize::all()
168            .iter()
169            .copied()
170            .map(ThumbnailSize::directory_name)
171            .collect::<Vec<_>>();
172
173        assert_eq!(names, ["normal", "large", "x-large", "xx-large"]);
174    }
175
176    #[test]
177    fn cache_namespace_strings_are_relative_directories() {
178        let size = CacheNamespace::Size(ThumbnailSize::Normal);
179        let failure = CacheNamespace::Failure(FailureNamespace::new("app-1").unwrap());
180
181        assert_eq!(size.relative_directory(), "normal");
182        assert_eq!(size.to_string(), "normal");
183        assert_eq!(failure.relative_directory(), "fail/app-1");
184        assert_eq!(failure.to_string(), "fail/app-1");
185    }
186}