@@ -137,6 +137,28 @@ fn parse_change(mode: &str, fperm: u32, considering_dir: bool) -> (u32, usize) {
137137 ( srwx, pos)
138138}
139139
140+ /// Takes a user-supplied string and tries to parse to u16 mode bitmask.
141+ /// Supports comma-separated mode strings like "ug+rwX,o+rX" (same as chmod).
142+ pub fn parse ( mode_string : & str , considering_dir : bool , umask : u32 ) -> Result < u32 , String > {
143+ // Split by commas and process each mode part sequentially
144+ let mut current_mode: u32 = 0 ;
145+
146+ for mode_part in mode_string. split ( ',' ) {
147+ let mode_part = mode_part. trim ( ) ;
148+ if mode_part. is_empty ( ) {
149+ continue ;
150+ }
151+
152+ current_mode = if mode_part. chars ( ) . any ( |c| c. is_ascii_digit ( ) ) {
153+ parse_numeric ( current_mode, mode_part, considering_dir) ?
154+ } else {
155+ parse_symbolic ( current_mode, mode_part, umask, considering_dir) ?
156+ } ;
157+ }
158+
159+ Ok ( current_mode)
160+ }
161+
140162#[ allow( clippy:: unnecessary_cast) ]
141163pub fn parse_mode ( mode : & str ) -> Result < mode_t , String > {
142164 let mut new_mode = ( S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH ) as u32 ;
@@ -178,7 +200,9 @@ pub fn get_umask() -> u32 {
178200}
179201
180202#[ cfg( test) ]
181- mod test {
203+ mod tests {
204+
205+ use super :: parse;
182206
183207 #[ test]
184208 fn symbolic_modes ( ) {
@@ -199,7 +223,121 @@ mod test {
199223 }
200224
201225 #[ test]
202- fn multiple_modes ( ) {
203- assert_eq ! ( super :: parse_mode( "+100,+010" ) . unwrap( ) , 0o776 ) ;
226+ fn test_parse_numeric_mode ( ) {
227+ // Simple numeric mode
228+ assert_eq ! ( parse( "644" , false , 0 ) . unwrap( ) , 0o644 ) ;
229+ assert_eq ! ( parse( "755" , false , 0 ) . unwrap( ) , 0o755 ) ;
230+ assert_eq ! ( parse( "777" , false , 0 ) . unwrap( ) , 0o777 ) ;
231+ assert_eq ! ( parse( "600" , false , 0 ) . unwrap( ) , 0o600 ) ;
232+ }
233+
234+ #[ test]
235+ fn test_parse_numeric_mode_with_operator ( ) {
236+ // Numeric mode with + operator
237+ assert_eq ! ( parse( "+100" , false , 0 ) . unwrap( ) , 0o100 ) ;
238+ assert_eq ! ( parse( "+644" , false , 0 ) . unwrap( ) , 0o644 ) ;
239+
240+ // Numeric mode with - operator (starting from 0, so nothing to remove)
241+ assert_eq ! ( parse( "-4" , false , 0 ) . unwrap( ) , 0 ) ;
242+ // But if we first set a mode, then remove bits
243+ assert_eq ! ( parse( "644,-4" , false , 0 ) . unwrap( ) , 0o640 ) ;
244+ }
245+
246+ #[ test]
247+ fn test_parse_symbolic_mode ( ) {
248+ // Simple symbolic modes
249+ assert_eq ! ( parse( "u+x" , false , 0 ) . unwrap( ) , 0o100 ) ;
250+ assert_eq ! ( parse( "g+w" , false , 0 ) . unwrap( ) , 0o020 ) ;
251+ assert_eq ! ( parse( "o+r" , false , 0 ) . unwrap( ) , 0o004 ) ;
252+ assert_eq ! ( parse( "a+x" , false , 0 ) . unwrap( ) , 0o111 ) ;
253+ }
254+
255+ #[ test]
256+ fn test_parse_symbolic_mode_multiple_permissions ( ) {
257+ // Multiple permissions in one mode
258+ assert_eq ! ( parse( "u+rw" , false , 0 ) . unwrap( ) , 0o600 ) ;
259+ assert_eq ! ( parse( "ug+rwx" , false , 0 ) . unwrap( ) , 0o770 ) ;
260+ assert_eq ! ( parse( "a+rwx" , false , 0 ) . unwrap( ) , 0o777 ) ;
261+ }
262+
263+ #[ test]
264+ fn test_parse_comma_separated_modes ( ) {
265+ // Comma-separated mode strings (as mentioned in the doc comment)
266+ assert_eq ! ( parse( "ug+rwX,o+rX" , false , 0 ) . unwrap( ) , 0o664 ) ;
267+ assert_eq ! ( parse( "u+rwx,g+rx,o+r" , false , 0 ) . unwrap( ) , 0o754 ) ;
268+ assert_eq ! ( parse( "u+w,g+w,o+w" , false , 0 ) . unwrap( ) , 0o222 ) ;
269+ }
270+
271+ #[ test]
272+ fn test_parse_comma_separated_with_spaces ( ) {
273+ // Comma-separated with spaces (should be trimmed)
274+ assert_eq ! ( parse( "u+rw, g+rw, o+r" , false , 0 ) . unwrap( ) , 0o664 ) ;
275+ assert_eq ! ( parse( " u+x , g+x " , false , 0 ) . unwrap( ) , 0o110 ) ;
276+ }
277+
278+ #[ test]
279+ fn test_parse_mixed_numeric_and_symbolic ( ) {
280+ // Mix of numeric and symbolic modes
281+ assert_eq ! ( parse( "644,u+x" , false , 0 ) . unwrap( ) , 0o744 ) ;
282+ assert_eq ! ( parse( "u+rw,755" , false , 0 ) . unwrap( ) , 0o755 ) ;
283+ }
284+
285+ #[ test]
286+ fn test_parse_empty_string ( ) {
287+ // Empty string should return 0
288+ assert_eq ! ( parse( "" , false , 0 ) . unwrap( ) , 0 ) ;
289+ assert_eq ! ( parse( " " , false , 0 ) . unwrap( ) , 0 ) ;
290+ assert_eq ! ( parse( ",," , false , 0 ) . unwrap( ) , 0 ) ;
291+ }
292+
293+ #[ test]
294+ fn test_parse_with_umask ( ) {
295+ // Test with umask (affects symbolic modes when no level is specified)
296+ let umask = 0o022 ;
297+ assert_eq ! ( parse( "+w" , false , umask) . unwrap( ) , 0o200 ) ;
298+ // The umask should be respected for symbolic modes without explicit level
299+ }
300+
301+ #[ test]
302+ fn test_parse_considering_dir ( ) {
303+ // Test directory vs file mode differences
304+ // For directories, X (capital X) should add execute permission
305+ assert_eq ! ( parse( "a+X" , true , 0 ) . unwrap( ) , 0o111 ) ;
306+ // For files without execute, X should not add execute
307+ assert_eq ! ( parse( "a+X" , false , 0 ) . unwrap( ) , 0o000 ) ;
308+
309+ // Numeric modes for directories preserve setuid/setgid bits
310+ assert_eq ! ( parse( "755" , true , 0 ) . unwrap( ) , 0o755 ) ;
311+ }
312+
313+ #[ test]
314+ fn test_parse_invalid_modes ( ) {
315+ // Invalid numeric mode (too large)
316+ assert ! ( parse( "10000" , false , 0 ) . is_err( ) ) ;
317+
318+ // Invalid operator
319+ assert ! ( parse( "u*rw" , false , 0 ) . is_err( ) ) ;
320+
321+ // Invalid symbolic mode
322+ assert ! ( parse( "invalid" , false , 0 ) . is_err( ) ) ;
323+ }
324+
325+ #[ test]
326+ fn test_parse_complex_combinations ( ) {
327+ // Complex real-world examples
328+ assert_eq ! ( parse( "u=rwx,g=rx,o=r" , false , 0 ) . unwrap( ) , 0o754 ) ;
329+ // To test removal, we need to first set permissions, then remove them
330+ assert_eq ! ( parse( "644,a-w" , false , 0 ) . unwrap( ) , 0o444 ) ;
331+ assert_eq ! ( parse( "644,g-r" , false , 0 ) . unwrap( ) , 0o604 ) ;
332+ }
333+
334+ #[ test]
335+ fn test_parse_sequential_application ( ) {
336+ // Test that comma-separated modes are applied sequentially
337+ // First set to 644, then add execute for user
338+ assert_eq ! ( parse( "644,u+x" , false , 0 ) . unwrap( ) , 0o744 ) ;
339+
340+ // First add user write, then set to 755 (should override)
341+ assert_eq ! ( parse( "u+w,755" , false , 0 ) . unwrap( ) , 0o755 ) ;
204342 }
205343}
0 commit comments