1fn is_valid_sh_var_name(s: &str) -> bool {
6 let mut bytes = s.bytes();
7 let Some(b'A'..=b'Z' | b'a'..=b'z' | b'_') = bytes.next() else {
8 return false;
9 };
10 bytes.all(|b| matches!(b, b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'_'))
11}
12
13fn write_shell_escaped_single_quoted<W>(f: &mut W, s: &str) -> std::fmt::Result
15where
16 W: std::fmt::Write,
17{
18 write!(f, "'")?;
19 for c in s.chars() {
20 if c == '\'' {
21 write!(f, "'\\''")?;
22 } else {
23 write!(f, "{c}")?;
24 }
25 }
26 write!(f, "'")?;
27 Ok(())
28}
29
30fn is_good_simple_value(v: &serde_json::Value) -> bool {
33 match v {
34 serde_json::Value::Null | serde_json::Value::Bool(_) | serde_json::Value::String(_) => true,
35 serde_json::Value::Number(number) => {
36 if number.as_i64().is_some() || number.as_u64().is_some() {
37 true
38 } else if let Some(n) = number.as_f64() {
39 n.ceil() == n
40 } else if number.as_i128().is_some() {
41 true
42 } else {
43 number.as_u128().is_some()
44 }
45 }
46 serde_json::Value::Array(_) | serde_json::Value::Object(_) => {
47 unreachable!("Snix bug: called write_simple_type on complex type")
48 }
49 }
50}
51
52fn write_simple_type<W>(f: &mut W, v: serde_json::Value) -> std::fmt::Result
53where
54 W: std::fmt::Write,
55{
56 match v {
57 serde_json::Value::Null => write!(f, "''")?,
58 serde_json::Value::Bool(v) => {
59 if v {
60 write!(f, "1")?;
61 } else {
62 write!(f, "")?;
63 }
64 }
65 serde_json::Value::Number(number) => {
66 if let Some(n) = number.as_i64() {
67 write!(f, "{n}")?;
68 } else if let Some(n) = number.as_u64() {
69 write!(f, "{n}")?;
70 } else if let Some(n) = number.as_f64() {
71 debug_assert!(n.ceil() == n, "bad number value");
72 write!(f, "{}", n.ceil() as i64)?;
73 } else if let Some(n) = number.as_i128() {
74 write!(f, "{n}")?;
75 } else if let Some(n) = number.as_u128() {
76 write!(f, "{n}")?;
77 } else {
78 panic!("unable to represent number");
79 }
80 }
81 serde_json::Value::String(s) => {
82 write_shell_escaped_single_quoted(f, &s)?;
83 }
84 serde_json::Value::Array(_) | serde_json::Value::Object(_) => {
85 unreachable!("Snix bug: called write_simple_type on complex type")
86 }
87 }
88
89 Ok(())
90}
91
92pub fn write_attrs_sh_file<W>(
95 f: &mut W,
96 map: serde_json::Map<String, serde_json::Value>,
97) -> std::fmt::Result
98where
99 W: std::fmt::Write,
100{
101 for (k, v) in map {
102 if !is_valid_sh_var_name(k.as_str()) {
106 continue;
107 }
108 match v {
109 serde_json::Value::Null | serde_json::Value::Bool(_) | serde_json::Value::String(_) => {
110 write!(f, "declare {k}=")?;
111 write_simple_type(f, v)?;
112 writeln!(f)?;
113 }
114 serde_json::Value::Number(_) => {
115 if !is_good_simple_value(&v) {
117 continue;
118 }
119 write!(f, "declare {k}=")?;
120 write_simple_type(f, v)?;
121 writeln!(f)?;
122 }
123 serde_json::Value::Array(values) => {
124 if values.iter().any(|v| !is_good_simple_value(v)) {
126 continue;
127 }
128 write!(f, "declare -a {k}=(")?;
129 for val in values {
130 write_simple_type(f, val)?;
131 write!(f, " ")?;
132 }
133 writeln!(f, ")")?;
134 }
135 serde_json::Value::Object(map) => {
136 if map.values().any(|v| {
139 matches!(
140 v,
141 serde_json::Value::Array(_) | serde_json::Value::Object(_)
142 ) || !is_good_simple_value(v)
143 }) {
144 continue;
145 }
146
147 write!(f, "declare -A {k}=(")?;
148 for (k, v) in map {
149 write!(f, "[")?;
153 write_shell_escaped_single_quoted(f, &k)?;
154 write!(f, "]=")?;
155 write_simple_type(f, v)?;
156 write!(f, " ")?;
157 }
158 writeln!(f, ")")?;
159 }
160 }
161 }
162
163 Ok(())
164}
165
166#[cfg(test)]
167mod test {
168 use rstest::rstest;
169 use serde_json::json;
170
171 use super::write_attrs_sh_file;
172
173 #[rstest]
174 #[case::empty(json!({}), "")]
175 #[case::empty_key(json!({"": "value"}), "")]
176 #[case::null(json!({"k": null}), r#"declare k=''"#)]
177 #[case::string(json!({"k":"v"}), r#"declare k='v'"#)]
178 #[case::string_escaping(json!({"k":"v'w"}), r#"declare k='v'\''w'"#)]
179 #[case::bool_false(json!({"k":false}), r#"declare k="#)]
180 #[case::bool_true(json!({"k":true}), r#"declare k=1"#)]
181 #[case::number(json!({"k":1}), r#"declare k=1"#)]
182 #[case::number_float(json!({"k":1.0}), r#"declare k=1"#)]
183 #[case::number_float_invalid(json!({"k":1.1}), r#""#)]
184 #[case::array_of_strings(json!({"k": ["bar", "baz"]}), r#"declare -a k=('bar' 'baz' )"#)]
185 #[case::array_of_strings_and_bool(json!({"k": ["bar", true]}), r#"declare -a k=('bar' 1 )"#)]
186 #[case::array_of_strings_and_invalid_number(json!({"k": ["bar", 1.1]}), "")]
187 #[case::object_key_escaping(json!(
188 {"k": {"it's": "v"}}), r#"declare -A k=(['it'\''s']='v' )"#)]
189 #[case::object(json!(
190 {"k": {
191 "bar": true,
192 "b": 1.0,
193 "c": false,
194 "d": true,
195 }}), r#"declare -A k=(['b']=1 ['bar']=1 ['c']= ['d']=1 )"#)]
196 #[case::object_invalid_number(json!(
197 {"k": {
198 "bar": true,
199 "b": 1.1,
200 }}), "")]
201 #[case::object_too_complex(json!(
202 {"k": {
203 "bar": true,
204 "baz": [],
205 }}), r#""#)]
206 #[case::multiple(json!(
207 {
208 "k": {
209 "bar": true,
210 "b": 1.0,
211 "c": false,
212 "d": true,
213 },
214 "l": 42,
215 "m": false,
216 "n": 1.1,
217 }),
218 r#"declare -A k=(['b']=1 ['bar']=1 ['c']= ['d']=1 )
219declare l=42
220declare m="#)]
221 fn write_attrs(#[case] val: serde_json::Value, #[case] exp_output: &str) {
222 let mut out = String::new();
223 let map = val.as_object().expect("must be map").to_owned();
224 write_attrs_sh_file(&mut out, map).expect("must succeed");
225
226 if exp_output.is_empty() {
227 assert_eq!(exp_output, out, "expected output to match");
228 } else {
229 assert_eq!(
230 {
231 let mut exp_output = String::from(exp_output);
232 exp_output.push('\n');
233 exp_output
234 },
235 out,
236 "expected output to match"
237 );
238 }
239 }
240}