Shefla

Zippy

May 31st, 2015
448
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 5.12 KB | None | 0 0
  1. --- Zippy - Archive compressor/decompressor for ComputerCraft
  2. -- @version 0.6-rc1
  3. -- @author Shefla
  4. -- @license MIT
  5. -- @changelog
  6. --   0.1 Initial version, concatenates file and folders
  7. --   0.2 Added basic RLE compression/decompression
  8. --   0.3 Public release, added comments to API section
  9. --   0.4 Removed root from file/folder paths inside archive
  10. --       Added relative path support to CLI parameters
  11. --   0.5 Added end delimiters to run-length encoded chunks
  12. --   0.6 Fixed bug when zippy is compressing itself
  13. --       Added token dictionnary for better compression ratio
  14.  
  15. local ch, header, trim, isAPI = string.char, '', {}, shell == nil
  16. local log = isAPI and function () end or print
  17. local STX, ETX, SUB, FS, GS = ch(2), ch(3), ch(26), ch(28), ch(29)
  18. -- header = [SOH]zippy1[DLE]
  19. for _, val in ipairs({ 1, 122, 105, 112, 112, 121, 49, 16 }) do
  20.   header = header..ch(val)
  21. end
  22. -- trim = { [TAB]=[DC1], [LF]=[DC2], [CR]=[DC3], space=[DC4] }
  23. for key, val in pairs({ ['9']=17, ['10']='18', ['13']='19', ['32']='20' }) do
  24.     trim[ch(key)] = ch(val)
  25. end
  26.  
  27. -- Compression
  28. --------------------------------------------------------------------------------
  29. local function concat (root, path, tokens)
  30.     local rel, file, data = path:gsub(root, '', 1)
  31.     local buf = { rel }
  32.     if fs.isDir(path) then
  33.         table.insert(buf, GS)
  34.         for _, file in ipairs(fs.list(path)) do
  35.             data, tokens = concat(root, fs.combine(path, file), tokens)
  36.             for _, chunk in ipairs(data) do table.insert(buf, chunk) end
  37.         end
  38.     else
  39.         file = fs.open(path, 'r')
  40.         data = file.readAll()
  41.         file.close()
  42.         table.insert(buf, FS..data..GS)
  43.         for token in data:gmatch('%w+') do
  44.             tokens[token] = tokens[token] and tokens[token] + 1 or 1
  45.         end
  46.     end
  47.     return buf, tokens
  48. end
  49.  
  50. local function shrink (data, tokens)
  51.     local dict, buf, size, len, tmp = {}, {}, 3
  52.     for chr, DCX in pairs(trim) do
  53.         for index, chunk in ipairs(data) do
  54.             buf[index] = chunk:gsub(chr:rep(3)..'+', function (str)
  55.                 return DCX..#str..DCX
  56.             end)
  57.         end
  58.     end
  59.     for token, count in pairs(tokens) do
  60.         len = #token
  61.         if len * count > len + 1 + (count * size) then
  62.             table.insert(dict, token)
  63.             len  = #dict
  64.             size = #tostring(len) + 2
  65.             for index, chunk in ipairs(buf) do
  66.                 buf[index] = chunk:gsub(token, function () return SUB..len..SUB end)
  67.             end
  68.         end
  69.     end
  70.     return table.concat(dict, STX)..ETX..table.concat(buf, '')
  71. end
  72.  
  73. -- Decompression
  74. --------------------------------------------------------------------------------
  75. function parse (data)
  76.     assert(data:sub(1, 8) == header, 'Invalid archive format')
  77.     local index, dict, files = 9, {}, {}
  78.     local split = data:find(ETX)
  79.     for token in data:sub(index, split - 1):gmatch('[^'..STX..']%w+') do
  80.         table.insert(dict, token)
  81.     end
  82.     for chunk in data:sub(split + 1):gmatch('(.-)'..GS) do
  83.         chunk = expand(chunk, dict)
  84.         index = chunk:find(FS)
  85.         if not index then files[chunk] = true
  86.         else files[chunk:sub(1, index - 1)] = chunk:sub(index + 1)
  87.         end
  88.     end
  89.     return files
  90. end
  91.  
  92. function expand (data, dict)
  93.     for char, DCX in pairs(trim) do
  94.         data = data:gsub(DCX..'(%d+)'..DCX, function (count)
  95.             return char:rep(count)
  96.         end)
  97.     end
  98.     for index, token in ipairs(dict) do
  99.         data = data:gsub(SUB..index..SUB, token)
  100.     end
  101.     return data
  102. end
  103.  
  104. -- Public API
  105. --------------------------------------------------------------------------------
  106.  
  107. --- Compress a file or folder into zippy archive
  108. -- @param path {string} - Absolute path to compress
  109. -- @param dest {string} - Absolute path of created archive
  110. function compress (path, dest)
  111.     assert(fs.exists(path), 'File not found: '..path)
  112.     log('Creating archive: '..dest)
  113.     local file = fs.open(dest, 'w')
  114.     assert(file, 'File is not writeable: '..dest)
  115.     file.write(header..shrink(
  116.         concat(fs.getDir(path), fs.combine(path, ''), {})
  117.     ))
  118.     file.close()
  119. end
  120.  
  121.  
  122. --- Extract a zippy archive contents
  123. -- @param path {string} - Absolute path to archive
  124. -- @param dest {string} - Absolute path of extracted contents
  125. function extract (path, dest)
  126.     local file = fs.open(path, 'r')
  127.     assert(file, 'File is not readable: '..path)
  128.     log('Extracting archive: '..path)
  129.     local data = parse(file.readAll())
  130.     file.close()
  131.     for path, contents in pairs(data) do
  132.         path = fs.combine(dest, path)
  133.         if contents == true then fs.makeDir(path)
  134.         else
  135.             file = fs.open(path, 'w')
  136.             file.write(contents)
  137.             file.close()
  138.         end
  139.     end
  140. end
  141.  
  142.  
  143. -- Command line interface
  144. --------------------------------------------------------------------------------
  145. if isAPI then return end
  146. local args, root = {...}, fs.getDir(shell.getRunningProgram())
  147. local function resolve (path)
  148.     return shell.resolve(path:sub(1, 1) ~= '/'
  149.         and fs.combine(root, path)
  150.         or  path
  151.     )
  152. end
  153. if #args < 3 then
  154.     print('Usage: zippy <action> <path> <dest>')
  155.     print('actions:')
  156.     print('  -x, extract     Extract path to dest')
  157.     print('  -c, compress    Compress path into dest')
  158. else
  159.     local action, path, dest = args[1], resolve(args[2]), resolve(args[3])
  160.     if     action == '-x' or action == 'extract'  then extract(path, dest)
  161.     elseif action == '-c' or action == 'compress' then compress(path, dest)
  162.     else error('Invalid action parameter')
  163.     end
  164. end
Advertisement
Add Comment
Please, Sign In to add comment