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;
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
99impl Display for MountParams {
100    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101        write!(f, "{}", itertools::join(self.options.iter().map(|(k, v)| format!("{k}={v}")), ","))
102    }
103}
104
105/// Parses `data` slice into another type.
106///
107/// This relies on str::parse so expects `data` to be utf8.
108pub fn parse<F: std::str::FromStr>(data: &FsStr) -> Result<F, Errno>
109where
110    <F as std::str::FromStr>::Err: std::fmt::Debug,
111{
112    parse_with(data, F::from_str)
113}
114
115/// Parses `data` slice into another type.
116///
117/// This relies on str::parse so expects `data` to be utf8.
118pub fn parse_with<F, E: std::fmt::Debug>(
119    data: &FsStr,
120    parser: impl FnOnce(&str) -> Result<F, E>,
121) -> Result<F, Errno> {
122    parser(std::str::from_utf8(data.as_ref()).map_err(|e| errno!(EINVAL, e))?.trim())
123        .map_err(|e| errno!(EINVAL, format!("{:?}: {:?}", data, e)))
124}
125
126mod parse_mount_options {
127    use crate::vfs::FsStr;
128    use flyweights::FlyByteStr;
129    use nom::branch::alt;
130    use nom::bytes::complete::{is_not, tag};
131    use nom::combinator::opt;
132    use nom::multi::separated_list0;
133    use nom::sequence::{delimited, separated_pair, terminated};
134    use nom::{IResult, Parser};
135    use starnix_uapi::errors::{Errno, errno, error};
136    use std::collections::HashMap;
137
138    fn unquoted(input: &[u8]) -> IResult<&[u8], &[u8]> {
139        is_not(",=").parse(input)
140    }
141
142    fn quoted(input: &[u8]) -> IResult<&[u8], &[u8]> {
143        delimited(tag("\""), is_not("\""), tag("\"")).parse(input)
144    }
145
146    fn value(input: &[u8]) -> IResult<&[u8], &[u8]> {
147        alt((quoted, unquoted)).parse(input)
148    }
149
150    fn key_value(input: &[u8]) -> IResult<&[u8], (&[u8], &[u8])> {
151        separated_pair(unquoted, tag("="), value).parse(input)
152    }
153
154    fn key_only(input: &[u8]) -> IResult<&[u8], (&[u8], &[u8])> {
155        let (input, key) = unquoted(input)?;
156        Ok((input, (key, b"")))
157    }
158
159    fn option(input: &[u8]) -> IResult<&[u8], (&[u8], &[u8])> {
160        alt((key_value, key_only)).parse(input)
161    }
162
163    pub(super) fn parse_mount_options(
164        input: &FsStr,
165    ) -> Result<HashMap<FlyByteStr, FlyByteStr>, Errno> {
166        let (input, options) = terminated(separated_list0(tag(","), option), opt(tag(",")))
167            .parse(input.into())
168            .map_err(|_| errno!(EINVAL))?;
169
170        // `[...],last_key="mis"quoted` not allowed.
171        if input.len() > 0 {
172            return error!(EINVAL);
173        }
174
175        // Insert in-order so that last `key=value` containing `key` "wins".
176        let mut options_map: HashMap<FlyByteStr, FlyByteStr> =
177            HashMap::with_capacity(options.len());
178        for (key, value) in options.into_iter() {
179            options_map.insert(key.into(), value.into());
180        }
181
182        Ok(options_map)
183    }
184}
185
186#[cfg(test)]
187mod tests {
188    use super::{MountParams, parse};
189    use flyweights::FlyByteStr;
190    use maplit::hashmap;
191    use starnix_uapi::mount_flags::MountFlags;
192
193    #[::fuchsia::test]
194    fn empty_data() {
195        assert!(MountParams::parse(Default::default()).unwrap().is_empty());
196    }
197
198    #[::fuchsia::test]
199    fn parse_options_with_trailing_comma() {
200        let data = b"key0=value0,";
201        let parsed_data =
202            MountParams::parse(data.into()).expect("mount options parse:  key0=value0,");
203        assert_eq!(
204            parsed_data.options,
205            hashmap! {
206                FlyByteStr::new(b"key0") => FlyByteStr::new(b"value0"),
207            }
208        );
209    }
210
211    #[::fuchsia::test]
212    fn parse_options_last_value_wins() {
213        // Repeat key `key0`.
214        let data = b"key0=value0,key1,key2=value2,key0=value3";
215        let parsed_data = MountParams::parse(data.into())
216            .expect("mount options parse:  key0=value0,key1,key2=value2,key0=value3");
217        assert_eq!(
218            parsed_data.options,
219            hashmap! {
220                FlyByteStr::new(b"key1") => FlyByteStr::new(b""),
221                FlyByteStr::new(b"key2") => FlyByteStr::new(b"value2"),
222                // Last `key0` value in list "wins":
223                FlyByteStr::new(b"key0") => FlyByteStr::new(b"value3"),
224            }
225        );
226    }
227
228    #[::fuchsia::test]
229    fn parse_options_quoted() {
230        let data = b"key0=unqouted,key1=\"quoted,with=punc:tua-tion.\"";
231        let parsed_data = MountParams::parse(data.into())
232            .expect("mount options parse:  key0=value0,key1,key2=value2,key0=value3");
233        assert_eq!(
234            parsed_data.options,
235            hashmap! {
236                FlyByteStr::new(b"key0") => FlyByteStr::new(b"unqouted"),
237                FlyByteStr::new(b"key1") => FlyByteStr::new(b"quoted,with=punc:tua-tion."),
238            }
239        );
240    }
241
242    #[::fuchsia::test]
243    fn parse_options_misquoted() {
244        let data = b"key0=\"mis\"quoted,key1=\"quoted\"";
245        let parse_result = MountParams::parse(data.into());
246        assert!(
247            parse_result.is_err(),
248            "expected parse failure:  key0=\"mis\"quoted,key1=\"quoted\""
249        );
250    }
251
252    #[::fuchsia::test]
253    fn parse_options_misquoted_tail() {
254        let data = b"key0=\"quoted\",key1=\"mis\"quoted";
255        let parse_result = MountParams::parse(data.into());
256        assert!(
257            parse_result.is_err(),
258            "expected parse failure:  key0=\"quoted\",key1=\"mis\"quoted"
259        );
260    }
261
262    #[::fuchsia::test]
263    fn parse_normal_mount_flags() {
264        let data = b"nosuid,nodev,noexec,relatime";
265        let parsed_data = MountParams::parse(data.into())
266            .expect("mount options parse:  nosuid,nodev,noexec,relatime");
267        assert_eq!(
268            parsed_data.options,
269            hashmap! {
270                FlyByteStr::new(b"nosuid") => FlyByteStr::default(),
271                FlyByteStr::new(b"nodev") => FlyByteStr::default(),
272                FlyByteStr::new(b"noexec") => FlyByteStr::default(),
273                FlyByteStr::new(b"relatime") => FlyByteStr::default(),
274            }
275        );
276    }
277
278    #[::fuchsia::test]
279    fn parse_and_remove_normal_mount_flags() {
280        let data = b"nosuid,nodev,noexec,relatime";
281        let mut parsed_data = MountParams::parse(data.into())
282            .expect("mount options parse:  nosuid,nodev,noexec,relatime");
283        let flags = parsed_data.remove_mount_flags();
284        assert_eq!(
285            flags,
286            MountFlags::NOSUID | MountFlags::NODEV | MountFlags::NOEXEC | MountFlags::RELATIME
287        );
288    }
289
290    #[::fuchsia::test]
291    fn parse_data() {
292        assert_eq!(parse::<usize>("42".into()), Ok(42));
293    }
294}