remi_fs/
content_type.rs

1// ๐Ÿปโ€โ„๏ธ๐Ÿงถ remi-rs: Asynchronous Rust crate to handle communication between applications and object storage providers
2// Copyright (c) 2022-2025 Noelware, LLC. <team@noelware.org>
3//
4// Permission is hereby granted, free of charge, to any person obtaining a copy
5// of this software and associated documentation files (the "Software"), to deal
6// in the Software without restriction, including without limitation the rights
7// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8// copies of the Software, and to permit persons to whom the Software is
9// furnished to do so, subject to the following conditions:
10//
11// The above copyright notice and this permission notice shall be included in all
12// copies or substantial portions of the Software.
13//
14// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20// SOFTWARE.
21
22use std::borrow::Cow;
23
24/// Default content type given from a [`ContentTypeResolver`]
25pub const DEFAULT_CONTENT_TYPE: &str = "application/octet-stream";
26
27/// Represents a resolver to resolve content types from a byte slice.
28pub trait ContentTypeResolver: Send + Sync {
29    /// Resolves a byte slice and returns the content type, or [`DEFAULT_CONTENT_TYPE`]
30    /// if none can be resolved from this resolver.
31    fn resolve(&self, data: &[u8]) -> Cow<'static, str>;
32}
33
34impl<F> ContentTypeResolver for F
35where
36    F: Fn(&[u8]) -> Cow<'static, str> + Send + Sync,
37{
38    fn resolve(&self, data: &[u8]) -> Cow<'static, str> {
39        (self)(data)
40    }
41}
42
43/// Default implementation of a [`ContentTypeResolver`]. It can detect any format
44/// that the [`file-format`] and [`infer`] crates can plus:
45///
46/// * [`serde_json`] for JSON documents
47/// * [`serde_yaml_ng`] for YAML documents
48#[cfg(feature = "file-format")]
49pub fn default_resolver(data: &[u8]) -> Cow<'static, str> {
50    #[cfg(feature = "serde_json")]
51    if serde_json::from_slice::<serde_json::Value>(data).is_ok() {
52        // representing "true", "false", "null", "{any string}", "{any number}" should be plain text
53        match serde_json::from_slice(data).unwrap() {
54            serde_json::Value::String(_)
55            | serde_json::Value::Bool(_)
56            | serde_json::Value::Number(_)
57            | serde_json::Value::Null => return Cow::Borrowed("text/plain"),
58
59            serde_json::Value::Array(_) | serde_json::Value::Object(_) => {
60                return Cow::Borrowed("application/json; charset=utf-8")
61            }
62        }
63    }
64
65    #[cfg(feature = "serde_yaml_ng")]
66    if serde_yaml_ng::from_slice::<serde_yaml_ng::Value>(data).is_ok() {
67        fn match_value(value: &serde_yaml_ng::Value) -> Cow<'static, str> {
68            match value {
69                serde_yaml_ng::Value::Bool(_)
70                | serde_yaml_ng::Value::Number(_)
71                | serde_yaml_ng::Value::String(_)
72                | serde_yaml_ng::Value::Null => Cow::Borrowed("text/plain"),
73
74                serde_yaml_ng::Value::Tagged(m) => match_value(&m.value),
75                serde_yaml_ng::Value::Mapping(_) | serde_yaml_ng::Value::Sequence(_) => {
76                    Cow::Borrowed("text/yaml; charset=utf-8")
77                }
78            }
79        }
80
81        return match_value(&serde_yaml_ng::from_slice(data).unwrap());
82    }
83
84    infer::get(data).map(|ty| Cow::Borrowed(ty.mime_type())).unwrap_or({
85        let format = file_format::FileFormat::from_bytes(data);
86        Cow::Owned(format.media_type().to_owned())
87    })
88}
89
90/// A default implementation of a [`ContentTypeResolver`]. It is a loose resolver
91/// that can also detect JSON and YAML documents from their respected `serde` crate.
92#[cfg(not(feature = "file-format"))]
93pub fn default_resolver(data: &[u8]) -> Cow<'static, str> {
94    #[cfg(feature = "serde_json")]
95    if serde_json::from_slice::<serde_json::Value>(data).is_ok() {
96        // representing "true", "false", "null", "{any string}", "{any number}" should be plain text
97        match serde_json::from_slice(data).unwrap() {
98            serde_json::Value::String(_)
99            | serde_json::Value::Bool(_)
100            | serde_json::Value::Number(_)
101            | serde_json::Value::Null => return Cow::Borrowed("text/plain"),
102
103            serde_json::Value::Array(_) | serde_json::Value::Object(_) => {
104                return Cow::Borrowed("application/json; charset=utf-8")
105            }
106        }
107    }
108
109    #[cfg(feature = "serde_yaml_ng")]
110    if serde_yaml_ng::from_slice::<serde_yaml_ng::Value>(data).is_ok() {
111        fn match_value(value: &serde_yaml_ng::Value) -> Cow<'static, str> {
112            match value {
113                serde_yaml_ng::Value::Bool(_)
114                | serde_yaml_ng::Value::Number(_)
115                | serde_yaml_ng::Value::String(_)
116                | serde_yaml_ng::Value::Null => Cow::Borrowed("text/plain"),
117
118                serde_yaml_ng::Value::Tagged(m) => match_value(&m.value),
119                serde_yaml_ng::Value::Mapping(_) | serde_yaml_ng::Value::Sequence(_) => {
120                    Cow::Borrowed("text/yaml; charset=utf-8")
121                }
122            }
123        }
124
125        return match_value(&serde_yaml_ng::from_slice(data).unwrap());
126    }
127
128    DEFAULT_CONTENT_TYPE.into()
129}
130
131#[cfg(test)]
132#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
133mod tests {
134    use super::default_resolver;
135
136    #[cfg(feature = "file-format")]
137    #[test]
138    fn test_other_stuff() {
139        assert_eq!("text/plain", default_resolver(b"some plain text"));
140        assert_eq!("image/jpeg", default_resolver(&[0xFF, 0xD8, 0xFF, 0xAA]));
141    }
142
143    #[cfg(feature = "serde_json")]
144    #[test]
145    fn test_json() {
146        use serde_json::{json, to_vec};
147
148        for (value, assertion) in [
149            (json!(null), "text/plain"),
150            (json!(true), "text/plain"),
151            (json!(false), "text/plain"),
152            (json!("any string"), "text/plain"),
153            (json!(1.2), "text/plain"),
154            (json!({ "hello": "world" }), "application/json; charset=utf-8"),
155            (json!(["hello", "world"]), "application/json; charset=utf-8"),
156        ] {
157            assert_eq!(
158                assertion,
159                default_resolver(&to_vec(&value).expect("failed to convert to JSON"))
160            );
161        }
162    }
163
164    #[cfg(feature = "serde_yaml_ng")]
165    #[test]
166    fn test_yaml() {
167        for (value, assertion) in [
168            (serde_yaml_ng::Value::Null, "text/plain"),
169            (serde_yaml_ng::Value::Bool(true), "text/plain"),
170            (serde_yaml_ng::Value::Bool(false), "text/plain"),
171            (serde_yaml_ng::Value::String("hello world".into()), "text/plain"),
172            (serde_yaml_ng::Value::Number(1.into()), "text/plain"),
173            (
174                serde_yaml_ng::Value::Sequence(vec![serde_yaml_ng::Value::Bool(true)]),
175                "text/yaml; charset=utf-8",
176            ),
177            (
178                serde_yaml_ng::Value::Mapping({
179                    let mut map = serde_yaml_ng::Mapping::new();
180                    map.insert(
181                        serde_yaml_ng::Value::String("hello".into()),
182                        serde_yaml_ng::Value::String("world".into()),
183                    );
184
185                    map
186                }),
187                "text/yaml; charset=utf-8",
188            ),
189        ] {
190            assert_eq!(
191                assertion,
192                default_resolver(
193                    serde_yaml_ng::to_string(&value)
194                        .expect("failed to parse YAML")
195                        .as_bytes()
196                )
197            );
198        }
199    }
200}