Skip to main content

ktrs_cli/
java_launcher.rs

1//! The Windows `java` launcher's wildcard expansion of application arguments, which ktfmt and ktlint get
2//! before their `main` runs: an argument with an unquoted `*` or `?` becomes the matching entries of its parent
3//! directory. Ports JDK 21 `cmdtoargs.c` (`next_arg`, `JLI_CmdToArgs`), `java_md.c` (`CreateApplicationArgs`) and
4//! `LauncherHelper.expandArgs`. Other platforms pass arguments through (the shell expands there).
5
6use crate::ktlint::java_glob::PathMatcher;
7
8/// The program's arguments without its name, as the drop-ins' JVM `main` would receive them.
9pub fn application_args() -> Vec<String> {
10    let args: Vec<String> = std::env::args().skip(1).collect();
11    #[cfg(windows)]
12    {
13        expand_application_args(args, &windows::command_line())
14    }
15    #[cfg(not(windows))]
16    {
17        args
18    }
19}
20
21/// `CreateApplicationArgs`: expands an argument only where the launcher's own split of `command_line` has the same
22/// text with an unquoted wildcard; when the two splits disagree on the count, everything is passed as is.
23pub fn expand_application_args(args: Vec<String>, command_line: &str) -> Vec<String> {
24    let std_args = cmd_to_args(command_line);
25    if std_args.len() != args.len() + 1 {
26        return args;
27    }
28    args.into_iter()
29        .zip(std_args.into_iter().skip(1))
30        .flat_map(|(arg, (std_arg, has_wildcard))| if has_wildcard && std_arg == arg { expand_arg(&arg) } else { vec![arg] })
31        .collect()
32}
33
34/// `JLI_CmdToArgs` without `JDK_JAVA_OPTIONS` and launcher `@argfiles` (neither reaches application arguments).
35fn cmd_to_args(command_line: &str) -> Vec<(String, bool)> {
36    let chars: Vec<char> = command_line.chars().collect();
37    let mut pos = 0;
38    let mut out = Vec::new();
39    loop {
40        let (arg, wildcard, more) = next_arg(&chars, &mut pos);
41        out.push((arg, wildcard));
42        if !more {
43            return out;
44        }
45    }
46}
47
48/// `next_arg`: the argument at `pos`, whether it has a `*`/`?` outside quotes, and whether another one follows.
49fn next_arg(chars: &[char], pos: &mut usize) -> (String, bool, bool) {
50    let mut dest = String::new();
51    let (mut separator, mut quotes, mut slashes, mut prev) = (false, 0i32, 0usize, '\0');
52    let mut wildcard = false;
53    let mut done = false;
54    let push_slashes = |dest: &mut String, n: usize| dest.extend(std::iter::repeat_n('\\', n));
55    while !done {
56        let Some(&ch) = chars.get(*pos) else { break };
57        match ch {
58            '"' => {
59                if separator {
60                    done = true;
61                } else {
62                    if prev == '\\' {
63                        push_slashes(&mut dest, slashes / 2);
64                        if slashes % 2 == 1 {
65                            dest.push(ch);
66                        } else {
67                            quotes += 1;
68                        }
69                    } else if prev == '"' && quotes % 2 == 0 {
70                        quotes += 1;
71                        dest.push(ch);
72                    } else if quotes == 0 {
73                        quotes += 1;
74                    } else {
75                        quotes -= 1;
76                    }
77                    slashes = 0;
78                }
79            }
80            '\\' => {
81                slashes += 1;
82                if separator {
83                    done = true;
84                    separator = false;
85                }
86            }
87            ' ' | '\t' => {
88                if prev == '\\' {
89                    push_slashes(&mut dest, slashes);
90                }
91                if quotes % 2 == 1 {
92                    dest.push(ch);
93                } else {
94                    separator = true;
95                }
96                slashes = 0;
97            }
98            '*' | '?' => {
99                if separator {
100                    done = true;
101                    separator = false;
102                } else {
103                    if quotes % 2 == 0 {
104                        wildcard = true;
105                    }
106                    if prev == '\\' {
107                        push_slashes(&mut dest, slashes);
108                    }
109                    dest.push(ch);
110                    slashes = 0;
111                }
112            }
113            _ => {
114                if prev == '\\' {
115                    push_slashes(&mut dest, slashes);
116                    dest.push(ch);
117                } else if separator {
118                    done = true;
119                } else {
120                    dest.push(ch);
121                }
122                slashes = 0;
123            }
124        }
125        if !done {
126            prev = ch;
127            *pos += 1;
128        }
129    }
130    if prev == '\\' {
131        push_slashes(&mut dest, slashes);
132    }
133    (dest, wildcard, done)
134}
135
136/// `LauncherHelper.expandArgs` for one argument: `Files.newDirectoryStream(parent, name)`, each entry's
137/// `normalize().toString()`; the argument itself when nothing matches or the stream throws.
138fn expand_arg(arg: &str) -> Vec<String> {
139    let file = windows_path::File::new(arg);
140    let parent = file.parent().unwrap_or_else(|| ".".to_owned());
141    let Some(dir) = windows_path::Path::parse(&parent) else { return vec![arg.to_owned()] };
142    let Ok(matcher) = PathMatcher::new(&file.name(), true) else { return vec![arg.to_owned()] };
143    let Ok(entries) = std::fs::read_dir(dir.to_string()) else { return vec![arg.to_owned()] };
144    let expanded: Vec<String> = entries
145        .flatten()
146        .map(|e| e.file_name().to_string_lossy().into_owned())
147        .filter(|name| matcher.matches(name))
148        .map(|name| dir.resolve(&name).normalize().to_string())
149        .collect();
150    if expanded.is_empty() { vec![arg.to_owned()] } else { expanded }
151}
152
153mod windows_path;
154
155#[cfg(windows)]
156mod windows {
157    #[link(name = "kernel32")]
158    unsafe extern "system" {
159        fn GetCommandLineW() -> *const u16;
160    }
161
162    pub fn command_line() -> String {
163        // SAFETY: GetCommandLineW returns a NUL-terminated string owned by the process for its lifetime.
164        unsafe {
165            let p = GetCommandLineW();
166            let len = (0..).take_while(|&i| *p.add(i) != 0).count();
167            String::from_utf16_lossy(std::slice::from_raw_parts(p, len))
168        }
169    }
170}
171
172#[cfg(test)]
173mod tests;