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