// Copyright 2025 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
///|
#cfg(platform="windows")
priv struct OsEnv(@c_buffer.Buffer)
///|
#cfg(platform="windows")
extern "C" fn get_curr_env() -> OsEnv = "moonbitlang_async_get_curr_env"
///|
#cfg(platform="windows")
extern "C" fn OsEnv::length(self : OsEnv) -> Int = "moonbitlang_async_env_block_length"
///|
#cfg(platform="windows")
extern "C" fn OsEnv::new(size : Int) -> OsEnv = "moonbitlang_async_allocate_env_block"
///|
#cfg(platform="windows")
extern "C" fn OsEnv::write_env_block(
dst : OsEnv,
block : OsEnv,
offset~ : Int,
) -> Unit = "moonbitlang_async_write_env_block"
///|
/// Return updated offset after writing the new entry.
/// The `dst` block should contain at least
/// `key_len + value_len + 2` characters of space after `offset`.
#cfg(platform="windows")
#borrow(key, value)
extern "C" fn OsEnv::add_entry(
dst : OsEnv,
offset : Int,
key~ : String,
key_len~ : Int,
value~ : String,
value_len~ : Int,
) -> Int = "moonbitlang_async_env_block_add_entry"
///|
#cfg(platform="windows")
fn OsEnv::make(extra_env : Map[String, String], inherit_env~ : Bool) -> OsEnv {
let extra_env_len = for k, v in extra_env; acc = 0 {
continue acc + k.length() + v.length() + 2
} nobreak {
acc
}
let (curr_env, curr_env_len) = if inherit_env {
let curr_env = get_curr_env()
(Some(curr_env), curr_env.length())
} else {
(None, 0)
}
let env = OsEnv::new(curr_env_len + extra_env_len)
for k, v in extra_env; offset = 0 {
let key_len = k.length()
let value_len = v.length()
continue env.add_entry(offset, key=k, key_len~, value=v, value_len~)
} nobreak {
if curr_env is Some(curr_env) {
env.write_env_block(curr_env, offset~)
}
}
env
}
///|
#cfg(any(target="wasm", platform="windows"))
fn StringBuilder::write_arg_with_windows_escape(
builder : StringBuilder,
arg : StringView,
) -> Unit {
if arg is "" {
builder.write("\"\"")
return
}
// Do not add double quotes if it is unnecessary to do so.
// This help with simple cases when the program is not using C argv syntax,
// such as `rundll32.exe`.
let need_quote = for char in arg.code_units() {
if char is (' ' | '\t' | '"' | 0) {
break true
}
} nobreak {
false
}
if !need_quote {
builder.write_stringview(arg)
return
}
let mut segment_start = 0
let mut index = 0
let mut trailing_backslash = 0
fn flush(skip : Int) {
if index > segment_start {
builder.write_stringview(arg[segment_start:index - trailing_backslash])
if trailing_backslash > 0 {
builder.write_string(String::make(trailing_backslash * 2, '\\'))
}
}
segment_start = index + skip
}
builder.write_char('"')
while index < arg.length() {
match arg.code_unit_at(index) {
0 =>
// truncate on NUL in input to prevent argv injection
break
'"' => {
flush(1)
builder <+ "\\\""
}
'\\' => trailing_backslash += 1
_ => trailing_backslash = 0
}
index += 1
}
flush(0)
builder.write_char('"')
}
///|
#cfg(any(target="wasm", platform="windows"))
async fn raw_spawn_windows(
cmd : StringView,
args : ArrayView[String],
extra_env~ : Map[String, String],
inherit_env~ : Bool,
stdin~ : &ProcessInput?,
stdout~ : &ProcessOutput?,
stderr~ : &ProcessOutput?,
cwd~ : StringView?,
no_console_window~ : Bool,
is_orphan~ : Bool,
context~ : String,
) -> @event_loop.Process {
let cmd = match cmd {
[.., '.', 'e' | 'E', 'x' | 'X', 'e' | 'E']
| [.., '.', 'c' | 'C', 'o' | 'O', 'm' | 'M'] => cmd
_ => cmd + ".exe"
}
let command_line = StringBuilder::new()
command_line.write_arg_with_windows_escape(cmd)
for arg in args {
command_line..write_char(' ').write_arg_with_windows_escape(arg)
}
let command_line = @os_string.encode(command_line.to_string())
let cwd = match cwd {
None => None
Some(cwd) => Some(@os_string.encode(cwd))
}
defer {
if stdin is Some(p) {
p.after_spawn()
}
if stdout is Some(p) {
p.after_spawn()
}
if stderr is Some(p) {
p.after_spawn()
}
}
let stdin = match stdin {
Some(pipe) => pipe.fd()
None => @fd_util.invalid_fd
}
let stdout = match stdout {
Some(pipe) => pipe.fd()
None => @fd_util.invalid_fd
}
let stderr = match stderr {
Some(pipe) => pipe.fd()
None => @fd_util.invalid_fd
}
@event_loop.spawn_windows(
command_line,
env=OsEnv::make(extra_env, inherit_env~).0,
stdin~,
stdout~,
stderr~,
cwd~,
no_console_window~,
is_orphan~,
context~,
)
}
///|
#cfg(platform="windows")
async fn raw_spawn(
cmd : StringView,
args : ArrayView[String],
extra_env~ : Map[String, String],
inherit_env~ : Bool,
stdin~ : &ProcessInput?,
stdout~ : &ProcessOutput?,
stderr~ : &ProcessOutput?,
cwd~ : StringView?,
no_console_window~ : Bool,
is_orphan~ : Bool,
context~ : String,
) -> @event_loop.Process {
raw_spawn_windows(
cmd,
args,
extra_env~,
inherit_env~,
stdin~,
stdout~,
stderr~,
cwd~,
no_console_window~,
is_orphan~,
context~,
)
}