Skip to main content

c/
wordexp.rs

1// Copyright (c) 2026 Meng Zhuo
2// SPDX-License-Identifier: BSD-3-Clause
3
4// A real wordexp(3): quoting, parameter expansion, command substitution and
5// pathname expansion.
6
7#[repr(C)]
8pub struct WordexpT {
9    pub we_wordc: usize,
10    pub we_wordv: *mut *mut c_char,
11    pub we_offs: usize,
12}
13
14const WRDE_APPEND_V: c_int = 1;
15const WRDE_DOOFFS_V: c_int = 2;
16const WRDE_NOCMD_V: c_int = 4;
17const WRDE_REUSE_V: c_int = 8;
18const WRDE_UNDEF_V: c_int = 32;
19
20const WRDE_NOSPACE_V: c_int = 1;
21const WRDE_BADCHAR_V: c_int = 2;
22const WRDE_BADVAL_V: c_int = 3;
23const WRDE_CMDSUB_V: c_int = 4;
24const WRDE_SYNTAX_V: c_int = 5;
25
26struct WordList {
27    items: *mut *mut c_char,
28    n: usize,
29    cap: usize,
30}
31
32unsafe fn wl_push(list: *mut WordList, s: *const u8, len: usize) {
33    if (*list).n == (*list).cap {
34        let ncap = if (*list).cap == 0 { 16 } else { (*list).cap * 2 };
35        let na = crate::realloc(
36            (*list).items as *mut c_void,
37            ncap * core::mem::size_of::<*mut c_char>(),
38        ) as *mut *mut c_char;
39        if na.is_null() { return; }
40        (*list).items = na;
41        (*list).cap = ncap;
42    }
43    let p = crate::malloc(len + 1) as *mut c_char;
44    if p.is_null() { return; }
45    core::ptr::copy_nonoverlapping(s, p as *mut u8, len);
46    *p.add(len) = 0;
47    *(*list).items.add((*list).n) = p;
48    (*list).n += 1;
49}
50
51unsafe fn wl_free(list: *mut WordList) {
52    for i in 0..(*list).n {
53        crate::free(*(*list).items.add(i) as *mut c_void);
54    }
55    crate::free((*list).items as *mut c_void);
56    (*list).items = core::ptr::null_mut();
57    (*list).n = 0;
58    (*list).cap = 0;
59}
60
61struct WordBuf {
62    inline: [u8; 4096],
63    heap: *mut u8,
64    len: usize,
65    cap: usize,
66    has_glob: bool,
67    quoted: bool,
68}
69
70unsafe fn wb_data(w: *mut WordBuf) -> *mut u8 {
71    if (*w).heap.is_null() {
72        (*w).inline.as_mut_ptr()
73    } else {
74        (*w).heap
75    }
76}
77
78unsafe fn wb_reserve(w: *mut WordBuf, extra: usize) -> bool {
79    let need = match (*w).len.checked_add(extra) {
80        Some(n) => n,
81        None => return false,
82    };
83    if need <= (*w).cap {
84        return true;
85    }
86    let mut cap = if (*w).cap == 0 { 64 } else { (*w).cap };
87    while cap < need {
88        cap = match cap.checked_mul(2) {
89            Some(n) => n,
90            None => return false,
91        };
92    }
93    let from_inline = (*w).heap.is_null();
94    let p = if from_inline {
95        crate::malloc(cap)
96    } else {
97        crate::realloc((*w).heap as *mut c_void, cap)
98    } as *mut u8;
99    if p.is_null() {
100        return false;
101    }
102    if from_inline {
103        core::ptr::copy_nonoverlapping((*w).inline.as_ptr(), p, (*w).len);
104    }
105    (*w).heap = p;
106    (*w).cap = cap;
107    true
108}
109
110unsafe fn wb_free(w: *mut WordBuf) {
111    if !(*w).heap.is_null() {
112        crate::free((*w).heap as *mut c_void);
113        (*w).heap = core::ptr::null_mut();
114    }
115    (*w).len = 0;
116    (*w).cap = 4096;
117}
118
119unsafe fn wb_push(w: *mut WordBuf, c: u8, quoted: bool) -> c_int {
120    if !wb_reserve(w, 1) { return WRDE_NOSPACE_V; }
121    *wb_data(w).add((*w).len) = c;
122    (*w).len += 1;
123    if quoted {
124        (*w).quoted = true;
125    } else if c == b'*' || c == b'?' || c == b'[' {
126        (*w).has_glob = true;
127    }
128    0
129}
130
131unsafe fn wb_push_str(w: *mut WordBuf, s: *const u8, len: usize, quoted: bool) -> c_int {
132    if !wb_reserve(w, len) { return WRDE_NOSPACE_V; }
133    core::ptr::copy_nonoverlapping(s, wb_data(w).add((*w).len), len);
134    (*w).len += len;
135    if !quoted {
136        for i in 0..len {
137            let c = *s.add(i);
138            if c == b'*' || c == b'?' || c == b'[' { (*w).has_glob = true; break; }
139        }
140    }
141    (*w).quoted = true;
142    0
143}
144
145
146/// Expand `$name` or `${name}`; returns the new index or an error code.
147unsafe fn expand_var(
148    s: &[u8],
149    i_start: usize,
150    w: *mut WordBuf,
151    flags: c_int,
152) -> Result<usize, c_int> {
153    let cap = s.len().max(1);
154    let name = crate::malloc(cap) as *mut u8;
155    if name.is_null() { return Err(WRDE_NOSPACE_V); }
156    let mut i = i_start + 1;
157    let mut nl = 0usize;
158    if s[i] == b'{' {
159        i += 1;
160        while s[i] != 0 && s[i] != b'}' {
161            if nl + 1 >= cap {
162                crate::free(name as *mut c_void);
163                return Err(WRDE_NOSPACE_V);
164            }
165            *name.add(nl) = s[i];
166            nl += 1;
167            i += 1;
168        }
169        if s[i] != b'}' {
170            crate::free(name as *mut c_void);
171            return Err(WRDE_SYNTAX_V);
172        }
173        i += 1;
174    } else {
175        while nl + 1 < cap {
176            let c = s[i];
177            if (c >= b'A' && c <= b'Z') || (c >= b'a' && c <= b'z') || (c >= b'0' && c <= b'9') || c == b'_' {
178                *name.add(nl) = c;
179                nl += 1;
180                i += 1;
181            } else {
182                break;
183            }
184        }
185        if nl == 0 {
186            // A lone '$' is literal.
187            let r = wb_push(w, b'$', false);
188            crate::free(name as *mut c_void);
189            if r != 0 { return Err(r); }
190            return Ok(i_start + 1);
191        }
192    }
193    *name.add(nl) = 0;
194    if nl == 1 && *name == b'$' {
195        let mut pidbuf = [0 as c_char; 16];
196        let pid = crate::getpid();
197        let mut n = 0usize;
198        let mut v = pid as u32;
199        let mut digits = [0u8; 10];
200        if v == 0 { digits[0] = b'0'; n = 1; }
201        while v > 0 { digits[n] = b'0' + (v % 10) as u8; v /= 10; n += 1; }
202        let mut o = 0usize;
203        while n > 0 { n -= 1; pidbuf[o] = digits[n] as c_char; o += 1; }
204        pidbuf[o] = 0;
205        let r = wb_push_str(w, pidbuf.as_ptr() as *const u8, o, false);
206        crate::free(name as *mut c_void);
207        if r != 0 { return Err(r); }
208        return Ok(i);
209    }
210    let val = crate::getenv(name as *const c_char);
211    crate::free(name as *mut c_void);
212    if val.is_null() {
213        if flags & WRDE_UNDEF_V != 0 { return Err(WRDE_BADVAL_V); }
214        return Ok(i);
215    }
216    let vl = crate::strlen(val);
217    let r = wb_push_str(w, val as *const u8, vl, false);
218    if r != 0 { return Err(r); }
219    Ok(i)
220}
221
222unsafe fn command_sub(
223    s: &[u8],
224    i_start: usize,
225    w: *mut WordBuf,
226    flags: c_int,
227) -> Result<usize, c_int> {
228    if flags & WRDE_NOCMD_V != 0 { return Err(WRDE_CMDSUB_V); }
229    let cap = s.len().max(1);
230    let cmd = crate::malloc(cap) as *mut u8;
231    if cmd.is_null() { return Err(WRDE_NOSPACE_V); }
232    let mut i = i_start;
233    let mut cl = 0usize;
234    if s[i] == b'`' {
235        i += 1;
236        while s[i] != 0 && s[i] != b'`' {
237            if cl + 1 >= cap { crate::free(cmd as *mut c_void); return Err(WRDE_NOSPACE_V); }
238            *cmd.add(cl) = s[i];
239            cl += 1;
240            i += 1;
241        }
242        if s[i] != b'`' { crate::free(cmd as *mut c_void); return Err(WRDE_SYNTAX_V); }
243        i += 1;
244    } else {
245        // $( ... ) with nesting.
246        i += 2;
247        let mut depth = 1;
248        while s[i] != 0 {
249            let c = s[i];
250            if c == b'\\' && s[i + 1] != 0 {
251                if cl + 1 >= cap { crate::free(cmd as *mut c_void); return Err(WRDE_NOSPACE_V); }
252                *cmd.add(cl) = c;
253                cl += 1;
254                i += 1;
255                if cl + 1 >= cap { crate::free(cmd as *mut c_void); return Err(WRDE_NOSPACE_V); }
256                *cmd.add(cl) = s[i];
257                cl += 1;
258                i += 1;
259                continue;
260            }
261            if c == b'\'' {
262                // Copy the quoted section verbatim.
263                if cl + 1 >= cap { crate::free(cmd as *mut c_void); return Err(WRDE_NOSPACE_V); }
264                *cmd.add(cl) = c;
265                cl += 1;
266                i += 1;
267                while s[i] != 0 && s[i] != b'\'' {
268                    if cl + 1 >= cap { crate::free(cmd as *mut c_void); return Err(WRDE_NOSPACE_V); }
269                    *cmd.add(cl) = s[i];
270                    cl += 1;
271                    i += 1;
272                }
273                if s[i] == b'\'' {
274                    if cl + 1 >= cap { crate::free(cmd as *mut c_void); return Err(WRDE_NOSPACE_V); }
275                    *cmd.add(cl) = s[i];
276                    cl += 1;
277                    i += 1;
278                }
279                continue;
280            }
281            if c == b'(' { depth += 1; }
282            if c == b')' {
283                depth -= 1;
284                if depth == 0 { i += 1; break; }
285            }
286            if cl + 1 >= cap { crate::free(cmd as *mut c_void); return Err(WRDE_NOSPACE_V); }
287            *cmd.add(cl) = c;
288            cl += 1;
289            i += 1;
290        }
291        if depth != 0 { crate::free(cmd as *mut c_void); return Err(WRDE_SYNTAX_V); }
292    }
293    *cmd.add(cl) = 0;
294    let f = crate::popen(cmd as *const c_char, b"r\0".as_ptr() as *const c_char);
295    if f.is_null() {
296        crate::free(cmd as *mut c_void);
297        return Ok(i);
298    }
299
300    let mut out: *mut c_char = core::ptr::null_mut();
301    let mut out_cap = 0usize;
302    let n = crate::getdelim(&raw mut out, &raw mut out_cap, 0, f);
303    crate::pclose(f);
304    crate::free(cmd as *mut c_void);
305    let mut l = if n > 0 { n as usize } else { 0 };
306    while l > 0 && (out.add(l - 1) as *const u8).read() != 0
307        && ((out.add(l - 1) as *const u8).read() == b'\n' || (out.add(l - 1) as *const u8).read() == b'\r') {
308        l -= 1;
309    }
310    // Word splitting on the command output.
311    let mut p = 0usize;
312    while p < l {
313        while p < l {
314            let c = (out.add(p) as *const u8).read();
315            if c != b' ' && c != b'\t' && c != b'\n' { break; }
316            p += 1;
317        }
318        if p >= l { break; }
319        let st = p;
320        while p < l {
321            let c = (out.add(p) as *const u8).read();
322            if c == b' ' || c == b'\t' || c == b'\n' { break; }
323            p += 1;
324        }
325        let r = wb_push_str(w, out.add(st) as *const u8, p - st, false);
326        if r != 0 {
327            if !out.is_null() { crate::free(out as *mut c_void); }
328            return Err(r);
329        }
330    }
331    if !out.is_null() { crate::free(out as *mut c_void); }
332    Ok(i)
333}
334
335unsafe fn we_fail(list: *mut WordList, w: *mut WordBuf, code: c_int) -> c_int {
336    wb_free(w);
337    wl_free(list);
338    code
339}
340
341#[no_mangle]
342pub unsafe extern "C" fn wordexp(s: *const c_char, p: *mut WordexpT, flags: c_int) -> c_int {
343    if s.is_null() || p.is_null() { return WRDE_SYNTAX_V; }
344    if flags & WRDE_REUSE_V != 0 { wordfree(p); }
345    let mut list = WordList { items: core::ptr::null_mut(), n: 0, cap: 0 };
346    let src = core::slice::from_raw_parts(s as *const u8, crate::strlen(s) + 1);
347    let mut i = 0usize;
348    loop {
349        while src[i] == b' ' || src[i] == b'\t' || src[i] == b'\n' { i += 1; }
350        if src[i] == 0 { break; }
351        let mut w = WordBuf { inline: [0u8; 4096], heap: core::ptr::null_mut(), len: 0, cap: 4096, has_glob: false, quoted: false };
352        let mut done = false;
353        while !done {
354            let c = src[i];
355            match c {
356                0 => { done = true; }
357                b' ' | b'\t' | b'\n' => { i += 1; done = true; }
358                b'\'' => {
359                    i += 1;
360                    loop {
361                        if src[i] == 0 {
362                            return we_fail(&mut list, &mut w, WRDE_SYNTAX_V);
363                        }
364                        if src[i] == b'\'' { i += 1; break; }
365                        let r = wb_push(&mut w, src[i], true);
366                        if r != 0 { return we_fail(&mut list, &mut w, r); }
367                        i += 1;
368                    }
369                    w.quoted = true;
370                }
371                b'"' => {
372                    i += 1;
373                    loop {
374                        if src[i] == 0 {
375                            return we_fail(&mut list, &mut w, WRDE_SYNTAX_V);
376                        }
377                        if src[i] == b'"' { i += 1; break; }
378                        if src[i] == b'\\' {
379                            let n = src[i + 1];
380                            if n == b'$' || n == b'`' || n == b'"' || n == b'\\' {
381                                let r = wb_push(&mut w, n, true);
382                                if r != 0 { return we_fail(&mut list, &mut w, r); }
383                                i += 2;
384                                continue;
385                            }
386                            let r = wb_push(&mut w, src[i], true);
387                            if r != 0 { return we_fail(&mut list, &mut w, r); }
388                            i += 1;
389                            continue;
390                        }
391                        if src[i] == b'`' || (src[i] == b'$' && src[i + 1] == b'(') {
392                            match command_sub(src, i, &mut w, flags) {
393                                Ok(ni) => { i = ni; continue; }
394                                Err(e) => { return we_fail(&mut list, &mut w, e); }
395                            }
396                        }
397                        if src[i] == b'$' {
398                            match expand_var(src, i, &mut w, flags) {
399                                Ok(ni) => { i = ni; continue; }
400                                Err(e) => { return we_fail(&mut list, &mut w, e); }
401                            }
402                        }
403                        let r = wb_push(&mut w, src[i], true);
404                        if r != 0 { return we_fail(&mut list, &mut w, r); }
405                        i += 1;
406                    }
407                    w.quoted = true;
408                }
409                b'\\' => {
410                    i += 1;
411                    if src[i] == 0 {
412                        return we_fail(&mut list, &mut w, WRDE_SYNTAX_V);
413                    }
414                    let r = wb_push(&mut w, src[i], true);
415                    if r != 0 { return we_fail(&mut list, &mut w, r); }
416                    i += 1;
417                }
418                b'$' => {
419                    if src[i + 1] == b'(' {
420                        match command_sub(src, i, &mut w, flags) {
421                            Ok(ni) => { i = ni; }
422                            Err(e) => { return we_fail(&mut list, &mut w, e); }
423                        }
424                    } else {
425                        match expand_var(src, i, &mut w, flags) {
426                            Ok(ni) => { i = ni; }
427                            Err(e) => { return we_fail(&mut list, &mut w, e); }
428                        }
429                    }
430                }
431                b'`' => {
432                    match command_sub(src, i, &mut w, flags) {
433                        Ok(ni) => { i = ni; }
434                        Err(e) => { return we_fail(&mut list, &mut w, e); }
435                    }
436                }
437                b'*' | b'?' | b'[' => {
438                    w.has_glob = true;
439                    let r = wb_push(&mut w, c, false);
440                    if r != 0 { return we_fail(&mut list, &mut w, r); }
441                    i += 1;
442                }
443                _ => {
444                    let r = wb_push(&mut w, c, false);
445                    if r != 0 { return we_fail(&mut list, &mut w, r); }
446                    i += 1;
447                }
448            }
449        }
450        if w.len == 0 && !w.quoted { continue; }
451        *wb_data(&mut w).add(w.len) = 0;
452        if w.has_glob && !w.quoted {
453            let mut g: GlobT = core::mem::zeroed();
454            let r = crate::glob(wb_data(&mut w) as *const c_char, 0x10 | 0x04, None, &mut g); // NOCHECK|NOSORT
455            if r == 0 {
456                for k in 0..g.gl_pathc {
457                    let gp = *g.gl_pathv.add(k);
458                    let l = crate::strlen(gp);
459                    wl_push(&mut list, gp as *const u8, l);
460                }
461                crate::globfree(&mut g);
462            } else {
463                wl_push(&mut list, wb_data(&mut w), w.len);
464            }
465        } else {
466            wl_push(&mut list, wb_data(&mut w), w.len);
467        }
468        wb_free(&mut w);
469    }
470
471    let old = if flags & WRDE_APPEND_V != 0 { (*p).we_wordc } else { 0 };
472    let offs = if flags & WRDE_DOOFFS_V != 0 { (*p).we_offs } else { 0 };
473    let total = offs + old + list.n + 1;
474    let pathv = crate::calloc(total, core::mem::size_of::<*mut c_char>()) as *mut *mut c_char;
475    if pathv.is_null() {
476        wl_free(&mut list);
477        return WRDE_NOSPACE_V;
478    }
479    for k in 0..offs { *pathv.add(k) = core::ptr::null_mut(); }
480    if flags & WRDE_APPEND_V != 0 && old != 0 && !(*p).we_wordv.is_null() {
481        for k in 0..old {
482            *pathv.add(offs + k) = *(*p).we_wordv.add(offs + k);
483        }
484        crate::free((*p).we_wordv as *mut c_void);
485    }
486    for k in 0..list.n {
487        *pathv.add(offs + old + k) = *list.items.add(k);
488    }
489    *pathv.add(offs + old + list.n) = core::ptr::null_mut();
490    crate::free(list.items as *mut c_void);
491    (*p).we_wordc = old + list.n;
492    (*p).we_wordv = pathv;
493    (*p).we_offs = offs;
494    0
495}
496
497#[no_mangle]
498pub unsafe extern "C" fn wordfree(p: *mut WordexpT) {
499    if p.is_null() || (*p).we_wordv.is_null() { return; }
500    let offs = (*p).we_offs;
501    for i in 0..(*p).we_wordc {
502        let q = *(*p).we_wordv.add(offs + i);
503        if !q.is_null() { crate::free(q as *mut c_void); }
504    }
505    crate::free((*p).we_wordv as *mut c_void);
506    (*p).we_wordv = core::ptr::null_mut();
507    (*p).we_wordc = 0;
508}