sonar

Scan files at memory bandwidth speed.
Log | Files | Refs

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 }