| 1 | module os |
| 2 | |
| 3 | import strings |
| 4 | import encoding.utf8.validate |
| 5 | |
| 6 | #flag windows -l advapi32 |
| 7 | #include <process.h> |
| 8 | #include <sys/utime.h> |
| 9 | |
| 10 | // path_separator is the platform specific separator string, used between the folders, and filenames in a path. It is '/' on POSIX, and '\\' on Windows. |
| 11 | pub const path_separator = '\\' |
| 12 | |
| 13 | // path_delimiter is the platform specific delimiter string, used between the paths in environment variables like PATH. It is ':' on POSIX, and ';' on Windows. |
| 14 | pub const path_delimiter = ';' |
| 15 | |
| 16 | // path_devnull is a platform-specific file path of the null device. |
| 17 | // It is '/dev/null' on POSIX, and r'\\.\nul' on Windows. |
| 18 | pub const path_devnull = r'\\.\nul' |
| 19 | |
| 20 | // See https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createsymboliclinkw |
| 21 | fn C.CreateSymbolicLinkW(&u16, &u16, u32) i32 |
| 22 | |
| 23 | // See https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createhardlinkw |
| 24 | fn C.CreateHardLinkW(&u16, &u16, C.SECURITY_ATTRIBUTES) i32 |
| 25 | |
| 26 | // See https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-getshortpathnamew |
| 27 | fn C.GetShortPathNameW(&u16, &u16, u32) u32 |
| 28 | |
| 29 | fn C.AddVectoredExceptionHandler(u32, voidptr) voidptr |
| 30 | |
| 31 | fn C._getpid() i32 |
| 32 | |
| 33 | const executable_suffixes = ['.exe', '.bat', '.cmd', ''] |
| 34 | |
| 35 | // these consts are declared for parity with the nix version, their values are not used, except for -cross |
| 36 | const s_ifmt = 0xF000 // type of file |
| 37 | const s_ifdir = 0x4000 // directory |
| 38 | const s_ifreg = 0x8000 // regular file |
| 39 | const s_iflnk = 0xa000 // link |
| 40 | const s_isuid = 0o4000 // SUID |
| 41 | const s_isgid = 0o2000 // SGID |
| 42 | const s_isvtx = 0o1000 // Sticky |
| 43 | const s_irusr = 0o0400 // Read by owner |
| 44 | const s_iwusr = 0o0200 // Write by owner |
| 45 | const s_ixusr = 0o0100 // Execute by owner |
| 46 | const s_irgrp = 0o0040 // Read by group |
| 47 | const s_iwgrp = 0o0020 // Write by group |
| 48 | const s_ixgrp = 0o0010 // Execute by group |
| 49 | const s_iroth = 0o0004 // Read by others |
| 50 | const s_iwoth = 0o0002 // Write by others |
| 51 | const s_ixoth = 0o0001 |
| 52 | |
| 53 | // Ref - https://docs.microsoft.com/en-us/windows/desktop/winprog/windows-data-types |
| 54 | // A handle to an object. |
| 55 | pub type HANDLE = voidptr |
| 56 | pub type HMODULE = voidptr |
| 57 | |
| 58 | // win: FILETIME |
| 59 | // https://docs.microsoft.com/en-us/windows/win32/api/minwinbase/ns-minwinbase-filetime |
| 60 | struct Filetime { |
| 61 | dw_low_date_time u32 |
| 62 | dw_high_date_time u32 |
| 63 | } |
| 64 | |
| 65 | // win: WIN32_FIND_DATA |
| 66 | // https://docs.microsoft.com/en-us/windows/win32/api/minwinbase/ns-minwinbase-win32_find_dataw |
| 67 | struct Win32finddata { |
| 68 | mut: |
| 69 | dw_file_attributes u32 |
| 70 | ft_creation_time Filetime |
| 71 | ft_last_access_time Filetime |
| 72 | ft_last_write_time Filetime |
| 73 | n_file_size_high u32 |
| 74 | n_file_size_low u32 |
| 75 | dw_reserved0 u32 |
| 76 | dw_reserved1 u32 |
| 77 | c_file_name [260]u16 // max_path_len = 260 |
| 78 | c_alternate_file_name [14]u16 // 14 |
| 79 | dw_file_type u32 |
| 80 | dw_creator_type u32 |
| 81 | w_finder_flags u16 |
| 82 | } |
| 83 | |
| 84 | struct ProcessInformation { |
| 85 | mut: |
| 86 | h_process voidptr |
| 87 | h_thread voidptr |
| 88 | dw_process_id u32 |
| 89 | dw_thread_id u32 |
| 90 | } |
| 91 | |
| 92 | struct StartupInfo { |
| 93 | mut: |
| 94 | cb u32 |
| 95 | lp_reserved &u16 = unsafe { nil } |
| 96 | lp_desktop &u16 = unsafe { nil } |
| 97 | lp_title &u16 = unsafe { nil } |
| 98 | dw_x u32 |
| 99 | dw_y u32 |
| 100 | dw_x_size u32 |
| 101 | dw_y_size u32 |
| 102 | dw_x_count_chars u32 |
| 103 | dw_y_count_chars u32 |
| 104 | dw_fill_attributes u32 |
| 105 | dw_flags u32 |
| 106 | w_show_window u16 |
| 107 | cb_reserved2 u16 |
| 108 | lp_reserved2 &u8 = unsafe { nil } |
| 109 | h_std_input voidptr |
| 110 | h_std_output voidptr |
| 111 | h_std_error voidptr |
| 112 | } |
| 113 | |
| 114 | struct SecurityAttributes { |
| 115 | mut: |
| 116 | n_length u32 |
| 117 | lp_security_descriptor voidptr |
| 118 | b_inherit_handle bool |
| 119 | } |
| 120 | |
| 121 | @[inline] |
| 122 | fn wide_ptr_to_string(wstr &u16) string { |
| 123 | return unsafe { string_from_wide(wstr) } |
| 124 | } |
| 125 | |
| 126 | fn looks_like_utf16le_captured_output(raw string) bool { |
| 127 | if raw.len < 2 { |
| 128 | return false |
| 129 | } |
| 130 | // Allow odd-length buffers: Windows text-mode translation can insert |
| 131 | // extra bytes (e.g. \r before \n), producing odd-length UTF-16LE output. |
| 132 | // In that case, just ignore the trailing byte during detection. |
| 133 | if raw[0] == 0xff && raw[1] == 0xfe { |
| 134 | return true |
| 135 | } |
| 136 | sample_len := if raw.len > 128 { 128 } else { raw.len } |
| 137 | mut checked_pairs := 0 |
| 138 | mut zero_high_bytes := 0 |
| 139 | for i := 1; i < sample_len; i += 2 { |
| 140 | checked_pairs++ |
| 141 | if raw[i] == 0 { |
| 142 | zero_high_bytes++ |
| 143 | } |
| 144 | } |
| 145 | return checked_pairs > 0 && zero_high_bytes * 4 >= checked_pairs * 3 |
| 146 | } |
| 147 | |
| 148 | fn looks_like_utf16be_captured_output(raw string) bool { |
| 149 | if raw.len < 2 || raw.len % 2 != 0 { |
| 150 | return false |
| 151 | } |
| 152 | if raw[0] == 0xfe && raw[1] == 0xff { |
| 153 | return true |
| 154 | } |
| 155 | sample_len := if raw.len > 128 { 128 } else { raw.len } |
| 156 | mut checked_pairs := 0 |
| 157 | mut zero_low_bytes := 0 |
| 158 | for i := 0; i < sample_len; i += 2 { |
| 159 | checked_pairs++ |
| 160 | if raw[i] == 0 { |
| 161 | zero_low_bytes++ |
| 162 | } |
| 163 | } |
| 164 | return checked_pairs > 0 && zero_low_bytes * 4 >= checked_pairs * 3 |
| 165 | } |
| 166 | |
| 167 | @[manualfree] |
| 168 | fn utf16_captured_output_to_string(raw string, little_endian bool) string { |
| 169 | mut start := 0 |
| 170 | if little_endian && raw[0] == 0xff && raw[1] == 0xfe { |
| 171 | start = 2 |
| 172 | } else if !little_endian && raw[0] == 0xfe && raw[1] == 0xff { |
| 173 | start = 2 |
| 174 | } |
| 175 | pairs := (raw.len - start) / 2 |
| 176 | mut wide := []u16{len: pairs + 1, init: u16(0)} |
| 177 | for idx := 0; idx < pairs; idx++ { |
| 178 | base := start + idx * 2 |
| 179 | b0 := u16(raw[base]) |
| 180 | b1 := u16(raw[base + 1]) |
| 181 | wide[idx] = if little_endian { b0 | (b1 << 8) } else { (b0 << 8) | b1 } |
| 182 | } |
| 183 | res := unsafe { string_from_wide2(wide.data, pairs) } |
| 184 | unsafe { wide.free() } |
| 185 | return res |
| 186 | } |
| 187 | |
| 188 | @[manualfree] |
| 189 | fn decode_windows_captured_output(raw string) string { |
| 190 | if raw.len == 0 { |
| 191 | return '' |
| 192 | } |
| 193 | if looks_like_utf16le_captured_output(raw) { |
| 194 | return utf16_captured_output_to_string(raw, true) |
| 195 | } |
| 196 | if looks_like_utf16be_captured_output(raw) { |
| 197 | return utf16_captured_output_to_string(raw, false) |
| 198 | } |
| 199 | if validate.utf8_string(raw) { |
| 200 | // Check for embedded null bytes, which suggest this is actually UTF-16 |
| 201 | // that wasn't detected (e.g. due to text-mode \r\n translation corrupting |
| 202 | // the byte alignment). Strip null bytes as a recovery heuristic. |
| 203 | mut has_null := false |
| 204 | for i in 0 .. raw.len { |
| 205 | if raw[i] == 0 { |
| 206 | has_null = true |
| 207 | break |
| 208 | } |
| 209 | } |
| 210 | if !has_null { |
| 211 | return raw |
| 212 | } |
| 213 | mut nulls := 0 |
| 214 | for i in 0 .. raw.len { |
| 215 | if raw[i] == 0 { |
| 216 | nulls++ |
| 217 | } |
| 218 | } |
| 219 | if nulls * 4 < raw.len { |
| 220 | return raw |
| 221 | } |
| 222 | // Contains many null bytes - likely corrupted UTF-16LE. |
| 223 | // Strip null bytes and normalize \r\n to \n (text-mode artifacts). |
| 224 | mut cleaned := strings.new_builder(raw.len) |
| 225 | for i in 0 .. raw.len { |
| 226 | if raw[i] == 0 { |
| 227 | continue |
| 228 | } |
| 229 | if raw[i] == 0x0D && i + 1 < raw.len && raw[i + 1] == 0x0A { |
| 230 | continue |
| 231 | } |
| 232 | cleaned.write_u8(raw[i]) |
| 233 | } |
| 234 | return cleaned.str() |
| 235 | } |
| 236 | mut wide := raw.to_wide(from_ansi: true) |
| 237 | if isnil(wide) { |
| 238 | return raw |
| 239 | } |
| 240 | res := unsafe { string_from_wide(wide) } |
| 241 | unsafe { free(wide) } |
| 242 | return res |
| 243 | } |
| 244 | |
| 245 | pub struct C._utimbuf { |
| 246 | actime i64 |
| 247 | modtime i64 |
| 248 | } |
| 249 | |
| 250 | fn C._utime(&char, voidptr) i32 |
| 251 | |
| 252 | fn native_glob_pattern(pattern string, mut matches []string) ! { |
| 253 | $if debug { |
| 254 | // FindFirstFile() and FindNextFile() both have a globbing function. |
| 255 | // Unfortunately this is not as pronounced as under Unix, but should provide some functionality |
| 256 | eprintln('os.glob() does not have all the features on Windows as it has on Unix operating systems') |
| 257 | } |
| 258 | normalized_pattern := pattern.replace('\\', '/') |
| 259 | mut match_base := dir(normalized_pattern).replace('\\', '/') |
| 260 | if match_base == '.' { |
| 261 | match_base = '' |
| 262 | } else if match_base != '/' && !match_base.ends_with('/') { |
| 263 | match_base += '/' |
| 264 | } |
| 265 | mut find_file_data := Win32finddata{} |
| 266 | wpattern := pattern.replace('/', '\\').to_wide() |
| 267 | h_find_files := C.FindFirstFile(wpattern, voidptr(&find_file_data)) |
| 268 | |
| 269 | defer { |
| 270 | C.FindClose(h_find_files) |
| 271 | } |
| 272 | |
| 273 | if h_find_files == C.INVALID_HANDLE_VALUE { |
| 274 | return error('os.glob(): Could not get a file handle: ' + |
| 275 | get_error_msg(int(C.GetLastError()))) |
| 276 | } |
| 277 | |
| 278 | // save first finding |
| 279 | fname := wide_ptr_to_string(&find_file_data.c_file_name[0]) |
| 280 | if fname !in ['.', '..'] { |
| 281 | mut fp := fname.replace('\\', '/') |
| 282 | if match_base != '' { |
| 283 | fp = '${match_base}${fp}' |
| 284 | } |
| 285 | if find_file_data.dw_file_attributes & u32(C.FILE_ATTRIBUTE_DIRECTORY) > 0 { |
| 286 | fp += '/' |
| 287 | } |
| 288 | matches << fp |
| 289 | } |
| 290 | |
| 291 | // check and save next findings |
| 292 | for i := 0; C.FindNextFile(h_find_files, voidptr(&find_file_data)) > 0; i++ { |
| 293 | filename := wide_ptr_to_string(&find_file_data.c_file_name[0]) |
| 294 | if filename in ['.', '..'] { |
| 295 | continue |
| 296 | } |
| 297 | mut fpath := filename.replace('\\', '/') |
| 298 | if match_base != '' { |
| 299 | fpath = '${match_base}${fpath}' |
| 300 | } |
| 301 | if find_file_data.dw_file_attributes & u32(C.FILE_ATTRIBUTE_DIRECTORY) > 0 { |
| 302 | fpath += '/' |
| 303 | } |
| 304 | matches << fpath |
| 305 | } |
| 306 | } |
| 307 | |
| 308 | // short_path returns the Windows DOS 8.3 short path when it is available. |
| 309 | // If the path does not exist, or if short names are unavailable, it returns the original path. |
| 310 | pub fn short_path(path string) string { |
| 311 | if path == '' { |
| 312 | return '' |
| 313 | } |
| 314 | normalized := path.replace('/', '\\') |
| 315 | mut short_buf := [max_path_buffer_size]u16{} |
| 316 | mut wpath := normalized.to_wide() |
| 317 | defer { |
| 318 | unsafe { free(voidptr(wpath)) } |
| 319 | } |
| 320 | short_len := C.GetShortPathNameW(wpath, &short_buf[0], max_path_buffer_size) |
| 321 | if short_len > 0 && short_len < u32(max_path_buffer_size) { |
| 322 | return unsafe { string_from_wide2(&short_buf[0], int(short_len)) } |
| 323 | } |
| 324 | parent := dir(normalized) |
| 325 | if parent == '' || parent == '.' || parent == normalized || !is_dir(parent) { |
| 326 | return path |
| 327 | } |
| 328 | short_parent := short_path(parent) |
| 329 | if short_parent == parent { |
| 330 | return path |
| 331 | } |
| 332 | return join_path_single(short_parent, file_name(normalized)) |
| 333 | } |
| 334 | |
| 335 | pub fn utime(path string, actime i64, modtime i64) ! { |
| 336 | mut u := C._utimbuf{actime, modtime} |
| 337 | if C._utime(&char(path.str), voidptr(&u)) != 0 { |
| 338 | return error_with_code(posix_get_error_msg(C.errno), C.errno) |
| 339 | } |
| 340 | } |
| 341 | |
| 342 | pub fn ls(path string) ![]string { |
| 343 | if path == '' { |
| 344 | return error('ls() expects a folder, not an empty string') |
| 345 | } |
| 346 | mut find_file_data := Win32finddata{} |
| 347 | mut dir_files := []string{} |
| 348 | // We can also check if the handle is valid. but using is_dir instead |
| 349 | // h_find_dir := C.FindFirstFile(path.str, &find_file_data) |
| 350 | // if (invalid_handle_value == h_find_dir) { |
| 351 | // return dir_files |
| 352 | // } |
| 353 | // C.FindClose(h_find_dir) |
| 354 | if !is_dir(path) { |
| 355 | return error('ls() couldnt open dir "${path}": directory does not exist') |
| 356 | } |
| 357 | // we need to add files to path eg. c:\windows\*.dll or :\windows\* |
| 358 | path_files := '${path}\\*' |
| 359 | // NOTE:TODO: once we have a way to convert utf16 wide character to utf8 |
| 360 | // we should use FindFirstFileW and FindNextFileW |
| 361 | h_find_files := C.FindFirstFile(path_files.to_wide(), voidptr(&find_file_data)) |
| 362 | // Handle cases where files cannot be opened. for example:"System Volume Information" |
| 363 | if h_find_files == C.INVALID_HANDLE_VALUE { |
| 364 | return error('ls(): Could not get a file handle: ' + get_error_msg(int(C.GetLastError()))) |
| 365 | } |
| 366 | first_filename := wide_ptr_to_string(&find_file_data.c_file_name[0]) |
| 367 | if first_filename != '.' && first_filename != '..' { |
| 368 | dir_files << first_filename |
| 369 | } |
| 370 | for C.FindNextFile(h_find_files, voidptr(&find_file_data)) > 0 { |
| 371 | filename := wide_ptr_to_string(&find_file_data.c_file_name[0]) |
| 372 | if filename != '.' && filename != '..' { |
| 373 | dir_files << filename.clone() |
| 374 | } |
| 375 | } |
| 376 | C.FindClose(h_find_files) |
| 377 | return dir_files |
| 378 | } |
| 379 | |
| 380 | // mkdir creates a new directory with the specified path. |
| 381 | pub fn mkdir(path string, params MkdirParams) ! { |
| 382 | if path == '.' { |
| 383 | return |
| 384 | } |
| 385 | apath := real_path(path) |
| 386 | if !C.CreateDirectory(apath.to_wide(), 0) { |
| 387 | return error('mkdir failed for "${apath}", because CreateDirectory returned: ' + |
| 388 | get_error_msg(int(C.GetLastError()))) |
| 389 | } |
| 390 | } |
| 391 | |
| 392 | // Ref - https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/get-osfhandle?view=vs-2019 |
| 393 | // get_file_handle retrieves the operating-system file handle that is associated with the specified file descriptor. |
| 394 | pub fn get_file_handle(path string) HANDLE { |
| 395 | cfile := vfopen(path, 'rb') or { return HANDLE(invalid_handle_value) } |
| 396 | handle := HANDLE(C._get_osfhandle(fileno(cfile))) // CreateFile? - hah, no -_- |
| 397 | return handle |
| 398 | } |
| 399 | |
| 400 | // Ref - https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-getmodulefilenamea |
| 401 | // get_module_filename retrieves the fully qualified path for the file that contains the specified module. |
| 402 | // The module must have been loaded by the current process. |
| 403 | pub fn get_module_filename(handle HANDLE) !string { |
| 404 | unsafe { |
| 405 | mut sz := 4096 // Optimized length |
| 406 | mut buf := &u16(malloc_noscan(4096)) |
| 407 | for { |
| 408 | status := int(C.GetModuleFileNameW(handle, voidptr(&buf), sz)) |
| 409 | match status { |
| 410 | success { |
| 411 | return string_from_wide2(buf, sz) |
| 412 | } |
| 413 | else { |
| 414 | // Must handled with GetLastError and converted by FormatMessageW |
| 415 | return error('Cannot get file name from handle') |
| 416 | } |
| 417 | } |
| 418 | } |
| 419 | } |
| 420 | panic('this should be unreachable') // TODO: remove unreachable after loop |
| 421 | } |
| 422 | |
| 423 | // Ref - https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-FormatMessageWa#parameters |
| 424 | const format_message_allocate_buffer = 0x00000100 |
| 425 | const format_message_argument_array = 0x00002000 |
| 426 | const format_message_from_hmodule = 0x00000800 |
| 427 | const format_message_from_string = 0x00000400 |
| 428 | const format_message_from_system = 0x00001000 |
| 429 | const format_message_ignore_inserts = 0x00000200 |
| 430 | |
| 431 | // Ref - winnt.h |
| 432 | const sublang_neutral = 0x00 |
| 433 | const sublang_default = 0x01 |
| 434 | const lang_neutral = sublang_neutral |
| 435 | |
| 436 | // Ref - https://docs.microsoft.com/en-us/windows/win32/debug/system-error-codes--12000-15999- |
| 437 | const max_error_code = 15841 |
| 438 | |
| 439 | // ptr_win_get_error_msg return string (voidptr) |
| 440 | // representation of error, only for windows. |
| 441 | fn ptr_win_get_error_msg(code u32) voidptr { |
| 442 | mut buf := voidptr(unsafe { nil }) |
| 443 | // Check for code overflow |
| 444 | if code > u32(max_error_code) { |
| 445 | return buf |
| 446 | } |
| 447 | C.FormatMessageW(format_message_allocate_buffer | format_message_from_system | format_message_ignore_inserts, |
| 448 | 0, code, 0, voidptr(&buf), 0, 0) |
| 449 | return buf |
| 450 | } |
| 451 | |
| 452 | // get_error_msg return error code representation in string. |
| 453 | pub fn get_error_msg(code int) string { |
| 454 | if code < 0 { // skip negative |
| 455 | return '' |
| 456 | } |
| 457 | ptr_text := ptr_win_get_error_msg(u32(code)) |
| 458 | if ptr_text == 0 { // compare with null |
| 459 | return '' |
| 460 | } |
| 461 | msg := wide_ptr_to_string(&u16(ptr_text)) |
| 462 | C.LocalFree(ptr_text) |
| 463 | return msg |
| 464 | } |
| 465 | |
| 466 | // execute starts the specified command, waits for it to complete, and returns its output. |
| 467 | // In opposition to `raw_execute` this function rejects `&&`, `||`, and linefeeds when they appear |
| 468 | // outside double-quoted strings before delegating to `cmd.exe`. |
| 469 | pub fn execute(cmd string) Result { |
| 470 | if windows_execute_has_forbidden_shell_operator(cmd) { |
| 471 | return Result{ |
| 472 | exit_code: -1 |
| 473 | output: '&&, || and \\n are not allowed in shell commands' |
| 474 | } |
| 475 | } |
| 476 | return unsafe { raw_execute(cmd) } |
| 477 | } |
| 478 | |
| 479 | // exec starts the specified command with arguments, waits for it to complete, and returns its output. |
| 480 | pub fn exec(args []string) Result { |
| 481 | if args.len == 0 { |
| 482 | return Result{ |
| 483 | exit_code: -1 |
| 484 | output: 'exec requires at least one argument' |
| 485 | } |
| 486 | } |
| 487 | mut command_line := requote_arg(args[0]) |
| 488 | if args.len > 1 { |
| 489 | command_line += ' ' + requote_args(args[1..]) |
| 490 | } |
| 491 | mut application_name := '' |
| 492 | filename_lc := args[0].to_lower_ascii() |
| 493 | if is_abs_path(args[0]) && !filename_lc.ends_with('.bat') && !filename_lc.ends_with('.cmd') { |
| 494 | application_name = args[0] |
| 495 | } |
| 496 | return windows_execute_command_line(command_line, application_name, args.join(' '), false) |
| 497 | } |
| 498 | |
| 499 | // windows_execute_has_forbidden_shell_operator rejects only the command chaining operators that `cmd.exe` |
| 500 | // treats specially outside double-quoted strings. |
| 501 | fn windows_execute_has_forbidden_shell_operator(cmd string) bool { |
| 502 | mut in_double_quotes := false |
| 503 | for i := 0; i < cmd.len; i++ { |
| 504 | ch := cmd[i] |
| 505 | if ch == `"` { |
| 506 | in_double_quotes = !in_double_quotes |
| 507 | continue |
| 508 | } |
| 509 | if ch == `\n` { |
| 510 | return true |
| 511 | } |
| 512 | if in_double_quotes { |
| 513 | continue |
| 514 | } |
| 515 | if ch == `&` && i + 1 < cmd.len && cmd[i + 1] == `&` { |
| 516 | return true |
| 517 | } |
| 518 | if ch == `|` && i + 1 < cmd.len && cmd[i + 1] == `|` { |
| 519 | return true |
| 520 | } |
| 521 | } |
| 522 | return false |
| 523 | } |
| 524 | |
| 525 | // raw_execute starts the specified command, waits for it to complete, and returns its output. |
| 526 | // It's marked as `unsafe` to help emphasize the problems that may arise by allowing, for example, |
| 527 | // user provided escape sequences. |
| 528 | @[unsafe] |
| 529 | pub fn raw_execute(cmd string) Result { |
| 530 | mut pcmd := cmd |
| 531 | if cmd.contains('./') { |
| 532 | pcmd = pcmd.replace('./', '.\\') |
| 533 | } |
| 534 | if cmd.contains('2>') { |
| 535 | pcmd = 'cmd /c "${pcmd}"' |
| 536 | } else { |
| 537 | pcmd = 'cmd /c "${pcmd} 2>&1"' |
| 538 | } |
| 539 | return windows_execute_command_line(pcmd, '', cmd, true) |
| 540 | } |
| 541 | |
| 542 | fn windows_execute_command_line(command_line_text string, application_name string, display_cmd string, expand_environment bool) Result { |
| 543 | mut child_stdin := &u32(unsafe { nil }) |
| 544 | mut child_stdout_read := &u32(unsafe { nil }) |
| 545 | mut child_stdout_write := &u32(unsafe { nil }) |
| 546 | mut sa := SecurityAttributes{} |
| 547 | sa.n_length = sizeof(C.SECURITY_ATTRIBUTES) |
| 548 | sa.b_inherit_handle = true |
| 549 | create_pipe_ok := C.CreatePipe(voidptr(&child_stdout_read), voidptr(&child_stdout_write), |
| 550 | voidptr(&sa), 0) |
| 551 | if !create_pipe_ok { |
| 552 | error_num := int(C.GetLastError()) |
| 553 | error_msg := get_error_msg(error_num) |
| 554 | return Result{ |
| 555 | exit_code: error_num |
| 556 | output: 'exec failed (CreatePipe): ${error_msg}' |
| 557 | } |
| 558 | } |
| 559 | set_handle_info_ok := C.SetHandleInformation(child_stdout_read, C.HANDLE_FLAG_INHERIT, 0) |
| 560 | if !set_handle_info_ok { |
| 561 | error_num := int(C.GetLastError()) |
| 562 | error_msg := get_error_msg(error_num) |
| 563 | return Result{ |
| 564 | exit_code: error_num |
| 565 | output: 'exec failed (SetHandleInformation): ${error_msg}' |
| 566 | } |
| 567 | } |
| 568 | proc_info := ProcessInformation{} |
| 569 | start_info := StartupInfo{ |
| 570 | lp_reserved2: unsafe { nil } |
| 571 | lp_reserved: unsafe { nil } |
| 572 | lp_desktop: unsafe { nil } |
| 573 | lp_title: unsafe { nil } |
| 574 | cb: sizeof(StartupInfo) |
| 575 | h_std_input: child_stdin |
| 576 | h_std_output: child_stdout_write |
| 577 | h_std_error: child_stdout_write |
| 578 | dw_flags: u32(C.STARTF_USESTDHANDLES) |
| 579 | } |
| 580 | |
| 581 | mut command_line_ptr := command_line_text.to_wide() |
| 582 | mut expanded_command_line := [32768]u16{} |
| 583 | if expand_environment { |
| 584 | C.ExpandEnvironmentStringsW(command_line_ptr, voidptr(&expanded_command_line), 32768) |
| 585 | command_line_ptr = unsafe { &expanded_command_line[0] } |
| 586 | } |
| 587 | mut application_name_ptr := &u16(unsafe { nil }) |
| 588 | if application_name != '' { |
| 589 | application_name_ptr = application_name.to_wide() |
| 590 | } |
| 591 | create_process_ok := C.CreateProcessW(application_name_ptr, command_line_ptr, 0, 0, C.TRUE, |
| 592 | C.CREATE_NO_WINDOW, 0, 0, voidptr(&start_info), voidptr(&proc_info)) |
| 593 | if !create_process_ok { |
| 594 | error_num := int(C.GetLastError()) |
| 595 | error_msg := get_error_msg(error_num) |
| 596 | return Result{ |
| 597 | exit_code: error_num |
| 598 | output: 'exec failed (CreateProcess) with code ${error_num}: ${error_msg} cmd: ${display_cmd}' |
| 599 | } |
| 600 | } |
| 601 | C.CloseHandle(child_stdin) |
| 602 | C.CloseHandle(child_stdout_write) |
| 603 | buf := [4096]u8{} |
| 604 | mut bytes_read := u32(0) |
| 605 | mut read_data := strings.new_builder(1024) |
| 606 | for { |
| 607 | mut result := false |
| 608 | unsafe { |
| 609 | result = C.ReadFile(child_stdout_read, &buf[0], 1000, voidptr(&bytes_read), 0) |
| 610 | read_data.write_ptr(&buf[0], int(bytes_read)) |
| 611 | } |
| 612 | if result == false || int(bytes_read) == 0 { |
| 613 | break |
| 614 | } |
| 615 | } |
| 616 | soutput := decode_windows_captured_output(read_data.str()) |
| 617 | unsafe { read_data.free() } |
| 618 | exit_code := u32(0) |
| 619 | C.WaitForSingleObject(proc_info.h_process, C.INFINITE) |
| 620 | C.GetExitCodeProcess(proc_info.h_process, voidptr(&exit_code)) |
| 621 | C.CloseHandle(proc_info.h_process) |
| 622 | C.CloseHandle(proc_info.h_thread) |
| 623 | return Result{ |
| 624 | output: soutput |
| 625 | exit_code: int(exit_code) |
| 626 | } |
| 627 | } |
| 628 | |
| 629 | pub fn symlink(origin string, target string) ! { |
| 630 | // this is a temporary fix for TCC32 due to runtime error |
| 631 | // TODO: find the cause why TCC32 for Windows does not work without the compiletime option |
| 632 | $if x64 || x32 { |
| 633 | mut flags := 0 |
| 634 | if is_dir(origin) { |
| 635 | flags ^= 1 |
| 636 | } |
| 637 | |
| 638 | flags ^= 2 // SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE |
| 639 | res := C.CreateSymbolicLinkW(target.to_wide(), origin.to_wide(), flags) |
| 640 | |
| 641 | // 1 = success, != 1 failure => https://stackoverflow.com/questions/33010440/createsymboliclink-on-windows-10 |
| 642 | if res != 1 { |
| 643 | return error(get_error_msg(int(C.GetLastError()))) |
| 644 | } |
| 645 | if !exists(target) { |
| 646 | return error('C.CreateSymbolicLinkW reported success, but symlink still does not exist') |
| 647 | } |
| 648 | return |
| 649 | } |
| 650 | return error('could not symlink') |
| 651 | } |
| 652 | |
| 653 | // readlink reads the target of a symbolic link. |
| 654 | // TODO: implement this for windows too. |
| 655 | pub fn readlink(path string) !string { |
| 656 | return error('${@METHOD} not yet supported on windows') |
| 657 | } |
| 658 | |
| 659 | pub fn link(origin string, target string) ! { |
| 660 | res := C.CreateHardLinkW(target.to_wide(), origin.to_wide(), C.NULL) |
| 661 | // 1 = success, != 1 failure => https://stackoverflow.com/questions/33010440/createsymboliclink-on-windows-10 |
| 662 | if res != 1 { |
| 663 | return error(get_error_msg(int(C.GetLastError()))) |
| 664 | } |
| 665 | if !exists(target) { |
| 666 | return error('C.CreateHardLinkW reported success, but link still does not exist') |
| 667 | } |
| 668 | } |
| 669 | |
| 670 | pub fn (mut f File) close() { |
| 671 | if !f.is_opened { |
| 672 | return |
| 673 | } |
| 674 | f.is_opened = false |
| 675 | cfile := f.cfile |
| 676 | f.cfile = unsafe { nil } |
| 677 | C.fflush(cfile) |
| 678 | C.fclose(cfile) |
| 679 | } |
| 680 | |
| 681 | pub struct ExceptionRecord { |
| 682 | pub: |
| 683 | // status_ constants |
| 684 | code u32 |
| 685 | flags u32 |
| 686 | record &ExceptionRecord = unsafe { nil } |
| 687 | address voidptr |
| 688 | param_count u32 |
| 689 | // params []voidptr |
| 690 | } |
| 691 | |
| 692 | pub struct ContextRecord { |
| 693 | // TODO |
| 694 | } |
| 695 | |
| 696 | pub struct ExceptionPointers { |
| 697 | pub: |
| 698 | exception_record &ExceptionRecord = unsafe { nil } |
| 699 | context_record &ContextRecord = unsafe { nil } |
| 700 | } |
| 701 | |
| 702 | pub type VectoredExceptionHandler = fn (&ExceptionPointers) u32 |
| 703 | |
| 704 | // This is defined in builtin because we use vectored exception handling |
| 705 | // for our unhandled exception handler on windows |
| 706 | // As a result this definition is commented out to prevent |
| 707 | // duplicate definitions from displeasing the compiler |
| 708 | // fn C.AddVectoredExceptionHandler(u32, VectoredExceptionHandler) |
| 709 | pub fn add_vectored_exception_handler(first bool, handler VectoredExceptionHandler) { |
| 710 | C.AddVectoredExceptionHandler(u32(first), voidptr(handler)) |
| 711 | } |
| 712 | |
| 713 | // uname returns information about the platform on which the program is running. |
| 714 | // Currently `uname` on windows is not standardized, so it just mimics current practices from other popular software/language implementations: |
| 715 | // busybox-v1.35.0 * `busybox uname -a` => "Windows_NT HOSTNAME 10.0 19044 x86_64 MS/Windows" |
| 716 | // rust/coreutils-v0.0.17 * `coreutils uname -a` => `Windows_NT HOSTNAME 10.0 19044 x86_64 MS/Windows (Windows 10)` |
| 717 | // Python3 => `uname_result(system='Windows', node='HOSTNAME', release='10', version='10.0.19044', machine='AMD64')` |
| 718 | // See: [NT Version Info](https://en.wikipedia.org/wiki/Windows_NT) @@ <https://archive.is/GnnvF> |
| 719 | // and: [NT Version Info (detailed)](https://en.wikipedia.org/wiki/Comparison_of_Microsoft_Windows_versions#NT_Kernel-based_2) |
| 720 | pub fn uname() Uname { |
| 721 | nodename := hostname() or { '' } |
| 722 | // ToDO: environment variables have low reliability; check for another quick way |
| 723 | machine := |
| 724 | getenv('PROCESSOR_ARCHITECTURE') // * note: 'AMD64' == 'x86_64' (not standardized, but 'x86_64' use is more common; but, python == 'AMD64') |
| 725 | version_info := execute('cmd /d/c ver').output |
| 726 | version_n := (version_info.split(' '))[3].replace(']', '').trim_space() |
| 727 | return Uname{ |
| 728 | sysname: 'Windows_NT' // as of 2022-12, WinOS has only two possible kernels ~ 'Windows_NT' or 'Windows_9x' |
| 729 | nodename: nodename |
| 730 | machine: machine.trim_space() |
| 731 | release: (version_n.split('.'))[0..2].join('.').trim_space() // Major.minor-only == "primary"/release version |
| 732 | version: (version_n.split('.'))[2].trim_space() |
| 733 | } |
| 734 | } |
| 735 | |
| 736 | pub fn hostname() !string { |
| 737 | hostname := [255]u16{} |
| 738 | size := u32(255) |
| 739 | res := C.GetComputerNameW(&hostname[0], voidptr(&size)) |
| 740 | if !res { |
| 741 | return error(get_error_msg(int(C.GetLastError()))) |
| 742 | } |
| 743 | return unsafe { string_from_wide(&hostname[0]) } |
| 744 | } |
| 745 | |
| 746 | pub fn loginname() !string { |
| 747 | loginname := [255]u16{} |
| 748 | size := u32(255) |
| 749 | res := C.GetUserNameW(&loginname[0], voidptr(&size)) |
| 750 | if !res { |
| 751 | return error(get_error_msg(int(C.GetLastError()))) |
| 752 | } |
| 753 | return unsafe { string_from_wide(&loginname[0]) } |
| 754 | } |
| 755 | |
| 756 | // ensure_folder_is_writable checks that `folder` exists, and is writable to the process, by creating an empty file in it, then deleting it. |
| 757 | pub fn ensure_folder_is_writable(folder string) ! { |
| 758 | if !exists(folder) { |
| 759 | return error_with_code('`${folder}` does not exist', 1) |
| 760 | } |
| 761 | if !is_dir(folder) { |
| 762 | return error_with_code('`folder` is not a folder', 2) |
| 763 | } |
| 764 | tmp_folder_name := 'tmp_perm_check_pid_' + getpid().str() |
| 765 | tmp_perm_check := join_path_single(folder, tmp_folder_name) |
| 766 | write_file(tmp_perm_check, 'test') or { |
| 767 | return error_with_code('cannot write to folder "${folder}": ${err}', 3) |
| 768 | } |
| 769 | rm(tmp_perm_check)! |
| 770 | } |
| 771 | |
| 772 | @[inline] |
| 773 | pub fn getpid() int { |
| 774 | return C._getpid() |
| 775 | } |
| 776 | |
| 777 | @[inline] |
| 778 | pub fn getppid() int { |
| 779 | return 0 |
| 780 | } |
| 781 | |
| 782 | @[inline] |
| 783 | pub fn getuid() int { |
| 784 | return 0 |
| 785 | } |
| 786 | |
| 787 | @[inline] |
| 788 | pub fn geteuid() int { |
| 789 | return 0 |
| 790 | } |
| 791 | |
| 792 | @[inline] |
| 793 | pub fn getgid() int { |
| 794 | return 0 |
| 795 | } |
| 796 | |
| 797 | @[inline] |
| 798 | pub fn getegid() int { |
| 799 | return 0 |
| 800 | } |
| 801 | |
| 802 | pub fn posix_set_permission_bit(path_s string, mode u32, enable bool) { |
| 803 | // windows has no concept of a permission mask, so do nothing |
| 804 | } |
| 805 | |
| 806 | fn C.GetLongPathName(short_path &u16, long_path &u16, long_path_bufsize u32) u32 |
| 807 | |
| 808 | // get_long_path has no meaning for *nix, but has for windows, where `c:\folder\some~1` for example |
| 809 | // can be the equivalent of `c:\folder\some spa ces`. On *nix, it just returns a copy of the input path. |
| 810 | fn get_long_path(path string) !string { |
| 811 | if !path.contains('~') { |
| 812 | return path |
| 813 | } |
| 814 | input_short_path := path.to_wide() |
| 815 | defer { |
| 816 | unsafe { free(input_short_path) } |
| 817 | } |
| 818 | long_path_buf := [4096]u16{} |
| 819 | res := C.GetLongPathName(input_short_path, &long_path_buf[0], sizeof(long_path_buf)) |
| 820 | if res == 0 { |
| 821 | return error(get_error_msg(int(C.GetLastError()))) |
| 822 | } |
| 823 | long_path := wide_ptr_to_string(&long_path_buf[0]) |
| 824 | return long_path |
| 825 | } |
| 826 | |
| 827 | // page_size returns the page size in bytes. |
| 828 | pub fn page_size() int { |
| 829 | sinfo := C.SYSTEM_INFO{} |
| 830 | C.GetSystemInfo(&sinfo) |
| 831 | return int(sinfo.dwPageSize) |
| 832 | } |
| 833 | |
| 834 | // disk_usage returns disk usage of `path`. |
| 835 | pub fn disk_usage(path string) !DiskUsage { |
| 836 | mut free_bytes_available_to_caller := u64(0) |
| 837 | mut total := u64(0) |
| 838 | mut available := u64(0) |
| 839 | mut ret := false |
| 840 | if path == '.' || path == '' { |
| 841 | ret = C.GetDiskFreeSpaceExA(&char(unsafe { nil }), &free_bytes_available_to_caller, &total, |
| 842 | &available) |
| 843 | } else { |
| 844 | ret = C.GetDiskFreeSpaceExA(&char(path.str), &free_bytes_available_to_caller, &total, |
| 845 | &available) |
| 846 | } |
| 847 | if ret == false { |
| 848 | return error('cannot get disk usage of path') |
| 849 | } |
| 850 | return DiskUsage{ |
| 851 | total: total |
| 852 | available: available |
| 853 | used: total - available |
| 854 | } |
| 855 | } |
| 856 | |