Jianw
2025-05-13 3b39fe3810c3ee2ec9ec97236c1769c5c85e062c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
local lfs = require 'lfs'
local core = require 'git.core'
local deflate = core.deflate
local inflate = core.inflate
local sha = core.sha
 
module(..., package.seeall)
 
local BUF_SIZE = 4096
 
local dirsep = package.config:sub(1,1)
 
-- replaces '/' path separators on Windows with the correct ones ('\\')
function correct_separators(path)
    return path:gsub('/', dirsep)
end
 
-- joins several path components into a single path, uses system-specific directory
-- separator, cleans input, i.e. join_path('a/', 'b', 'c/') => 'a/b/c'
function join_path(...)
    local n = select('#', ...)
    local args = {...}
    for i=1,n do
        args[i] = args[i]:gsub(dirsep..'?$', '')
    end
    return table.concat(args, dirsep, 1, n)
end
 
-- Return the path with the all occurences of '/.' or '\.' (representing
-- the current directory) removed.
local function remove_curr_dir_dots(path)
    while path:match(dirsep .. "%." .. dirsep) do             -- match("/%./")
        path = path:gsub(dirsep .. "%." .. dirsep, dirsep)    -- gsub("/%./", "/")
    end
    return path:gsub(dirsep .. "%.$", "")                     -- gsub("/%.$", "")
end
 
-- Return whether the path is a root.
local function is_root(path)
    return path:find("^[%u%U.]?:?[/\\]$")
end
 
-- Return the path with the unnecessary trailing separator removed.
local function remove_trailing(path)
    if path:sub(-1) == dirsep and not is_root(path) then path = path:sub(1,-2) end
    return path
end
 
-- Extract file or directory name from its path.
local function extract_name(path)
    if is_root(path) then return path end
 
    path = remove_trailing(path)
    path = path:gsub("^.*" .. dirsep, "")
    return path
end
 
-- Return the string 'str', with all magic (pattern) characters escaped.
local function escape_magic(str)
    local escaped = str:gsub('[%-%.%+%[%]%(%)%^%%%?%*%^%$]','%%%1')
    return escaped
end
 
-- Return parent directory of the 'path' or nil if there's no parent directory.
-- If 'path' is a path to file, return the directory the file is in.
function parent_dir(path)
    path = remove_curr_dir_dots(path)
    path = remove_trailing(path)
 
    local dir = path:gsub(escape_magic(extract_name(path)) .. "$", "")
    if dir == "" then
        return nil
    else
        return remove_trailing(dir)
    end
end
 
-- Make a new directory, making also all of its parent directories that doesn't exist.
function make_dir(path)
    if lfs.attributes(path) then
        return true
    else
        local par_dir = parent_dir(path)
        if par_dir then
            assert(make_dir(par_dir))
        end
        return lfs.mkdir(path)
    end
end
 
 
-- Reader class
-- adapted from Penlight: https://raw.github.com/stevedonovan/Penlight/master/lua/pl/stringio.lua
 
local SR = {}
SR.__index = SR
 
function SR:_read(fmt)
    local i,str = self.i,self.str
    local sz = #str
    if i > sz then return nil, "past end of file" end
    local res
    if fmt == '*l' or fmt == '*L' then
        local idx = str:find('\n',i) or (sz+1)
        res = str:sub(i,fmt == '*l' and idx-1 or idx)
        self.i = idx+1
    elseif fmt == '*a' then
        res = str:sub(i)
        self.i = sz+1
    elseif fmt == '*n' then
        local _,i2,i2,idx
        _,idx = str:find ('%s*%d+',i)
        _,i2 = str:find ('^%.%d+',idx+1)
        if i2 then idx = i2 end
        _,i2 = str:find ('^[eE][%+%-]*%d+',idx+1)
        if i2 then idx = i2 end
        local val = str:sub(i,idx)
        res = tonumber(val)
        self.i = idx+1
    elseif type(fmt) == 'number' then
        res = str:sub(i,i+fmt-1)
        self.i = i + fmt
    else
        error("bad read format",2)
    end
    return res
        end
 
function SR:read(...)
    if select('#',...) == 0 then
        return self:_read('*l')
    else
        local res, fmts = {},{...}
        for i = 1, #fmts do
            res[i] = self:_read(fmts[i])
            end
        return unpack(res)
                end
            end
 
function SR:seek(whence,offset)
    local base
    whence = whence or 'cur'
    offset = offset or 0
    if whence == 'set' then
        base = 1
    elseif whence == 'cur' then
        base = self.i
    elseif whence == 'end' then
        base = #self.str
    end
    self.i = base + offset
    return self.i
end
 
function SR:close() -- for compatibility only
end
 
--- create a file-like object for reading from a given string.
-- @param s The input string.
function reader(s)
    return setmetatable({str=s,i=1},SR)
end
 
 
-- decompress the file and return a handle to temporary uncompressed file
function decompressed(path)
    local fi = assert(io.open(path, 'rb'))
    local result = {}
 
    local z = inflate()
    repeat
        local str = fi:read(BUF_SIZE)
        local data = z(str)
        if type(data) == 'string' then
            result[#result+1] = data
        else print('!!!', data) end
    until not str
    fi:close()
 
    return reader(table.concat(result))
end
 
-- reads until the byte \0, consumes it and returns the string up to the \0
function read_until_nul(f)
    local t = {}
    repeat
        local c = f:read(1)
        if c and c ~= '\0' then t[#t+1] = c end
    until not c or c == '\0'
    if #t > 0 then
        return table.concat(t)
    else
        return nil
    end
end
 
-- converts a string to lowercase hex
function to_hex(s)
    return (s:gsub('.', function(c)
        return string.format('%02x', string.byte(c))
    end))
end
 
-- converts a string from hex to binary
function from_hex(s)
    return (s:gsub('..', function(cc)
        return string.char(tonumber(cc, 16))
    end))
end
 
-- always returns readable (hex) hash
function readable_sha(s)
    if #s ~= 40 then return to_hex(s)
    else return s end
end
 
-- always returns binary hash
function binary_sha(s)
    if #s ~= 20 then return from_hex(s)
    else return s end
end
 
function object_sha(data, len, type)
    local header = type .. ' ' .. len .. '\0'
    local res = sha(header .. data)
    return res
end
 
function deflate(data)
    local c = deflate()
    return c(data, "finish")
end