read_linux.odin (3828B)
1 #+build linux 2 package walk 3 4 import "core:mem" 5 import "core:strings" 6 import "core:sys/linux" 7 8 /* 9 Read one directory with the calls the work actually needs. 10 11 core:os describes every entry in full: an fd opened and closed around it, a statx on 12 that fd, and the entry's path read back through /proc/self/fd. Four syscalls, where 13 one statx against the directory answers everything the tree records. Walking 14,478 14 entries cost 64,235 calls that way against du's 21,511 for the same tree, and the 15 procfs readlink was over half of it. 16 17 getdents also names each entry's type, so most are settled without asking again, and 18 statx reports allocated blocks as readily as length. That makes the on-disk figure 19 exact here rather than the length rounded up to a guessed block, so a sparse or 20 compressed file is charged what it occupies. 21 */ 22 @(private) 23 read_dir :: proc(d: Dir, w: ^Worker) -> (nodes, bytes: u64, ok: bool) { 24 path: Path 25 cpath, path_ok := terminate(d.path, &path) 26 if !path_ok { 27 return 0, 0, false 28 } 29 // Not NOFOLLOW: the queue holds directories, and symlinks only when the caller 30 // asked for those to be followed, so refusing a link here would refuse exactly 31 // the ones it was told to walk. DIRECTORY still rejects a link onto a file. 32 fd, open_errno := linux.open(cpath, {.DIRECTORY, .CLOEXEC}) 33 if open_errno != .NONE { 34 return 0, 0, false 35 } 36 defer linux.close(fd) 37 38 // Sized so a directory of any ordinary size comes back in one call. core:os 39 // starts at 512 bytes and grows only when the kernel refuses, which cost it 40 // roughly twice the getdents calls for the same tree. 41 buf: [32 * mem.Kilobyte]u8 42 for { 43 n, errno := linux.getdents(fd, buf[:]) 44 if errno != .NONE { 45 return nodes, bytes, false 46 } 47 if n == 0 { 48 return nodes, bytes, true 49 } 50 offset := 0 51 for entry in linux.dirent_iterate_buf(buf[:n], &offset) { 52 name := linux.dirent_name(entry) 53 if name == "." || name == ".." { 54 continue 55 } 56 e := Entry { 57 name = name, 58 directory = entry.type == .DIR, 59 symlink = entry.type == .LNK, 60 } 61 // The name sits in the buffer already terminated, so it addresses the 62 // entry without being copied anywhere first. 63 mask := linux.Statx_Mask{.TYPE, .SIZE, .BLOCKS} 64 st: linux.Statx 65 if linux.statx(fd, cstring(raw_data(name)), {.SYMLINK_NOFOLLOW}, mask, &st) == .NONE { 66 // Some filesystems leave the type out of a directory entry. Those are 67 // the only ones that have to be told apart here. 68 if entry.type == .UNKNOWN { 69 e.directory = linux.S_ISDIR(st.mode) 70 e.symlink = linux.S_ISLNK(st.mode) 71 } 72 e.size = st.size 73 if !e.directory && !e.symlink { 74 // Blocks are counted in 512 byte units whatever the filesystem 75 // builds them from. 76 e.disk = st.blocks * 512 77 } 78 } 79 index, descend, recorded := record(w, d.index, e) 80 if !recorded { 81 return nodes, bytes, false 82 } 83 if descend { 84 child, join_err := join(d.path, name, w.allocator) 85 if join_err != nil { 86 w.err = .Out_Of_Memory 87 return nodes, bytes, false 88 } 89 append(&w.found, Dir{index = index, path = child}) 90 } 91 nodes += 1 92 bytes += e.disk 93 } 94 } 95 } 96 97 // A path as the kernel wants it. Long enough for anything the kernel will accept, so 98 // building one costs no allocation. 99 @(private) 100 Path :: [4096]u8 101 102 @(private) 103 terminate :: proc(path: string, buf: ^Path) -> (cstring, bool) { 104 if len(path) >= len(buf) { 105 return nil, false 106 } 107 copy(buf[:], path) 108 buf[len(path)] = 0 109 return cstring(&buf[0]), true 110 } 111 112 // The child's path, which getdents does not hand back and the queue needs. 113 @(private) 114 join :: proc(dir, name: string, allocator: mem.Allocator) -> (string, mem.Allocator_Error) { 115 if strings.has_suffix(dir, "/") { 116 return strings.concatenate({dir, name}, allocator) 117 } 118 return strings.concatenate({dir, "/", name}, allocator) 119 }