v2 / vlib / os / os.c.v
1263 lines · 1176 sloc · 35.29 KB · a8c01eb5f0eebd14f53d4ccc9e227803169d1e6b
Raw
1module os
2
3import strings
4
5#include <sys/stat.h> // #include <signal.h>
6#include <errno.h>
7
8$if macos {
9 #include <mach-o/dyld.h>
10 // needed for os.executable():
11 fn C._dyld_get_image_name(image u32) &char
12}
13
14$if freebsd || openbsd {
15 #include <sys/sysctl.h>
16}
17
18@[markused]
19pub const args = arguments()
20
21fn C.readdir(voidptr) &C.dirent
22
23fn C.readlink(pathname &char, buf &char, bufsiz usize) i32
24
25fn C.getline(voidptr, voidptr, voidptr) i32
26
27fn C.sigaction(i32, voidptr, i32) i32
28
29fn C.open(&char, i32, ...int) i32
30
31fn C._wopen(&u16, i32, ...int) i32
32
33fn C.fdopen(fd i32, mode &char) &C.FILE
34
35fn C.ferror(stream &C.FILE) i32
36
37fn C.feof(stream &C.FILE) i32
38
39fn C.CopyFile(&u16, &u16, bool) i32
40
41fn write_file_direct(path string, text string) ! {
42 mut f := create(path)!
43 defer {
44 f.close()
45 }
46 mut sp := text.str
47 mut remaining := text.len
48 for remaining > 0 {
49 C.errno = 0
50 written := int(C.write(f.fd, sp, remaining))
51 if written < 0 {
52 cerror := int(C.errno)
53 if cerror == C.EINTR {
54 continue
55 }
56 return error(posix_get_error_msg(cerror))
57 }
58 if written == 0 {
59 return error('0 bytes written')
60 }
61 remaining -= written
62 sp = unsafe { &u8(voidptr(usize(sp) + usize(written))) }
63 }
64}
65
66// fn C.lstat(charptr, voidptr) u64
67
68fn C._wstat64(&u16, voidptr) u64
69
70fn C.chown(&char, i32, i32) i32
71
72fn C.ftruncate(voidptr, u64) i32
73
74fn C._chsize_s(voidptr, u64) i32
75
76// read_bytes returns all bytes read from file in `path`.
77@[manualfree]
78pub fn read_bytes(path string) ![]u8 {
79 mut fp := vfopen(path, 'rb')!
80 defer {
81 C.fclose(fp)
82 }
83 fsize := find_cfile_size(fp)!
84 if fsize == 0 {
85 mut sb := slurp_file_in_builder(fp)!
86 return unsafe { sb.reuse_as_plain_u8_array() }
87 }
88 mut res := []u8{len: fsize}
89 nr_read_elements := int(C.fread(res.data, 1, fsize, fp))
90 if nr_read_elements == 0 && fsize > 0 {
91 return error('fread failed')
92 }
93 res.trim(nr_read_elements)
94 return res
95}
96
97// write_bytes writes all the `bytes` to `path`.
98@[manualfree]
99pub fn write_bytes(path string, bytes []u8) ! {
100 write_file_array(path, bytes)!
101}
102
103fn find_cfile_size(fp &C.FILE) !int {
104 // NB: Musl's fseek returns -1 for virtual files, while Glibc's fseek returns 0
105 cseek := C.fseek(fp, 0, C.SEEK_END)
106 raw_fsize := C.ftell(fp)
107 if raw_fsize != 0 && cseek != 0 {
108 return error('fseek failed')
109 }
110 if raw_fsize < 0 {
111 if cseek != 0 {
112 return error('ftell failed')
113 }
114 // fseek succeeded but ftell returned -1 (e.g. device files like NUL on Windows).
115 // Rewind before returning so the caller can read from the beginning.
116 C.rewind(fp)
117 return 0
118 }
119 len := int(raw_fsize)
120 // For files > 2GB, C.ftell can return values that, when cast to `int`, can result in values below 0.
121 if i64(len) < raw_fsize {
122 return error('int(${raw_fsize}) cast results in ${len}')
123 }
124 C.rewind(fp)
125 return len
126}
127
128const buf_size = 4096
129
130// slurp_file_in_builder reads an entire file into a strings.Builder chunk by chunk, without relying on its file size.
131// It is intended for reading 0 sized files, or a dynamic files in a virtual filesystem like /proc/cpuinfo.
132// For these, we can not allocate all memory in advance (since we do not know the final size), and so we have no choice
133// but to read the file in `buf_size` chunks.
134@[manualfree]
135fn slurp_file_in_builder(fp &C.FILE) !strings.Builder {
136 buf := [buf_size]u8{}
137 mut sb := strings.new_builder(buf_size)
138 for {
139 mut read_bytes := fread(&buf[0], 1, buf_size, fp) or {
140 if err is Eof {
141 break
142 }
143 unsafe { sb.free() }
144 return err
145 }
146 unsafe { sb.write_ptr(&buf[0], read_bytes) }
147 }
148 return sb
149}
150
151// read_file reads the file in `path` and returns the contents.
152@[manualfree]
153pub fn read_file(path string) !string {
154 mode := 'rb'
155 mut fp := vfopen(path, mode)!
156 defer {
157 C.fclose(fp)
158 }
159 allocate := find_cfile_size(fp)!
160 if allocate == 0 {
161 mut sb := slurp_file_in_builder(fp)!
162 res := sb.str()
163 unsafe { sb.free() }
164 return res
165 }
166 unsafe {
167 mut str := malloc_noscan(allocate + 1)
168 nelements := int(C.fread(str, 1, allocate, fp))
169 is_eof := int(C.feof(fp))
170 is_error := int(C.ferror(fp))
171 if is_eof == 0 && is_error != 0 {
172 free(str)
173 return error('fread failed')
174 }
175 str[nelements] = 0
176 if nelements == 0 {
177 // It is highly likely that the file was a virtual file from
178 // /sys or /proc, with information generated on the fly, so
179 // fsize was not reliably reported. Using vstring() here is
180 // slower (it calls strlen internally), but will return more
181 // consistent results.
182 // For example reading from /sys/class/sound/card0/id produces
183 // a `PCH\n` string, but fsize is 4096, and otherwise you would
184 // get a V string with .len = 4096 and .str = "PCH\n\\000".
185 return str.vstring()
186 }
187 return str.vstring_with_len(nelements)
188 }
189}
190
191// truncate changes the size of the file located in `path` to `len`.
192// Note that changing symbolic links on Windows only works as admin.
193pub fn truncate(path string, len u64) ! {
194 fp := $if windows {
195 C._wopen(path.to_wide(), o_wronly | o_trunc, 0)
196 } $else {
197 C.open(&char(path.str), o_wronly | o_trunc, 0)
198 }
199 if fp < 0 {
200 return error_posix()
201 }
202 defer {
203 C.close(fp)
204 }
205 $if windows {
206 if C._chsize_s(fp, len) != 0 {
207 return error_posix()
208 }
209 } $else {
210 if C.ftruncate(fp, len) != 0 {
211 return error_posix()
212 }
213 }
214}
215
216// file_size returns the size in bytes, of the file located in `path`.
217// If an error occurs it returns 0.
218// Note that use of this on symbolic links on Windows returns always 0.
219pub fn file_size(path string) u64 {
220 attr := stat(path) or {
221 eprintln('os.file_size() Cannot determine file-size: ' + posix_get_error_msg(C.errno))
222 return 0
223 }
224 return attr.size
225}
226
227// rename_dir renames the folder from `src` to `dst`.
228// Use mv to move or rename a file in a platform independent manner.
229pub fn rename_dir(src string, dst string) ! {
230 $if windows {
231 w_src := src.replace('/', '\\')
232 w_dst := dst.replace('/', '\\')
233 ret := C._wrename(w_src.to_wide(), w_dst.to_wide())
234 if ret != 0 {
235 return error_with_code('failed to rename ${src} to ${dst}', int(ret))
236 }
237 } $else {
238 ret := C.rename(&char(src.str), &char(dst.str))
239 if ret != 0 {
240 return error_with_code('failed to rename ${src} to ${dst}', ret)
241 }
242 }
243}
244
245// rename renames the file or folder from `src` to `dst`.
246// Use mv to move or rename a file in a platform independent manner.
247pub fn rename(src string, dst string) ! {
248 mut rdst := dst
249 if is_dir(rdst) {
250 rdst = join_path_single(rdst.trim_right(path_separator),
251 file_name(src.trim_right(path_separator)))
252 }
253 $if windows {
254 w_src := src.replace('/', '\\')
255 w_dst := rdst.replace('/', '\\')
256 ret := C._wrename(w_src.to_wide(), w_dst.to_wide())
257 if ret != 0 {
258 return error_with_code('failed to rename ${src} to ${dst}', int(ret))
259 }
260 } $else {
261 ret := C.rename(&char(src.str), &char(rdst.str))
262 if ret != 0 {
263 return error_with_code('failed to rename ${src} to ${dst}', ret)
264 }
265 }
266}
267
268@[params]
269pub struct CopyParams {
270pub:
271 fail_if_exists bool
272}
273
274// cp copies the file src to the file or directory dst. If dst specifies a directory, the file will be copied into dst
275// using the base filename from src. If dst specifies a file that already exists, it will be replaced by
276// default. Can be overridden to fail by setting fail_if_exists: true
277pub fn cp(src string, dst string, config CopyParams) ! {
278 $if windows {
279 w_src := src.replace('/', '\\')
280 mut w_dst := dst.replace('/', '\\')
281 if is_dir(w_dst) {
282 w_dst = join_path_single(w_dst, file_name(w_src))
283 }
284 if C.CopyFile(w_src.to_wide(), w_dst.to_wide(), config.fail_if_exists) == 0 {
285 // we must save error immediately, or it will be overwritten by other API function calls.
286 code := int(C.GetLastError())
287 return error_win32(
288 msg: 'cp: failed to copy ${src} to ${dst}'
289 code: code
290 )
291 }
292 } $else {
293 mut w_dst := dst
294 if is_dir(dst) {
295 w_dst = join_path_single(w_dst, file_name(src))
296 }
297 fp_from := C.open(&char(src.str), C.O_RDONLY, 0)
298 if fp_from < 0 { // Check if file opened
299 return error_with_code('cp: failed to open ${src} for reading', int(fp_from))
300 }
301 mode_flags := C.S_IWUSR | C.S_IRUSR
302 mut open_flags := C.O_WRONLY | C.O_CREAT | C.O_TRUNC
303 if config.fail_if_exists {
304 open_flags |= C.O_EXCL
305 }
306 fp_to := C.open(&char(w_dst.str), open_flags, mode_flags)
307 if fp_to < 0 { // Check if file opened (permissions problems ...)
308 C.close(fp_from)
309 return error_with_code('cp: failed to open ${w_dst} for writing', int(fp_to))
310 }
311 // TODO: use defer{} to close files in case of error or return.
312 // Currently there is a C-Error when building.
313 mut buf := [1024]u8{}
314 mut count := int(0)
315 for {
316 count = int(C.read(fp_from, &buf[0], sizeof(buf)))
317 if count == 0 {
318 break
319 }
320 if C.write(fp_to, &buf[0], count) < 0 {
321 C.close(fp_to)
322 C.close(fp_from)
323 return error_with_code('cp: failed to write to ${w_dst}', int(-1))
324 }
325 }
326 from_attr := stat(src)!
327 if C.chmod(&char(w_dst.str), from_attr.mode) < 0 {
328 C.close(fp_to)
329 C.close(fp_from)
330 return error_with_code('failed to set permissions for ${w_dst}', int(-1))
331 }
332 C.close(fp_to)
333 C.close(fp_from)
334 }
335}
336
337// vfopen returns an opened C file, given its path and open mode.
338// Note: os.vfopen is useful for compatibility with C libraries, that expect `FILE *`.
339// If you write pure V code, os.create or os.open are more convenient.
340pub fn vfopen(path string, mode string) !&C.FILE {
341 if path == '' {
342 return error('vfopen called with ""')
343 }
344 mut fp := &C.FILE(unsafe { nil })
345 $if windows {
346 fp = C._wfopen(path.to_wide(), mode.to_wide())
347 } $else {
348 fp = C.fopen(&char(path.str), &char(mode.str))
349 }
350 if isnil(voidptr(fp)) {
351 return error_posix(msg: 'failed to open file "${path}"')
352 }
353 return fp
354}
355
356// fileno returns the file descriptor of an opened C file.
357pub fn fileno(cfile voidptr) int {
358 $if windows {
359 return C._fileno(cfile)
360 } $else {
361 // Required on FreeBSD/OpenBSD/NetBSD as stdio.h defines fileno(..) with a macro
362 // that performs a field access on its argument without casting from void*.
363 return C.fileno(unsafe { &C.FILE(cfile) })
364 }
365}
366
367// vpopen system starts the specified command, waits for it to complete, and returns its code.
368fn vpopen(path string) voidptr {
369 // *C.FILE {
370 $if windows {
371 mode := 'rb'
372 wpath := path.to_wide()
373 return C._wpopen(wpath, mode.to_wide())
374 } $else {
375 cpath := path.str
376 return C.popen(&char(cpath), c'r')
377 }
378}
379
380fn posix_wait_status_exited(status int) bool {
381 return (status & 0x7f) == 0
382}
383
384fn posix_wait_status_exit_code(status int) int {
385 return (status >> 8) & 0xff
386}
387
388fn posix_wait_status_signaled(status int) bool {
389 signal := status & 0x7f
390 return signal != 0 && signal != 0x7f
391}
392
393fn posix_wait_status_signal(status int) int {
394 return status & 0x7f
395}
396
397fn posix_wait4_to_exit_status(waitret int) (int, bool) {
398 $if windows {
399 return waitret, false
400 } $else {
401 mut ret := 0
402 mut is_signaled := true
403 if posix_wait_status_exited(waitret) {
404 ret = posix_wait_status_exit_code(waitret)
405 is_signaled = false
406 } else if posix_wait_status_signaled(waitret) {
407 ret = posix_wait_status_signal(waitret)
408 is_signaled = true
409 }
410 return ret, is_signaled
411 }
412}
413
414// posix_get_error_msg returns error code representation in string.
415pub fn posix_get_error_msg(code int) string {
416 ptr_text := C.strerror(code) // voidptr?
417 if ptr_text == 0 {
418 return ''
419 }
420 return unsafe { tos3(ptr_text) }
421}
422
423// vpclose will close a file pointer opened with `vpopen`.
424fn vpclose(f voidptr) int {
425 $if windows {
426 return C._pclose(f)
427 } $else {
428 ret, _ := posix_wait4_to_exit_status(C.pclose(f))
429 return ret
430 }
431}
432
433// system works like `exec`, but only returns a return code.
434pub fn system(cmd string) int {
435 // if cmd.contains(';') || cmd.contains('&&') || cmd.contains('||') || cmd.contains('\n') {
436 // TODO: remove panic
437 // panic(';, &&, || and \\n are not allowed in shell commands')
438 // }
439 mut ret := 0
440 $if windows {
441 // overcome bug in system & _wsystem (cmd) when first char is quote `"`
442 wcmd := if cmd.len > 1 && cmd[0] == `"` && cmd[1] != `"` { '"${cmd}"' } else { cmd }
443 flush_stdout()
444 flush_stderr()
445 unsafe {
446 ret = C._wsystem(wcmd.to_wide())
447 }
448 } $else {
449 $if ios {
450 unsafe {
451 arg := [c'/bin/sh', c'-c', &u8(cmd.str), 0]
452 pid := 0
453 ret = C.posix_spawn(&pid, c'/bin/sh', 0, 0, arg.data, 0)
454 status := 0
455 ret = C.waitpid(pid, &status, 0)
456 if posix_wait_status_exited(status) {
457 ret = posix_wait_status_exit_code(status)
458 }
459 }
460 } $else {
461 unsafe {
462 ret = C.system(&char(cmd.str))
463 }
464 }
465 }
466 if ret == -1 {
467 print_c_errno()
468 }
469 $if !windows {
470 pret, is_signaled := posix_wait4_to_exit_status(ret)
471 ret = pret
472 if is_signaled {
473 eprintln('Terminated by signal ${pret:2d} (' + sigint_to_signal_name(pret) + ')')
474 ret = pret + 128
475 }
476 }
477 return ret
478}
479
480// exists returns true if `path` (file or directory) exists.
481//
482// Note that when used on symlinks, if the target of the symlink does not exist, the behavior of this function is complex.
483// On linux, mac, and similar systems, on such a symlink, this function returns false.
484// (This may make sense, in the sense that such a path does not contain anything readable.
485// It is also the same as the libc 'access' function, and may be familiar from that regard.
486// On the other hand, this case may be surprising, since it means this function can return "false" for a path that exists enough
487// that trying to create a new file or dir there subsequently (even aside from TOCTOU issues) will fail with EEXIST.)
488// On windows systems, a symlink with a target that does not exist, this function returns true.
489// Consider using lstat() for a less ambiguous result about whether a path is occupied or not;
490// whether lstat returns any result or a "not exists" error gives a clear indication of whether the path is occupied.
491pub fn exists(path string) bool {
492 $if windows {
493 p := path.replace('/', '\\')
494 return C._waccess(p.to_wide(), f_ok) != -1
495 } $else {
496 return C.access(&char(path.str), f_ok) != -1
497 }
498}
499
500// is_executable returns `true` if `path` is executable.
501// Warning: `is_executable()` is known to cause a TOCTOU vulnerability when used incorrectly
502// (for more information: https://github.com/vlang/v/blob/master/vlib/os/README.md).
503pub fn is_executable(path string) bool {
504 $if windows {
505 // Note: https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/access-waccess?view=vs-2019
506 // i.e. there is no X bit there, the modes can be:
507 // 00 Existence only
508 // 02 Write-only
509 // 04 Read-only
510 // 06 Read and write
511 p := real_path(path)
512 if !exists(p) {
513 return false
514 }
515 ext := p.to_lower().all_after_last('.')
516 // Note: Extensions like 'ps1', 'vbs', 'js', 'msi', 'scr', 'pif' require specific interpreters and are not directly executable
517 return ext in ['exe', 'com', 'bat', 'cmd']
518 }
519 $if solaris {
520 attr := stat(path) or { return false }
521 return (int(attr.mode) & (s_ixusr | s_ixgrp | s_ixoth)) != 0
522 }
523 return C.access(&char(path.str), x_ok) != -1
524}
525
526// is_writable returns `true` if `path` is writable.
527// Warning: `is_writable()` is known to cause a TOCTOU vulnerability when used incorrectly
528// (for more information: https://github.com/vlang/v/blob/master/vlib/os/README.md).
529@[manualfree]
530pub fn is_writable(path string) bool {
531 $if windows {
532 p := path.replace('/', '\\')
533 wp := p.to_wide()
534 res := C._waccess(wp, w_ok) != -1
535 unsafe { free(wp) } // &u16
536 unsafe { p.free() }
537 return res
538 } $else {
539 return C.access(&char(path.str), w_ok) != -1
540 }
541}
542
543// is_readable returns `true` if `path` is readable.
544// Warning: `is_readable()` is known to cause a TOCTOU vulnerability when used incorrectly
545// (for more information: https://github.com/vlang/v/blob/master/vlib/os/README.md).
546@[manualfree]
547pub fn is_readable(path string) bool {
548 $if windows {
549 p := path.replace('/', '\\')
550 wp := p.to_wide()
551 res := C._waccess(wp, r_ok) != -1
552 unsafe { free(wp) } // &u16
553 unsafe { p.free() }
554 return res
555 } $else {
556 return C.access(&char(path.str), r_ok) != -1
557 }
558}
559
560// rm removes file in `path`.
561pub fn rm(path string) ! {
562 mut rc := 0
563 $if windows {
564 rc = C._wremove(path.to_wide())
565 } $else {
566 rc = C.remove(&char(path.str))
567 }
568 if rc == -1 {
569 return error_posix(msg: 'Failed to remove "${path}": ' + posix_get_error_msg(C.errno))
570 }
571 // C.unlink(path.cstr())
572}
573
574// rmdir removes a specified directory.
575pub fn rmdir(path string) ! {
576 $if windows {
577 rc := C.RemoveDirectory(path.to_wide())
578 if !rc {
579 // https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-removedirectorya - 0 == false, is failure
580 // we must save error immediately, or it will be overwritten by other API function calls.
581 code := int(C.GetLastError())
582 return error_win32(
583 msg: 'Failed to remove "${path}"'
584 code: code
585 )
586 }
587 } $else {
588 rc := C.rmdir(&char(path.str))
589 if rc == -1 {
590 return error_posix()
591 }
592 }
593}
594
595// print_c_errno will print the current value of `C.errno`.
596fn print_c_errno() {
597 e := C.errno
598 se := unsafe { cstring_to_vstring(C.strerror(e)) }
599 eprintln('errno=${e} err=${se}')
600}
601
602// get_raw_line returns a one-line string from stdin along with `\n` if there is any.
603@[manualfree]
604pub fn get_raw_line() string {
605 $if windows {
606 is_console := is_atty(0) > 0
607 wide_char_size := if is_console { 2 } else { 1 }
608 h_input := C.GetStdHandle(C.STD_INPUT_HANDLE)
609 if h_input == C.INVALID_HANDLE_VALUE {
610 return ''
611 }
612 unsafe {
613 initial_size := 256 * wide_char_size
614 mut buf := malloc_noscan(initial_size)
615 defer { unsafe { buf.free() } }
616 mut capacity := initial_size
617 mut offset := 0
618
619 for {
620 required_space := offset + wide_char_size
621 if required_space > capacity {
622 new_capacity := capacity * 2
623 new_buf := realloc_data(buf, capacity, new_capacity)
624 if new_buf == 0 {
625 break
626 }
627 buf = new_buf
628 capacity = new_capacity
629 }
630
631 pos := buf + offset
632 mut bytes_read := u32(0)
633 res := if is_console {
634 C.ReadConsole(h_input, pos, 1, voidptr(&bytes_read), 0)
635 } else {
636 C.ReadFile(h_input, pos, 1, voidptr(&bytes_read), 0)
637 }
638
639 if !res || bytes_read == 0 {
640 break
641 }
642
643 // check for `\n` and Ctrl+Z
644 if is_console {
645 read_char := *(&u16(pos))
646 if read_char == `\n` {
647 offset += wide_char_size
648 break
649 } else if read_char == 0x1A {
650 break
651 }
652 } else {
653 read_byte := *pos
654 if read_byte == `\n` {
655 offset += wide_char_size
656 break
657 } else if read_byte == 0x1A {
658 break
659 }
660 }
661
662 offset += wide_char_size
663 }
664
665 return if is_console {
666 string_from_wide2(&u16(buf), offset / 2)
667 } else {
668 // let defer buf.free() to avoid memory leak
669 buf.vstring_with_len(offset).clone()
670 }
671 }
672 } $else {
673 max := usize(0)
674 buf := &u8(unsafe { nil })
675
676 mut str := ''
677 nr_chars := unsafe { C.getline(voidptr(&buf), &max, C.stdin) }
678 if nr_chars >= 0 && buf != 0 {
679 str = unsafe { tos(buf, nr_chars) }
680 } else if int(C.feof(C.stdin)) == 0 && int(C.ferror(C.stdin)) != 0 {
681 panic('get_raw_line(): error reading from stdin')
682 }
683 ret := str.clone()
684 $if !autofree {
685 unsafe {
686 if buf != 0 {
687 C.free(buf)
688 }
689 }
690 }
691 return ret
692 }
693}
694
695// get_raw_stdin will get the raw input from stdin.
696pub fn get_raw_stdin() []u8 {
697 $if windows {
698 unsafe {
699 block_bytes := 512
700 mut old_size := block_bytes
701 mut buf := malloc_noscan(block_bytes)
702 h_input := C.GetStdHandle(C.STD_INPUT_HANDLE)
703 mut bytes_read := 0
704 mut offset := 0
705 for {
706 pos := buf + offset
707 res := C.ReadFile(h_input, pos, block_bytes, voidptr(&bytes_read), 0)
708 offset += bytes_read
709 if !res {
710 break
711 }
712 new_size := offset + block_bytes + (block_bytes - bytes_read)
713 buf = realloc_data(buf, old_size, new_size)
714 old_size = new_size
715 }
716 return array{
717 element_size: 1
718 data: voidptr(buf)
719 len: offset
720 cap: offset
721 }
722 }
723 } $else {
724 max := usize(0)
725 buf := &u8(unsafe { nil })
726 nr_chars := unsafe { C.getline(voidptr(&buf), &max, C.stdin) }
727 return array{
728 element_size: 1
729 data: voidptr(buf)
730 len: if nr_chars < 0 { 0 } else { nr_chars }
731 cap: int(max)
732 }
733 }
734}
735
736// read_file_array reads an array of `T` values from file `path`.
737pub fn read_file_array[T](path string) []T {
738 a := T{}
739 tsize := int(sizeof(a))
740 // prepare for reading, get current file size
741 mut fp := vfopen(path, 'rb') or { return []T{} }
742 C.fseek(fp, 0, C.SEEK_END)
743 fsize := C.ftell(fp)
744 C.rewind(fp)
745 // read the actual data from the file
746 len := fsize / tsize
747 allocate := int(fsize)
748 // On some systems C.ftell can return values in the 64-bit range
749 // that, when cast to `int`, can result in values below 0.
750 if i64(allocate) < fsize {
751 panic_n2('cast to int results in (fsize, int(fsize)):', i64(fsize), i64(int(fsize)))
752 }
753 buf := unsafe {
754 malloc_noscan(allocate)
755 }
756 nread := C.fread(buf, tsize, len, fp)
757 C.fclose(fp)
758 return unsafe {
759 array{
760 element_size: tsize
761 data: buf
762 len: int(nread)
763 cap: int(len)
764 }
765 }
766}
767
768// executable returns the path name of the executable that started the current
769// process.
770@[manualfree]
771pub fn executable() string {
772 mut result := [max_path_buffer_size]u8{}
773 $if windows {
774 pu16_result := unsafe { &u16(&result[0]) }
775 len := C.GetModuleFileName(0, pu16_result, 512)
776 // determine if the file is a windows symlink
777 attrs := C.GetFileAttributesW(pu16_result)
778 is_set := attrs & 0x400 // FILE_ATTRIBUTE_REPARSE_POINT
779 if is_set != 0 { // it's a windows symlink
780 // gets handle with GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0
781 file := C.CreateFile(pu16_result, 0x80000000, 1, 0, 3, 0x80, 0)
782 if file != voidptr(-1) {
783 defer {
784 C.CloseHandle(file)
785 }
786 final_path := [max_path_buffer_size]u8{}
787 // https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfinalpathnamebyhandlew
788 final_len := C.GetFinalPathNameByHandleW(file, unsafe { &u16(&final_path[0]) },
789 max_path_buffer_size, 0)
790 if final_len < u32(max_path_buffer_size) && final_len != 0 {
791 sret := unsafe { string_from_wide2(&u16(&final_path[0]), int(final_len)) }
792 defer {
793 unsafe { sret.free() }
794 }
795 return normalize_windows_extended_path_prefix(sret)
796 } else if final_len != 0 {
797 eprintln('os.executable() saw that the executable file path was too long')
798 }
799 }
800 }
801 res := unsafe { string_from_wide2(pu16_result, int(len)) }
802 return res
803 }
804 $if macos {
805 self_path := &char(C._dyld_get_image_name(u32(0)))
806 if self_path == C.NULL {
807 return executable_fallback()
808 }
809 return unsafe { cstring_to_vstring(self_path) }
810 }
811 $if freebsd {
812 bufsize := usize(max_path_buffer_size)
813 mib := [C.CTL_KERN, C.KERN_PROC, C.KERN_PROC_PATHNAME, -1]!
814 unsafe { C.sysctl(&mib[0], mib.len, &result[0], &bufsize, 0, 0) }
815 res := unsafe { tos_clone(&result[0]) }
816 return res
817 }
818 $if openbsd {
819 // Sadly, unlike on FreeBSD, there is still no reliable way, to get the full path of the
820 // current process in OpenBSD. However, we can try our best, by first checking, if the passed
821 // argv[0] to the process, contains an absolute path (starting with /), according to the kernel,
822 // and only go use the slower PATH scanning fallback method, when it does not.
823 // See also https://github.com/gpakosz/whereami/blob/master/src/whereami.c#L591
824 // and https://github.com/ziglang/zig/issues/6718#issuecomment-711134120 .
825 mut pbuf := unsafe { &&u8(&result[0]) }
826 bufsize := usize(max_path_buffer_size)
827 pid := C.getpid()
828 mib := [C.CTL_KERN, C.KERN_PROC_ARGS, pid, C.KERN_PROC_ARGV]!
829 if unsafe { C.sysctl(&mib[0], mib.len, C.NULL, &bufsize, C.NULL, 0) } == 0 {
830 if bufsize > max_path_buffer_size {
831 pbuf = unsafe { &&u8(malloc(int(bufsize))) }
832 defer(fn) {
833 unsafe { free(pbuf) }
834 }
835 }
836 if unsafe { C.sysctl(&mib[0], mib.len, pbuf, &bufsize, C.NULL, 0) } == 0 {
837 if unsafe { *pbuf[0] } == `/` {
838 res := unsafe { tos_clone(pbuf[0]) }
839 return res
840 }
841 }
842 }
843 return executable_fallback()
844 }
845 $if netbsd {
846 count := C.readlink(c'/proc/curproc/exe', &char(&result[0]), max_path_len)
847 if count < 0 {
848 eprintln('os.executable() failed at reading /proc/curproc/exe to get exe path')
849 return executable_fallback()
850 }
851 res := unsafe { tos_clone(&result[0]) }
852 return res
853 }
854 $if dragonfly {
855 count := C.readlink(c'/proc/curproc/file', &char(&result[0]), max_path_len)
856 if count < 0 {
857 eprintln('os.executable() failed at reading /proc/curproc/file to get exe path')
858 return executable_fallback()
859 }
860 res := unsafe { tos_clone(&result[0]) }
861 return res
862 }
863 $if linux {
864 count := C.readlink(c'/proc/self/exe', &char(&result[0]), max_path_len)
865 if count < 0 {
866 eprintln('os.executable() failed at reading /proc/self/exe to get exe path')
867 return executable_fallback()
868 }
869 res := unsafe { tos_clone(&result[0]) }
870 return res
871 }
872 $if solaris {
873 }
874 $if haiku {
875 }
876 return executable_fallback()
877}
878
879struct PathKind {
880mut:
881 is_file bool
882 is_dir bool
883 is_link bool
884}
885
886// chdir changes the current working directory to the new directory in `path`.
887pub fn chdir(path string) ! {
888 ret := $if windows { C._wchdir(path.to_wide()) } $else { C.chdir(&char(path.str)) }
889 if ret == -1 {
890 return error_posix()
891 }
892}
893
894// getwd returns the absolute path of the current directory.
895@[manualfree]
896pub fn getwd() string {
897 unsafe {
898 $if windows {
899 buf := [max_path_buffer_size]u8{}
900 if C._wgetcwd(&u16(&buf[0]), max_path_len) == 0 {
901 return ''
902 }
903 res := string_from_wide(&u16(&buf[0]))
904 return res
905 } $else {
906 // Use libc-managed buffer to avoid fixed-array address lowering pitfalls in v2.
907 cwd_ptr := C.getcwd(0, 4096)
908 if cwd_ptr == 0 {
909 return ''
910 }
911 res := tos_clone(byteptr(cwd_ptr))
912 C.free(cwd_ptr)
913 return res
914 }
915 }
916}
917
918// normalize_windows_extended_path_prefix converts Win32 extended-length paths from
919// `GetFinalPathNameByHandleW` back to standard DOS and UNC paths.
920@[manualfree]
921fn normalize_windows_extended_path_prefix(path string) string {
922 $if windows {
923 if path.starts_with('\\\\?\\UNC\\') || path.starts_with('\\\\.\\UNC\\') {
924 return ('\\\\' + path[8..]).clone()
925 }
926 if path.starts_with('\\\\?\\') || path.starts_with('\\\\.\\') {
927 return path[4..].clone()
928 }
929 }
930 return path.clone()
931}
932
933// real_path returns the full absolute path for fpath, with all relative ../../, symlinks and so on resolved.
934// See http://pubs.opengroup.org/onlinepubs/9699919799/functions/realpath.html
935// Also https://insanecoding.blogspot.com/2007/11/pathmax-simply-isnt.html
936// and https://insanecoding.blogspot.com/2007/11/implementing-realpath-in-c.html
937// On Windows, extended-length path prefixes like `\\?\` and `\\?\UNC\` are normalized back
938// to standard DOS and UNC paths.
939// Note: this particular rabbit hole is *deep* ...
940@[manualfree]
941pub fn real_path(fpath string) string {
942 mut fullpath := [max_path_buffer_size]u8{}
943 mut res := ''
944 $if windows {
945 pu16_fullpath := unsafe { &u16(&fullpath[0]) }
946 // gets handle with GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING,
947 // FILE_FLAG_BACKUP_SEMANTICS, 0
948 // FILE_FLAG_BACKUP_SEMANTICS (0x02000000) is needed to open directories
949 // and resolve directory symlinks properly.
950 // try to open the file to get symbolic link path
951 fpath_wide := fpath.to_wide()
952 defer {
953 unsafe { free(voidptr(fpath_wide)) }
954 }
955 file := C.CreateFile(fpath_wide, 0x80000000, 1, 0, 3, 0x02000000, 0)
956 if file != voidptr(-1) {
957 defer { C.CloseHandle(file) }
958 // https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfinalpathnamebyhandlew
959 final_len := C.GetFinalPathNameByHandleW(file, pu16_fullpath, max_path_buffer_size, 0)
960 if final_len < u32(max_path_buffer_size) && final_len != 0 {
961 rt := unsafe { string_from_wide2(pu16_fullpath, int(final_len)) }
962 defer {
963 unsafe { rt.free() }
964 }
965 unsafe { res.free() }
966 res = normalize_windows_extended_path_prefix(rt)
967 } else {
968 if final_len != 0 {
969 eprintln('os.real_path() saw that the file path was too long')
970 }
971 unsafe { res.free() }
972 return fpath.clone()
973 }
974 } else {
975 // if it is not a file C.CreateFile doesn't gets a file handle, use GetFullPath instead
976 ret := C.GetFullPathName(fpath_wide, max_path_len, pu16_fullpath, 0)
977 if ret == 0 {
978 // TODO: check errors if path len is not enough
979 unsafe { res.free() }
980 return fpath.clone()
981 }
982 unsafe { res.free() }
983 res = normalize_windows_extended_path_prefix(unsafe { string_from_wide(pu16_fullpath) })
984 }
985 } $else {
986 ret := &char(C.realpath(&char(fpath.str), &char(&fullpath[0])))
987 if ret == 0 {
988 unsafe { res.free() }
989 return fpath.clone()
990 }
991 // Note: fullpath is much larger (usually ~4KB), than what C.realpath will
992 // actually fill in the vast majority of the cases => it pays to copy the
993 // resulting string from that buffer, to a shorter one, and then free the
994 // 4KB fullpath buffer.
995 unsafe { res.free() }
996 res = unsafe { tos_clone(&fullpath[0]) }
997 }
998 unsafe { normalize_drive_letter(res) }
999 return res
1000}
1001
1002@[direct_array_access; manualfree; unsafe]
1003fn normalize_drive_letter(path string) {
1004 $if !windows {
1005 return
1006 }
1007 // normalize_drive_letter is needed, because
1008 // a path like c:\nv\.bin (note the small `c`) in %PATH,
1009 // is NOT recognized by cmd.exe (and probably other programs too)...
1010 // Capital drive letters do work fine.
1011 // vfmt off
1012 if path.len > 2 && path[0] >= `a` && path[0] <= `z` && path[1] == `:` && path[2] == path_separator[0] {
1013 unsafe {
1014 mut x := &u8(path.str)
1015 x[0] = x[0] - 32
1016 }
1017 }
1018 // vfmt on
1019}
1020
1021// fork will fork the current system process and return the pid of the fork.
1022pub fn fork() int {
1023 mut pid := -1
1024 $if !windows {
1025 pid = C.fork()
1026 }
1027 $if windows {
1028 panic('os.fork not supported in windows') // TODO
1029 }
1030 return pid
1031}
1032
1033// wait blocks the calling process until one of its child processes exits or a signal is received.
1034// After child process terminates, parent continues its execution after wait system call instruction.
1035pub fn wait() int {
1036 mut pid := -1
1037 $if !windows {
1038 $if !emscripten ? {
1039 pid = C.wait(0)
1040 }
1041 }
1042 $if windows {
1043 panic('os.wait not supported in windows') // TODO
1044 }
1045 return pid
1046}
1047
1048// file_last_mod_unix returns the "last modified" time stamp of file in `path`.
1049pub fn file_last_mod_unix(path string) i64 {
1050 if attr := stat(path) {
1051 return attr.mtime
1052 }
1053 return 0
1054}
1055
1056// flush will flush the stdout buffer.
1057pub fn flush() {
1058 C.fflush(C.stdout)
1059}
1060
1061// chmod change file access attributes of `path` to `mode`.
1062// Octals like `0o600` can be used.
1063pub fn chmod(path string, mode int) ! {
1064 if C.chmod(&char(path.str), mode) != 0 {
1065 return error_posix(msg: 'chmod failed: ' + posix_get_error_msg(C.errno))
1066 }
1067}
1068
1069// chown changes the owner and group attributes of `path` to `owner` and `group`.
1070pub fn chown(path string, owner int, group int) ! {
1071 $if windows {
1072 return error('os.chown() not implemented for Windows')
1073 } $else {
1074 if C.chown(&char(path.str), owner, group) != 0 {
1075 return error_posix()
1076 }
1077 }
1078}
1079
1080// open_append tries to open a file from a given path.
1081// If successful, it and returns a `File` for appending.
1082pub fn open_append(path string) !File {
1083 mut file := File{}
1084 $if windows {
1085 wpath := path.replace('/', '\\').to_wide()
1086 mode := 'ab'
1087 file = File{
1088 cfile: C._wfopen(wpath, mode.to_wide())
1089 }
1090 } $else {
1091 cpath := path.str
1092 file = File{
1093 cfile: C.fopen(&char(cpath), c'ab')
1094 }
1095 }
1096 if isnil(file.cfile) {
1097 return error_posix(msg: 'failed to create(append) file "${path}"')
1098 }
1099 file.is_opened = true
1100 return file
1101}
1102
1103// execvp - loads and executes a new child process, *in place* of the current process.
1104// The child process executable is located in `cmdpath`.
1105// The arguments, that will be passed to it are in `args`.
1106// Note: this function will NOT return when successful, since
1107// the child process will take control over execution.
1108pub fn execvp(cmdpath string, cmdargs []string) ! {
1109 mut cargs := []&char{}
1110 cargs << &char(cmdpath.str)
1111 for i in 0 .. cmdargs.len {
1112 cargs << &char(cmdargs[i].str)
1113 }
1114 cargs << &char(unsafe { nil })
1115 mut res := int(0)
1116 $if windows {
1117 res = C._execvp(&char(cmdpath.str), cargs.data)
1118 } $else {
1119 res = C.execvp(&char(cmdpath.str), cargs.data)
1120 }
1121 if res == -1 {
1122 return error_posix()
1123 }
1124
1125 // just in case C._execvp returned ... that happens on windows ...
1126 exit(res)
1127}
1128
1129// execve - loads and executes a new child process, *in place* of the current process.
1130// The child process executable is located in `cmdpath`.
1131// The arguments, that will be passed to it are in `args`.
1132// You can pass environment variables to through `envs`.
1133// Note: this function will NOT return when successful, since
1134// the child process will take control over execution.
1135pub fn execve(cmdpath string, cmdargs []string, envs []string) ! {
1136 mut cargv := []&char{}
1137 mut cenvs := []&char{}
1138 cargv << &char(cmdpath.str)
1139 for i in 0 .. cmdargs.len {
1140 cargv << &char(cmdargs[i].str)
1141 }
1142 for i in 0 .. envs.len {
1143 cenvs << &char(envs[i].str)
1144 }
1145 cargv << &char(unsafe { nil })
1146 cenvs << &char(unsafe { nil })
1147 mut res := int(0)
1148 $if windows {
1149 res = C._execve(&char(cmdpath.str), cargv.data, cenvs.data)
1150 } $else {
1151 res = C.execve(&char(cmdpath.str), cargv.data, cenvs.data)
1152 }
1153 // Note: normally execve does not return at all.
1154 // If it returns, then something went wrong...
1155 if res == -1 {
1156 return error_posix()
1157 }
1158}
1159
1160// is_atty returns 1 if the `fd` file descriptor is open and refers to a terminal.
1161pub fn is_atty(fd int) int {
1162 $if windows {
1163 mut mode := u32(0)
1164 osfh := voidptr(C._get_osfhandle(fd))
1165 C.GetConsoleMode(osfh, voidptr(&mode))
1166 return int(mode)
1167 } $else {
1168 return C.isatty(fd)
1169 }
1170}
1171
1172// write_file_array writes the data in `buffer` to a file in `path`.
1173pub fn write_file_array(path string, buffer array) ! {
1174 mut f := create(path)!
1175 unsafe { f.write_full_buffer(buffer.data, usize(buffer.len * buffer.element_size))! }
1176 f.close()
1177}
1178
1179// glob function searches for all the pathnames matching patterns and returns a
1180// sorted list of found pathnames.
1181@[manualfree]
1182pub fn glob(patterns ...string) ![]string {
1183 mut matches := []string{}
1184 for pattern in patterns {
1185 native_glob_pattern(pattern, mut matches)!
1186 }
1187 matches.sort()
1188 return matches
1189}
1190
1191// last_error returns a V error, formed by the last libc error (from
1192// `GetLastError()` on windows and from `errno` on !windows).
1193@[manualfree]
1194pub fn last_error() IError {
1195 $if windows {
1196 code := int(C.GetLastError())
1197 msg := get_error_msg(code)
1198 return error_with_code(msg, code)
1199 } $else {
1200 code := C.errno
1201 msg := posix_get_error_msg(code)
1202 return error_with_code(msg, code)
1203 }
1204}
1205
1206// Magic constant because zero is used explicitly at times
1207pub const error_code_not_set = int(-1)
1208
1209@[params]
1210pub struct SystemError {
1211pub:
1212 msg string
1213 code int = error_code_not_set
1214}
1215
1216// error_posix returns a POSIX error:
1217// Code defaults to last error (from C.errno)
1218// Message defaults to POSIX error message for the error code
1219@[inline; manualfree]
1220pub fn error_posix(e SystemError) IError {
1221 code := if e.code == error_code_not_set { C.errno } else { e.code }
1222 message := if e.msg == '' { posix_get_error_msg(code) } else { e.msg }
1223 return error_with_code(message, code)
1224}
1225
1226// error_win32 returns a Win32 API error:
1227// example:
1228// ```
1229// // save error code immediately, or it will be overwritten by other API
1230// // function calls, even by `str_intp`.
1231// code := int(C.GetLastError())
1232// error_win32(
1233// msg : 'some error'
1234// code : code
1235// )
1236// ```
1237// wrong usage:
1238// ```
1239// error_win32(
1240// msg : 'some error ${path}' // this will overwrite error code
1241// code : int(C.GetLastError())
1242// )
1243// ```
1244// Message defaults to Win 32 API error message for the error code
1245@[inline; manualfree]
1246pub fn error_win32(e SystemError) IError {
1247 $if windows {
1248 if e.code == error_code_not_set {
1249 panic('before calling `error_win32`, you must set `e.code` first.')
1250 }
1251 message := if e.msg == '' { get_error_msg(e.code) } else { e.msg }
1252 return error_with_code(message, e.code)
1253 } $else {
1254 panic('Win32 API not available on this platform.')
1255 }
1256}
1257
1258pub struct DiskUsage {
1259pub:
1260 total u64
1261 available u64
1262 used u64
1263}
1264