windir.lua (6795B)
1 --[[ 2 An addon for mpv-file-browser which uses the Windows dir command to parse native directories 3 This behaves near identically to the native parser, but IO is done asynchronously. 4 5 Available at: https://github.com/CogentRedTester/mpv-file-browser/tree/master/addons 6 ]]-- 7 8 local mp = require "mp" 9 local msg = require "mp.msg" 10 local fb = require "file-browser" 11 12 local PLATFORM = fb.get_platform() 13 14 ---@param bytes string 15 ---@return fun(): number, number 16 local function byte_iterator(bytes) 17 ---@async 18 ---@return number? 19 local function iter() 20 for i = 1, #bytes do 21 coroutine.yield(bytes:byte(i), i) 22 end 23 error('malformed utf16le string - expected byte but found end of string') 24 end 25 26 return coroutine.wrap(iter) 27 end 28 29 ---@param bits number 30 ---@param by number 31 ---@return number 32 local function lshift(bits, by) 33 return bits * 2^by 34 end 35 36 ---@param bits number 37 ---@param by number 38 ---@return integer 39 local function rshift(bits, by) 40 return math.floor(bits / 2^by) 41 end 42 43 ---@param bits number 44 ---@param i number 45 ---@return number 46 local function bits_below(bits, i) 47 return bits % 2^i 48 end 49 50 ---@param bits number 51 ---@param i number exclusive 52 ---@param j number inclusive 53 ---@return integer 54 local function bits_between(bits, i, j) 55 return rshift(bits_below(bits, j), i) 56 end 57 58 ---@param bytes string 59 ---@return number[] 60 local function utf16le_to_unicode(bytes) 61 msg.trace('converting from utf16-le to unicode codepoints') 62 63 ---@type number[] 64 local codepoints = {} 65 66 local get_byte = byte_iterator(bytes) 67 68 while true do 69 -- start of a char 70 local success, little, i = pcall(get_byte) 71 if not success then break end 72 73 local big = get_byte() 74 local codepoint = little + lshift(big, 8) 75 76 if codepoint < 0xd800 or codepoint > 0xdfff then 77 table.insert(codepoints, codepoint) 78 else 79 -- handling surrogate pairs 80 -- grab the next two bytes to grab the low surrogate 81 local high_pair = codepoint 82 local low_pair = get_byte() + lshift(get_byte(), 8) 83 84 if high_pair >= 0xdc00 then 85 error(('malformed utf16le string at byte #%d (0x%04X) - high surrogate pair should be < 0xDC00'):format(i, high_pair)) 86 elseif low_pair < 0xdc00 then 87 error(('malformed utf16le string at byte #%d (0x%04X) - low surrogate pair should be >= 0xDC00'):format(i+2, low_pair)) 88 end 89 90 -- The last 10 bits of each surrogate are the two halves of the codepoint 91 -- https://en.wikipedia.org/wiki/UTF-16#Code_points_from_U+010000_to_U+10FFFF 92 local high_bits = bits_below(high_pair, 10) 93 local low_bits = bits_below(low_pair, 10) 94 local surrogate_par = (low_bits + lshift(high_bits, 10)) + 0x10000 95 96 table.insert(codepoints, surrogate_par) 97 end 98 end 99 100 return codepoints 101 end 102 103 ---@param codepoints number[] 104 ---@return string 105 local function unicode_to_utf8(codepoints) 106 ---@type number[] 107 local bytes = {} 108 109 -- https://en.wikipedia.org/wiki/UTF-8#Description 110 for i, codepoint in ipairs(codepoints) do 111 if codepoint >= 0xd800 and codepoint <= 0xdfff then 112 error(('codepoint %d (U+%05X) is within the reserved surrogate pair range (U+D800-U+DFFF)'):format(i, codepoint)) 113 elseif codepoint <= 0x7f then 114 table.insert(bytes, codepoint) 115 elseif codepoint <= 0x7ff then 116 table.insert(bytes, 0xC0 + rshift(codepoint, 6)) 117 table.insert(bytes, 0x80 + bits_below(codepoint, 6)) 118 elseif codepoint <= 0xffff then 119 table.insert(bytes, 0xE0 + rshift(codepoint, 12)) 120 table.insert(bytes, 0x80 + bits_between(codepoint, 6, 12)) 121 table.insert(bytes, 0x80 + bits_below(codepoint, 6)) 122 elseif codepoint <= 0x10ffff then 123 table.insert(bytes, 0xF0 + rshift(codepoint, 18)) 124 table.insert(bytes, 0x80 + bits_between(codepoint, 12, 18)) 125 table.insert(bytes, 0x80 + bits_between(codepoint, 6, 12)) 126 table.insert(bytes, 0x80 + bits_below(codepoint, 6)) 127 else 128 error(('codepoint %d (U+%05X) is larger than U+10FFFF'):format(i, codepoint)) 129 end 130 end 131 132 return string.char(table.unpack(bytes)) 133 end 134 135 local function utf8(text) 136 return unicode_to_utf8(utf16le_to_unicode(text)) 137 end 138 139 ---@type ParserConfig 140 local dir = { 141 priority = 109, 142 api_version = "1.9.0", 143 name = "cmd-dir", 144 keybind_name = "file" 145 } 146 147 ---@async 148 ---@param args string[] 149 ---@param parse_state ParseState 150 ---@return string|nil 151 local function command(args, parse_state) 152 local async = mp.command_native_async({ 153 name = "subprocess", 154 playback_only = false, 155 capture_stdout = true, 156 capture_stderr = true, 157 args = args, 158 }, fb.coroutine.callback(30) ) 159 160 ---@type boolean, boolean, MPVSubprocessResult 161 local completed, _, cmd = parse_state:yield() 162 if not completed then 163 msg.warn('read timed out for:', table.unpack(args)) 164 mp.abort_async_command(async) 165 return nil 166 end 167 168 local success = xpcall(function() 169 cmd.stdout = utf8(cmd.stdout) or '' 170 cmd.stderr = utf8(cmd.stderr) or '' 171 end, fb.traceback) 172 173 if not success then return msg.error('failed to convert utf16-le string to utf8') end 174 175 --dir returns this exact error message if the directory is empty 176 if cmd.status == 1 and cmd.stderr == "File Not Found\r\n" then cmd.status = 0 end 177 if cmd.status ~= 0 then return msg.error(cmd.stderr) end 178 179 return cmd.status == 0 and cmd.stdout or nil 180 end 181 182 function dir:can_parse(directory) 183 if not fb.get_opt('windir_parser') then return false end 184 return PLATFORM == 'windows' and directory ~= '' and not fb.get_protocol(directory) 185 end 186 187 ---@async 188 function dir:parse(directory, parse_state) 189 local list = {} 190 191 -- the dir command expects backslashes for our paths 192 directory = string.gsub(directory, "/", "\\") 193 194 local dirs = command({ "cmd", "/U", "/c", "dir", "/b", "/ad", directory }, parse_state) 195 if not dirs then return end 196 197 local files = command({ "cmd", "/U", "/c", "dir", "/b", "/a-d", directory }, parse_state) 198 if not files then return end 199 200 for name in dirs:gmatch("[^\n\r]+") do 201 name = name.."/" 202 if fb.valid_dir(name) then 203 table.insert(list, { name = name, type = "dir" }) 204 msg.trace(name) 205 end 206 end 207 208 for name in files:gmatch("[^\n\r]+") do 209 if fb.valid_file(name) then 210 table.insert(list, { name = name, type = "file" }) 211 msg.trace(name) 212 end 213 end 214 215 return list, { filtered = true } 216 end 217 218 return dir