target_windows.odin (4929B)
1 #+build windows 2 package scan 3 4 import "core:strings" 5 import win "core:sys/windows" 6 7 foreign import kernel32 "system:Kernel32.lib" 8 9 @(default_calling_convention = "system") 10 foreign kernel32 { 11 // Not bound by core:sys/windows, so declared here. 12 GetVolumePathNameW :: proc(lpszFileName: win.LPCWSTR, lpszVolumePathName: win.LPWSTR, cchBufferLength: win.DWORD) -> win.BOOL --- 13 GetVolumeInformationW :: proc(lpRootPathName: win.LPCWSTR, lpVolumeNameBuffer: win.LPWSTR, nVolumeNameSize: win.DWORD, lpVolumeSerialNumber: ^win.DWORD, lpMaximumComponentLength: ^win.DWORD, lpFileSystemFlags: ^win.DWORD, lpFileSystemNameBuffer: win.LPWSTR, nFileSystemNameSize: win.DWORD) -> win.BOOL --- 14 } 15 16 /* 17 Resolve what was given into a target backends can dispatch on. 18 19 A bare drive letter is accepted for convenience, an existing path is asked about, and 20 anything else is taken to be an image file holding a volume. Failing here is better 21 than failing inside a backend, because this is the layer that knows why. 22 23 The reader comes back with the target. Choosing one needs to know whether the volume 24 opens, which is found out here, so leaving the caller to ask separately would only 25 mean opening it twice. 26 */ 27 resolve :: proc( 28 input: string, 29 allocator := context.allocator, 30 ) -> ( 31 t: Target, 32 c: Choice, 33 err: Error, 34 ) { 35 t.allocator = allocator 36 t.input = strings.clone(input, allocator) 37 38 path := input 39 if is_drive_spec(input) { 40 path = strings.concatenate({input[:1], `:\`}, context.temp_allocator) 41 } 42 wpath := win.utf8_to_wstring(path, context.temp_allocator) 43 44 attrs := win.GetFileAttributesW(wpath) 45 if attrs == win.INVALID_FILE_ATTRIBUTES { 46 target_destroy(&t) 47 return {}, {}, .Target_Not_Found 48 } 49 50 // A file that is not a directory is taken to be an image of a volume. Nothing 51 // here can say which filesystem is inside it, so a backend has to look. 52 if attrs & win.FILE_ATTRIBUTE_DIRECTORY == 0 { 53 t.image = true 54 t.volume = strings.clone(path, allocator) 55 return t, choose(t, can_open(t.volume)), .None 56 } 57 58 // The mount point tells us which volume the path sits on, which is the only way 59 // to be right about a directory mounted from another volume. 60 mount_buf: [win.MAX_PATH]u16 61 if !GetVolumePathNameW(wpath, &mount_buf[0], len(mount_buf)) { 62 target_destroy(&t) 63 return {}, {}, .Target_Not_Found 64 } 65 mount, mount_err := win.wstring_to_utf8(win.wstring(&mount_buf[0]), -1, allocator) 66 if mount_err != nil { 67 target_destroy(&t) 68 return {}, {}, .Out_Of_Memory 69 } 70 t.mount = mount 71 72 fs_buf: [win.MAX_PATH]u16 73 if GetVolumeInformationW( 74 win.wstring(&mount_buf[0]), 75 nil, 76 0, 77 nil, 78 nil, 79 nil, 80 &fs_buf[0], 81 len(fs_buf), 82 ) { 83 name, name_err := win.wstring_to_utf8(win.wstring(&fs_buf[0]), -1, context.temp_allocator) 84 if name_err == nil { 85 t.fs = filesystem_from_name(name) 86 } 87 } 88 89 // A mount point of the form `C:\` opens as `\\.\C:`; anything else is a volume 90 // mounted into a directory, which needs its own device path and is left alone. 91 if len(t.mount) >= 2 && t.mount[1] == ':' { 92 t.volume = strings.concatenate({`\\.\`, t.mount[:1], ":"}, allocator) 93 } 94 if strings.has_prefix(t.mount, `\\`) { 95 t.fs = .Network 96 } 97 98 // Whatever of the path lies below the mount point is the subtree to report on. 99 if len(path) > len(t.mount) { 100 t.root = strings.clone(path[len(t.mount) - 1:], allocator) 101 } 102 // The mount point answered everything above; nothing has touched the volume 103 // itself. Whether that opens is what decides the reader. 104 return t, choose(t, can_open(t.volume)), .None 105 } 106 107 @(private) 108 filesystem_from_name :: proc(name: string) -> Filesystem { 109 switch name { 110 case "NTFS": 111 return .Ntfs 112 case "ReFS": 113 return .Refs 114 case "exFAT": 115 return .Exfat 116 case "FAT32", "FAT": 117 return .Fat32 118 } 119 return .Unknown 120 } 121 122 // `C`, `C:` and `C:\` all mean the same volume. 123 @(private) 124 is_drive_spec :: proc(s: string) -> bool { 125 if len(s) == 0 || len(s) > 3 { 126 return false 127 } 128 c := s[0] 129 if !((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')) { 130 return false 131 } 132 if len(s) >= 2 && s[1] != ':' { 133 return false 134 } 135 if len(s) == 3 && s[2] != '\\' && s[2] != '/' { 136 return false 137 } 138 return true 139 } 140 141 /* 142 Whether a volume opens for raw reading. 143 144 Elevation is the usual reason a raw handle is refused, but an elevated token still 145 says nothing about a volume that is missing, or locked by something else. Opening it 146 answers all of those at once and costs one handle. 147 148 FILE_SHARE_WRITE is required: the volume is mounted and in use, so asking to exclude 149 writers would fail for a reason that has nothing to do with permission. 150 */ 151 @(private) 152 can_open :: proc(path: string) -> bool { 153 if path == "" { 154 return false 155 } 156 h := win.CreateFileW( 157 win.utf8_to_wstring(path, context.temp_allocator), 158 win.GENERIC_READ, 159 win.FILE_SHARE_READ | win.FILE_SHARE_WRITE | win.FILE_SHARE_DELETE, 160 nil, 161 win.OPEN_EXISTING, 162 0, 163 nil, 164 ) 165 if h == win.INVALID_HANDLE_VALUE { 166 return false 167 } 168 win.CloseHandle(h) 169 return true 170 }