Skip to main content

starnix_core/vfs/
fs_args.rs

1// Copyright 2023 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use crate::vfs::{FsStr, FsString};
6use flyweights::FlyByteStr;
7use starnix_uapi::errno;
8use starnix_uapi::errors::Errno;
9use starnix_uapi::mount_flags::MountFlags;
10use std::collections::HashMap;
11use std::fmt::Display;
12
13/// Parses a comma-separated list of options of the form `key` or `key=value` or `key="value"`.
14/// Commas and equals-signs are only permitted in the `key="value"` case. In the case of
15/// `key=value1,key=value2` collisions, the last value wins. Returns a hashmap of key/value pairs,
16/// or `EINVAL` in the case of malformed input. Note that no escape character sequence is supported,
17/// so values may not contain the `"` character.
18///
19/// # Examples
20///
21/// `key0=value0,key1,key2=value2,key0=value3` -> `map{"key0":"value3","key1":"","key2":"value2"}`
22///
23/// `key0=value0,key1="quoted,with=punc:tua-tion."` ->
24/// `map{"key0":"value0","key1":"quoted,with=punc:tua-tion."}`
25///
26/// `key0="mis"quoted,key2=unquoted` -> `EINVAL`
27#[derive(Debug, Default, Clone)]
28pub struct MountParams {
29    options: HashMap<FlyByteStr, FlyByteStr>,
30}
31
32impl MountParams {
33    pub fn parse(data: &FsStr) -> Result<Self, Errno> {
34        let options = parse_mount_options::parse_mount_options(data).map_err(|_| errno!(EINVAL))?;
35        Ok(MountParams { options })
36    }
37
38    pub fn keys(&self) -> impl Iterator<Item = &FlyByteStr> {
39        self.options.keys()
40    }
41
42    pub fn get(&self, key: &[u8]) -> Option<&FlyByteStr> {
43        self.options.get(&key.into())
44    }
45
46    pub fn get_as<T: std::str::FromStr>(&self, key: &[u8]) -> Result<Option<T>, Errno>
47    where
48        <T as std::str::FromStr>::Err: std::fmt::Debug,
49    {
50        self.get(key).map(|v| parse::<T>(v.as_ref())).transpose()
51    }
52
53    pub fn get_with<T, E: std::fmt::Debug>(
54        &self,
55        key: &[u8],
56        parser: impl FnOnce(&str) -> Result<T, E>,
57    ) -> Result<Option<T>, Errno> {
58        self.get(key).map(|v| parse_with(v.as_ref(), parser)).transpose()
59    }
60
61    pub fn remove(&mut self, key: &[u8]) -> Option<FlyByteStr> {
62        self.options.remove(&key.into())
63    }
64
65    pub fn is_empty(&self) -> bool {
66        self.options.is_empty()
67    }
68
69    pub fn remove_mount_flags(&mut self) -> MountFlags {
70        let mut flags = MountFlags::empty();
71        if self.remove(b"ro").is_some() {
72            flags |= MountFlags::RDONLY;
73        }
74        if self.remove(b"nosuid").is_some() {
75            flags |= MountFlags::NOSUID;
76        }
77        if self.remove(b"nodev").is_some() {
78            flags |= MountFlags::NODEV;
79        }
80        if self.remove(b"noexec").is_some() {
81            flags |= MountFlags::NOEXEC;
82        }
83        if self.remove(b"noatime").is_some() {
84            flags |= MountFlags::NOATIME;
85        }
86        if self.remove(b"nodiratime").is_some() {
87            flags |= MountFlags::NODIRATIME;
88        }
89        if self.remove(b"relatime").is_some() {
90            flags |= MountFlags::RELATIME;
91        }
92        if self.remove(b"strictatime").is_some() {
93            flags |= MountFlags::STRICTATIME;
94        }
95        flags
96    }
97
98    /// Formats the mount parameters as comma-separated key[=value] pairs, with each option
99    /// preceded by a leading comma (e.g. `,gid=3009,hidepid=2`). The order is unspecified.
100    pub fn format_options(&self) -> FsString {
101        let mut result = FsString::default();
102        for (key, value) in &self.options {
103            result.push(b',');
104            result.extend_from_slice(key.as_ref());
105            if !value.is_empty() {
106                result.push(b'=');
107                result.extend_from_slice(value.as_ref());
108            }
109        }
110        result
111    }
112}
113
114impl Display for MountParams {
115    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116        write!(f, "{}", itertools::join(self.options.iter().map(|(k, v)| format!("{k}={v}")), ","))
117    }
118}
119
120/// Parses `data` slice into another type.
121///
122/// This relies on str::parse so expects `data` to be utf8.
123pub fn parse<F: std::str::FromStr>(data: &FsStr) -> Result<F, Errno>
124where
125    <F as std::str::FromStr>::Err: std::fmt::Debug,
126{
127    parse_with(data, F::from_str)
128}
129
130/// Parses `data` slice into another type.
131///
132/// This relies on str::parse so expects `data` to be utf8.
133pub fn parse_with<F, E: std::fmt::Debug>(
134    data: &FsStr,
135    parser: impl FnOnce(&str) -> Result<F, E>,
136) -> Result<F, Errno> {
137    parser(std::str::from_utf8(data.as_ref()).map_err(|e| errno!(EINVAL, e))?.trim())
138        .map_err(|e| errno!(EINVAL, format!("{:?}: {:?}", data, e)))
139}
140
141mod parse_mount_options {
142    use crate::vfs::FsStr;
143    use flyweights::FlyByteStr;
144    use nom::branch::alt;
145    use nom::bytes::complete::{is_not, tag};
146    use nom::combinator::opt;
147    use nom::multi::separated_list0;
148    use nom::sequence::{delimited, separated_pair, terminated};
149    use nom::{IResult, Parser};
150    use starnix_uapi::errors::{Errno, errno, error};
151    use std::collections::HashMap;
152
153    fn unquoted(input: &[u8]) -> IResult<&[u8], &[u8]> {
154        is_not(",=").parse(input)
155    }
156
157    fn quoted(input: &[u8]) -> IResult<&[u8], &[u8]> {
158        delimited(tag("\""), is_not("\""), tag("\"")).parse(input)
159    }
160
161    fn value(input: &[u8]) -> IResult<&[u8], &[u8]> {
162        alt((quoted, unquoted)).parse(input)
163    }
164
165    fn key_value(input: &[u8]) -> IResult<&[u8], (&[u8], &[u8])> {
166        separated_pair(unquoted, tag("="), value).parse(input)
167    }
168
169    fn key_only(input: &[u8]) -> IResult<&[u8], (&[u8], &[u8])> {
170        let (input, key) = unquoted(input)?;
171        Ok((input, (key, b"")))
172    }
173
174    fn option(input: &[u8]) -> IResult<&[u8], (&[u8], &[u8])> {
175        alt((key_value, key_only)).parse(input)
176    }
177
178    pub(super) fn parse_mount_options(
179        input: &FsStr,
180    ) -> Result<HashMap<FlyByteStr, FlyByteStr>, Errno> {
181        let (input, options) = terminated(separated_list0(tag(","), option), opt(tag(",")))
182            .parse(input.into())
183            .map_err(|_| errno!(EINVAL))?;
184
185        // `[...],last_key="mis"quoted` not allowed.
186        if input.len() > 0 {
187            return error!(EINVAL);
188        }
189
190        // Insert in-order so that last `key=value` containing `key` "wins".
191        let mut options_map: HashMap<FlyByteStr, FlyByteStr> =
192            HashMap::with_capacity(options.len());
193        for (key, value) in options.into_iter() {
194            options_map.insert(key.into(), value.into());
195        }
196
197        Ok(options_map)
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use super::{MountParams, parse};
204    use crate::vfs::FsStr;
205    use flyweights::FlyByteStr;
206    use maplit::hashmap;
207    use starnix_uapi::mount_flags::MountFlags;
208    use std::collections::HashSet;
209
210    #[::fuchsia::test]
211    fn empty_data() {
212        assert!(MountParams::parse(Default::default()).unwrap().is_empty());
213    }
214
215    #[::fuchsia::test]
216    fn parse_options_with_trailing_comma() {
217        let data = b"key0=value0,";
218        let parsed_data =
219            MountParams::parse(data.into()).expect("mount options parse:  key0=value0,");
220        assert_eq!(
221            parsed_data.options,
222            hashmap! {
223                FlyByteStr::new(b"key0") => FlyByteStr::new(b"value0"),
224            }
225        );
226    }
227
228    #[::fuchsia::test]
229    fn parse_options_last_value_wins() {
230        // Repeat key `key0`.
231        let data = b"key0=value0,key1,key2=value2,key0=value3";
232        let parsed_data = MountParams::parse(data.into())
233            .expect("mount options parse:  key0=value0,key1,key2=value2,key0=value3");
234        assert_eq!(
235            parsed_data.options,
236            hashmap! {
237                FlyByteStr::new(b"key1") => FlyByteStr::new(b""),
238                FlyByteStr::new(b"key2") => FlyByteStr::new(b"value2"),
239                // Last `key0` value in list "wins":
240                FlyByteStr::new(b"key0") => FlyByteStr::new(b"value3"),
241            }
242        );
243    }
244
245    #[::fuchsia::test]
246    fn parse_options_quoted() {
247        let data = b"key0=unqouted,key1=\"quoted,with=punc:tua-tion.\"";
248        let parsed_data = MountParams::parse(data.into())
249            .expect("mount options parse:  key0=value0,key1,key2=value2,key0=value3");
250        assert_eq!(
251            parsed_data.options,
252            hashmap! {
253                FlyByteStr::new(b"key0") => FlyByteStr::new(b"unqouted"),
254                FlyByteStr::new(b"key1") => FlyByteStr::new(b"quoted,with=punc:tua-tion."),
255            }
256        );
257    }
258
259    #[::fuchsia::test]
260    fn parse_options_misquoted() {
261        let data = b"key0=\"mis\"quoted,key1=\"quoted\"";
262        let parse_result = MountParams::parse(data.into());
263        assert!(
264            parse_result.is_err(),
265            "expected parse failure:  key0=\"mis\"quoted,key1=\"quoted\""
266        );
267    }
268
269    #[::fuchsia::test]
270    fn parse_options_misquoted_tail() {
271        let data = b"key0=\"quoted\",key1=\"mis\"quoted";
272        let parse_result = MountParams::parse(data.into());
273        assert!(
274            parse_result.is_err(),
275            "expected parse failure:  key0=\"quoted\",key1=\"mis\"quoted"
276        );
277    }
278
279    #[::fuchsia::test]
280    fn parse_normal_mount_flags() {
281        let data = b"nosuid,nodev,noexec,relatime";
282        let parsed_data = MountParams::parse(data.into())
283            .expect("mount options parse:  nosuid,nodev,noexec,relatime");
284        assert_eq!(
285            parsed_data.options,
286            hashmap! {
287                FlyByteStr::new(b"nosuid") => FlyByteStr::default(),
288                FlyByteStr::new(b"nodev") => FlyByteStr::default(),
289                FlyByteStr::new(b"noexec") => FlyByteStr::default(),
290                FlyByteStr::new(b"relatime") => FlyByteStr::default(),
291            }
292        );
293    }
294
295    #[::fuchsia::test]
296    fn parse_and_remove_normal_mount_flags() {
297        let data = b"nosuid,nodev,noexec,relatime";
298        let mut parsed_data = MountParams::parse(data.into())
299            .expect("mount options parse:  nosuid,nodev,noexec,relatime");
300        let flags = parsed_data.remove_mount_flags();
301        assert_eq!(
302            flags,
303            MountFlags::NOSUID | MountFlags::NODEV | MountFlags::NOEXEC | MountFlags::RELATIME
304        );
305    }
306
307    #[::fuchsia::test]
308    fn parse_data() {
309        assert_eq!(parse::<usize>("42".into()), Ok(42));
310    }
311
312    #[::fuchsia::test]
313    fn test_format_options() {
314        // Splits `,a,b=c` into {"a", "b=c"}, since the order of options is unspecified.
315        fn split(options: &FsStr) -> HashSet<&[u8]> {
316            let bytes: &[u8] = options;
317            let bytes = bytes.strip_prefix(b",").expect("leading comma");
318            bytes.split(|&b| b == b',').collect()
319        }
320
321        let empty = MountParams::default();
322        assert_eq!(empty.format_options(), "");
323
324        let data = b"hidepid=2,gid=3009";
325        let parsed = MountParams::parse(data.into()).expect("parse options");
326        assert_eq!(
327            split(parsed.format_options().as_ref()),
328            HashSet::from([&b"gid=3009"[..], &b"hidepid=2"[..]])
329        );
330
331        let flags_data = b"nodev,nosuid,custom=opt";
332        let mut parsed = MountParams::parse(flags_data.into()).expect("parse options");
333        let _ = parsed.remove_mount_flags();
334        assert_eq!(parsed.format_options(), ",custom=opt");
335    }
336}