openvpn_plugin/ffi/
parse.rs

1// Copyright 2023 Mullvad VPN AB.
2//
3// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
5// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
6// option. This file may not be copied, modified, or distributed
7// except according to those terms.
8
9use std::collections::HashMap;
10use std::error::Error;
11use std::ffi::{CStr, CString};
12use std::fmt;
13use std::os::raw::c_char;
14use std::str::Utf8Error;
15
16/// Error type returned by the ffi parsing functions if the input data is invalid in some way.
17#[derive(Debug, Clone, Eq, PartialEq, Hash)]
18pub enum ParseError {
19    /// Given pointer is a null pointer.
20    NullPtr,
21    /// A string in the environment has no '=' char in it, and is thus not a valid environment
22    /// entry.
23    NoEqual(CString),
24}
25
26impl fmt::Display for ParseError {
27    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
28        match *self {
29            ParseError::NullPtr => "Input is null pointer".fmt(f),
30            ParseError::NoEqual(ref s) => write!(f, "No equal sign in \"{}\"", s.to_string_lossy()),
31        }
32    }
33}
34
35impl Error for ParseError {}
36
37
38/// Parses a null-terminated C string array into a Vec<CString> for safe usage.
39///
40/// Returns an Err if given a null pointer.
41///
42/// # Segfaults
43///
44/// Can cause the program to crash if the pointer array starting at `ptr` is not correctly null
45/// terminated. Likewise, if any string pointed to is not properly null-terminated it may crash.
46pub unsafe fn string_array(mut ptr: *const *const c_char) -> Result<Vec<CString>, ParseError> {
47    if ptr.is_null() {
48        Err(ParseError::NullPtr)
49    } else {
50        let mut strings = Vec::new();
51        while !(*ptr).is_null() {
52            strings.push(CStr::from_ptr(*ptr).to_owned());
53            ptr = ptr.offset(1);
54        }
55        Ok(strings)
56    }
57}
58
59/// Convenience method for plugins to convert the C string arrays they are given into real Rust
60/// strings.
61pub fn string_array_utf8(strings: &[CString]) -> Result<Vec<String>, Utf8Error> {
62    strings
63        .iter()
64        .map(|s| s.to_str().map(|s| s.to_owned()))
65        .collect()
66}
67
68
69/// Parses a null-terminated array of C strings with "=" delimiters into a key-value map.
70///
71/// The input environment has to contain null-terminated strings containing at least
72/// one equal sign ("="). Every string is split at the first equal sign and added to the map with
73/// the first part being the key and the second the value.
74///
75/// If multiple entries have the same key, the last one will be in the result map.
76///
77/// # Segfaults
78///
79/// Uses `string_array` internally and will segfault for the same reasons as that function.
80pub unsafe fn env(envptr: *const *const c_char) -> Result<HashMap<CString, CString>, ParseError> {
81    let mut map = HashMap::new();
82    for string in string_array(envptr)? {
83        let string_bytes = string.as_bytes();
84        let equal_index = string_bytes
85            .iter()
86            .position(|&c| c == b'=')
87            .ok_or_else(|| ParseError::NoEqual(string.clone()))?;
88
89        // It's safe to unwrap since CString guarantees no null bytes.
90        let key = CString::new(&string_bytes[..equal_index]).unwrap();
91        let value = CString::new(&string_bytes[equal_index + 1..]).unwrap();
92        map.insert(key, value);
93    }
94    Ok(map)
95}
96
97/// Convenience method for plugins to convert the environments given to them into Rust String based
98/// environments.
99pub fn env_utf8(env: &HashMap<CString, CString>) -> Result<HashMap<String, String>, Utf8Error> {
100    let mut output_env = HashMap::with_capacity(env.len());
101    for (key, value) in env {
102        output_env.insert(key.to_str()?.to_owned(), value.to_str()?.to_owned());
103    }
104    Ok(output_env)
105}
106
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111    use std::os::raw::c_char;
112    use std::ptr;
113
114    #[test]
115    fn string_array_null() {
116        assert_eq!(Err(ParseError::NullPtr), unsafe {
117            string_array(ptr::null())
118        });
119    }
120
121    #[test]
122    fn string_array_empty() {
123        let ptr_arr = [ptr::null()];
124        let result = unsafe { string_array(&ptr_arr as *const *const c_char).unwrap() };
125        assert!(result.is_empty());
126    }
127
128    #[test]
129    fn string_array_no_space_trim() {
130        let test_str = " foobar \0";
131        let ptr_arr = [test_str as *const _ as *const c_char, ptr::null()];
132        let result = unsafe { string_array(&ptr_arr as *const *const c_char).unwrap() };
133        assert_eq!([CString::new(" foobar ").unwrap()], &result[..]);
134    }
135
136    #[test]
137    fn string_array_two_strings() {
138        let test_str1 = "foo\0";
139        let test_str2 = "bar\0";
140        let ptr_arr = [
141            test_str1 as *const _ as *const c_char,
142            test_str2 as *const _ as *const c_char,
143            ptr::null(),
144        ];
145        let result = unsafe { string_array(&ptr_arr as *const *const c_char).unwrap() };
146        assert_eq!(
147            [CString::new("foo").unwrap(), CString::new("bar").unwrap()],
148            &result[..]
149        );
150    }
151
152    #[test]
153    fn string_array_utf8_happy_path() {
154        let array = &[CString::new("foo").unwrap(), CString::new("bar").unwrap()];
155        let result = string_array_utf8(array).unwrap();
156        assert_eq!(vec!["foo", "bar"], result);
157    }
158
159    #[test]
160    fn string_array_utf8_invalid() {
161        // 192 is not a valid utf8 byte
162        let array = &[CString::new(vec![192]).unwrap()];
163        assert!(string_array_utf8(array).is_err());
164    }
165
166    #[test]
167    fn env_one_value() {
168        let test_str = "var_a=value_b\0";
169        let key = CString::new("var_a").unwrap();
170        let value = CString::new("value_b").unwrap();
171
172        let ptr_arr = [test_str as *const _ as *const c_char, ptr::null()];
173        let result = unsafe { env(&ptr_arr as *const *const c_char).unwrap() };
174        assert_eq!(1, result.len());
175        assert_eq!(Some(&value), result.get(&key));
176    }
177
178    #[test]
179    fn env_no_equal() {
180        let test_str = "foobar\0";
181        let ptr_arr = [test_str as *const _ as *const c_char, ptr::null()];
182        let result = unsafe { env(&ptr_arr as *const *const c_char) };
183        assert_eq!(
184            result,
185            Err(ParseError::NoEqual(CString::new("foobar").unwrap()))
186        );
187    }
188
189    #[test]
190    fn env_double_equal() {
191        let test_str = "foo=bar=baz\0";
192        let key = CString::new("foo").unwrap();
193        let value = CString::new("bar=baz").unwrap();
194
195        let ptr_arr = [test_str as *const _ as *const c_char, ptr::null()];
196        let env = unsafe { env(&ptr_arr as *const *const c_char).unwrap() };
197        assert_eq!(1, env.len());
198        assert_eq!(Some(&value), env.get(&key));
199    }
200
201    #[test]
202    fn env_two_same_key() {
203        let test_str1 = "foo=123\0";
204        let test_str2 = "foo=abc\0";
205        let ptr_arr = [
206            test_str1 as *const _ as *const c_char,
207            test_str2 as *const _ as *const c_char,
208            ptr::null(),
209        ];
210        let key = CString::new("foo").unwrap();
211        let value = CString::new("abc").unwrap();
212
213        let env = unsafe { env(&ptr_arr as *const *const c_char).unwrap() };
214        assert_eq!(1, env.len());
215        assert_eq!(Some(&value), env.get(&key));
216    }
217
218    #[test]
219    fn env_utf8_happy_path() {
220        let mut env = HashMap::new();
221        env.insert(CString::new("foo").unwrap(), CString::new("bar").unwrap());
222        env.insert(CString::new("baz").unwrap(), CString::new("123").unwrap());
223        let result = env_utf8(&env).unwrap();
224        assert_eq!("bar", result.get("foo").unwrap());
225        assert_eq!("123", result.get("baz").unwrap());
226    }
227
228    #[test]
229    fn env_utf8_invalid() {
230        // 192 is not a valid utf8 byte
231        let mut env = HashMap::new();
232        env.insert(
233            CString::new(vec![192]).unwrap(),
234            CString::new("bar").unwrap(),
235        );
236        assert!(env_utf8(&env).is_err());
237    }
238}