target_other.odin (6629B)
1 #+build !windows 2 package scan 3 4 import "core:io" 5 import "core:os" 6 import "core:strings" 7 8 /* 9 Resolve what was given into a target backends can dispatch on. 10 11 Three shapes arrive here. A block device names a volume, a regular file is taken to 12 hold an image of one, and both are identified by the signature at the front of them. 13 A directory names a place inside a volume, which the mount table accounts for. 14 15 Reading a signature is not parsing a filesystem: saying which reader applies is this 16 layer's job, and doing it here keeps that job free of every backend, exactly as 17 asking Windows for a volume's filesystem name does. 18 19 The reader comes back with the target. Choosing one needs to know whether the volume 20 opens, which is found out here, so leaving the caller to ask separately would only 21 mean opening it twice. 22 */ 23 resolve :: proc( 24 input: string, 25 allocator := context.allocator, 26 ) -> ( 27 t: Target, 28 c: Choice, 29 err: Error, 30 ) { 31 info, stat_err := os.stat(input, context.temp_allocator) 32 if stat_err != nil { 33 if stat_err == io.Error.Permission_Denied { 34 return {}, {}, .Access_Denied 35 } 36 return {}, {}, .Target_Not_Found 37 } 38 path := info.fullpath 39 40 t.allocator = allocator 41 t.input = strings.clone(input, allocator) 42 43 #partial switch info.type { 44 case .Block_Device, .Regular: 45 t.image = info.type == .Regular 46 t.volume = strings.clone(path, allocator) 47 fs, sig_err := signature(path) 48 if sig_err != .None { 49 target_destroy(&t) 50 return {}, {}, sig_err 51 } 52 t.fs = fs 53 // Reading the signature opened it, so there is nothing left to find out. 54 return t, choose(t, true), .None 55 56 case .Directory: 57 device, mount, fs, found := mounted_at(path, allocator) 58 if !found { 59 target_destroy(&t) 60 return {}, {}, .Unsupported_Platform 61 } 62 t.volume = device 63 t.mount = mount 64 t.fs = fs 65 // Whatever lies below the mount point is the subtree to report on. Kept so 66 // that the mount point and this concatenate back into the path. 67 if len(path) > len(mount) { 68 t.root = strings.clone(path[len(mount):], allocator) 69 } 70 // Reached through its mount point, so nothing so far has touched the device. 71 // Whether that opens is what decides between reading the filesystem whole and 72 // walking what is mounted from it. 73 return t, choose(t, can_open(device)), .None 74 } 75 76 target_destroy(&t) 77 return {}, {}, .Target_Not_Found 78 } 79 80 // Whether a volume opens for raw reading. Unix keeps that answer in the device's 81 // permissions and the groups held rather than in the process, so opening it is the 82 // only test that covers every reason it might be refused. 83 @(private) 84 can_open :: proc(path: string) -> bool { 85 if path == "" { 86 return false 87 } 88 f, err := os.open(path, {.Read}) 89 if err != nil { 90 return false 91 } 92 os.close(f) 93 return true 94 } 95 96 /* 97 The filesystem whose signature sits at the front of a volume. 98 99 Every one of these writes its name into the boot sector, at one of two offsets: the 100 OEM field for the NTFS family, and the type field further in for FAT. Reading 512 101 bytes is enough to tell them apart, and enough to say that none of them match. 102 */ 103 @(private) 104 signature :: proc(path: string) -> (Filesystem, Error) { 105 f, open_err := os.open(path, {.Read}) 106 if open_err != nil { 107 if open_err == io.Error.Permission_Denied { 108 return .Unknown, .Access_Denied 109 } 110 return .Unknown, .Target_Not_Found 111 } 112 defer os.close(f) 113 114 boot: [512]byte 115 n, read_err := os.read_at(f, boot[:], 0) 116 if read_err != nil || n < len(boot) { 117 // Too small to hold a boot sector, so nothing here names a filesystem. 118 return .Unknown, .None 119 } 120 121 oem := string(boot[3:11]) 122 switch { 123 case oem == "NTFS ": 124 return .Ntfs, .None 125 case oem == "EXFAT ": 126 return .Exfat, .None 127 case strings.has_prefix(oem, "ReFS"): 128 return .Refs, .None 129 } 130 if string(boot[82:90]) == "FAT32 " { 131 return .Fat32, .None 132 } 133 if strings.has_prefix(string(boot[54:62]), "FAT") { 134 return .Fat32, .None 135 } 136 return .Unknown, .None 137 } 138 139 /* 140 The mount covering `path`: the device behind it, where it is mounted, and what runs 141 there. 142 143 The longest mount point that prefixes the path wins, which is what makes a filesystem 144 mounted inside another resolve to the inner one. Names are returned owned. 145 */ 146 @(private) 147 mounted_at :: proc( 148 path: string, 149 allocator := context.allocator, 150 ) -> ( 151 device, mount: string, 152 fs: Filesystem, 153 found: bool, 154 ) { 155 when ODIN_OS != .Linux { 156 // Other unixes report their mounts through getmntinfo rather than a file. 157 return "", "", .Unknown, false 158 } else { 159 table, read_err := os.read_entire_file_from_path(PROC_MOUNTS, context.temp_allocator) 160 if read_err != nil { 161 return "", "", .Unknown, false 162 } 163 164 best_device, best_mount, best_type: string 165 text := string(table) 166 167 for line in strings.split_lines_iterator(&text) { 168 fields := strings.fields(line, context.temp_allocator) 169 if len(fields) < 3 { 170 continue 171 } 172 point := unescape(fields[1], context.temp_allocator) 173 if !covers(point, path) || len(point) < len(best_mount) { 174 continue 175 } 176 best_device, best_mount, best_type = fields[0], point, fields[2] 177 } 178 179 if best_mount == "" { 180 return "", "", .Unknown, false 181 } 182 183 device = strings.clone(best_device, allocator) 184 mount = strings.clone(best_mount, allocator) 185 fs = filesystem_from_name(best_type) 186 found = true 187 188 return device, mount, fs, found 189 } 190 } 191 192 @(private) 193 PROC_MOUNTS :: "/proc/mounts" 194 195 // Whether `path` lies at or below `mount`. A prefix is not enough: /home does not 196 // cover /home2, only /home and what is under it. 197 @(private) 198 covers :: proc(mount, path: string) -> bool { 199 if !strings.has_prefix(path, mount) { 200 return false 201 } 202 return len(path) == len(mount) || strings.has_suffix(mount, "/") || path[len(mount)] == '/' 203 } 204 205 // The mount table escapes the characters that would otherwise end a field. 206 @(private) 207 unescape :: proc(s: string, allocator := context.allocator) -> string { 208 if !strings.contains(s, `\`) { 209 return s 210 } 211 b := strings.builder_make(allocator) 212 for i := 0; i < len(s); i += 1 { 213 if s[i] == '\\' && i + 3 < len(s) { 214 v := (int(s[i + 1]) - '0') * 64 + (int(s[i + 2]) - '0') * 8 + (int(s[i + 3]) - '0') 215 if v >= 0 && v < 256 { 216 strings.write_byte(&b, byte(v)) 217 i += 3 218 continue 219 } 220 } 221 strings.write_byte(&b, s[i]) 222 } 223 return strings.to_string(b) 224 } 225 226 // Only the filesystems a reader here could specialise for are named; everything else 227 // is walkable and nothing more. 228 @(private) 229 filesystem_from_name :: proc(name: string) -> Filesystem { 230 switch name { 231 case "ntfs", "ntfs3": 232 return .Ntfs 233 case "exfat": 234 return .Exfat 235 case "vfat", "msdos": 236 return .Fat32 237 case "nfs", "nfs4", "cifs", "smb3": 238 return .Network 239 } 240 return .Other 241 }