aboutsummaryrefslogtreecommitdiff
path: root/src/luarocks/tools/tar.lua
blob: a6a95252f0411f4d20c86e2fffbf0f4f3da3f7aa (plain)
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

--- A pure-Lua implementation of untar (unpacking .tar archives)
local tar = {}

local fs = require("luarocks.fs")
local dir = require("luarocks.dir")
local fun = require("luarocks.fun")

local blocksize = 512

local function get_typeflag(flag)
   if flag == "0" or flag == "\0" then return "file"
   elseif flag == "1" then return "link"
   elseif flag == "2" then return "symlink" -- "reserved" in POSIX, "symlink" in GNU
   elseif flag == "3" then return "character"
   elseif flag == "4" then return "block"
   elseif flag == "5" then return "directory"
   elseif flag == "6" then return "fifo"
   elseif flag == "7" then return "contiguous" -- "reserved" in POSIX, "contiguous" in GNU
   elseif flag == "x" then return "next file"
   elseif flag == "g" then return "global extended header"
   elseif flag == "L" then return "long name"
   elseif flag == "K" then return "long link name"
   end
   return "unknown"
end

local function octal_to_number(octal)
   local exp = 0
   local number = 0
   octal = octal:gsub("%s", "")
   for i = #octal,1,-1 do
      local digit = tonumber(octal:sub(i,i)) 
      if not digit then
         break
      end
      number = number + (digit * 8^exp)
      exp = exp + 1
   end
   return number
end

local function checksum_header(block)
   local sum = 256
   for i = 1,148 do
      local b = block:byte(i) or 0
      sum = sum + b
   end
   for i = 157,500 do
      local b = block:byte(i) or 0
      sum = sum + b
   end
   return sum
end

local function nullterm(s)
   return s:match("^[^%z]*")
end

local function read_header_block(block)
   local header = {}
   header.name = nullterm(block:sub(1,100))
   header.mode = nullterm(block:sub(101,108)):gsub(" ", "")
   header.uid = octal_to_number(nullterm(block:sub(109,116)))
   header.gid = octal_to_number(nullterm(block:sub(117,124)))
   header.size = octal_to_number(nullterm(block:sub(125,136)))
   header.mtime = octal_to_number(nullterm(block:sub(137,148)))
   header.chksum = octal_to_number(nullterm(block:sub(149,156)))
   header.typeflag = get_typeflag(block:sub(157,157))
   header.linkname = nullterm(block:sub(158,257))
   header.magic = block:sub(258,263)
   header.version = block:sub(264,265)
   header.uname = nullterm(block:sub(266,297))
   header.gname = nullterm(block:sub(298,329))
   header.devmajor = octal_to_number(nullterm(block:sub(330,337)))
   header.devminor = octal_to_number(nullterm(block:sub(338,345)))
   header.prefix = block:sub(346,500)
   -- if header.magic ~= "ustar " and header.magic ~= "ustar\0" then
   --    return false, ("Invalid header magic %6x"):format(bestring_to_number(header.magic))
   -- end
   -- if header.version ~= "00" and header.version ~= " \0" then
   --    return false, "Unknown version "..header.version
   -- end
   if not checksum_header(block) == header.chksum then
      return false, "Failed header checksum"
   end
   return header
end

function tar.untar(filename, destdir)
   assert(type(filename) == "string")
   assert(type(destdir) == "string")

   local tar_handle = io.open(filename, "rb")
   if not tar_handle then return nil, "Error opening file "..filename end
   
   local long_name, long_link_name
   local ok, err
   local make_dir = fun.memoize(fs.make_dir)
   while true do
      local block
      repeat
         block = tar_handle:read(blocksize)
      until (not block) or checksum_header(block) > 256
      if not block then break end
      if #block < blocksize then
         ok, err = nil, "Invalid block size -- corrupted file?"
         break
      end
      local header
      header, err = read_header_block(block)
      if not header then
         ok = false
         break
      end

      local file_data = tar_handle:read(math.ceil(header.size / blocksize) * blocksize):sub(1,header.size)

      if header.typeflag == "long name" then
         long_name = nullterm(file_data)
      elseif header.typeflag == "long link name" then
         long_link_name = nullterm(file_data)
      else
         if long_name then
            header.name = long_name
            long_name = nil
         end
         if long_link_name then
            header.name = long_link_name
            long_link_name = nil
         end
      end
      local pathname = dir.path(destdir, header.name)
      pathname = fs.absolute_name(pathname)
      if header.typeflag == "directory" then
         ok, err = make_dir(pathname)
         if not ok then
            break
         end
      elseif header.typeflag == "file" then
         local dirname = dir.dir_name(pathname)
         if dirname ~= "" then
            ok, err = make_dir(dirname)
            if not ok then
               break
            end
         end
         local file_handle
         file_handle, err = io.open(pathname, "wb")
         if not file_handle then
            ok = nil
            break
         end
         file_handle:write(file_data)
         file_handle:close()
         fs.set_time(pathname, header.mtime)
         if header.mode:match("[75]") then
            fs.set_permissions(pathname, "exec", "all")
         else
            fs.set_permissions(pathname, "read", "all")
         end
      end
      --[[
      for k,v in pairs(header) do
         util.printout("[\""..tostring(k).."\"] = "..(type(v)=="number" and v or "\""..v:gsub("%z", "\\0").."\""))
      end
      util.printout()
      --]]
   end
   tar_handle:close()
   return ok, err
end

return tar