ZNZNCOOP

[OpenComputers]tar

Nov 26th, 2016
115
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. -----------------------------------------------------
  2. --name       : bin/tar.lua
  3. --description: creating, viewing and extracting tar archives on disk and tape
  4. --author     : mpmxyz
  5. --github page: https://github.com/mpmxyz/ocprograms
  6. --forum page : http://oc.cil.li/index.php?/topic/421-tar-for-opencomputers/
  7. -----------------------------------------------------
  8. --[[
  9.   tar archiver for OpenComputers
  10.   for further information check the usage text or man page
  11.  
  12.   TODO: support non primary tape drives
  13.   TODO: detect symbolic link cycles (-> remember already visited, resolved paths)
  14. ]]
  15. local shell     = require 'shell'
  16. local fs        = require 'filesystem'
  17. local component = require 'component'
  18.  
  19. local BLOCK_SIZE = 512
  20. local NULL_BLOCK = ("\0"):rep(BLOCK_SIZE)
  21. local WORKING_DIRECTORY = fs.canonical(shell.getWorkingDirectory()):gsub("/$","")
  22.  
  23. --load auto_progress library if possible
  24. local auto_progress
  25. if true then
  26.   local ok
  27.   ok, auto_progress = pcall(require, "mpm.auto_progress")
  28.   if not ok then
  29.     auto_progress = {}
  30.     function auto_progress.new()
  31.       --library not available, create stub
  32.       return {
  33.         update = function() end,
  34.         finish = function() end,
  35.       }
  36.     end
  37.   end
  38. end
  39.  
  40. --error information
  41. local USAGE_TEXT = [[
  42. Usage:
  43. tar <function letter> [other options] FILES...
  44. function letter    description
  45. -c --create         creates a new archive
  46. -r --append         appends to existing archive
  47. -t --list           lists contents from archive
  48. -x --extract --get  extracts from archive
  49. other options      description
  50. -f --file FILE      first FILE is archive, else:
  51.                     uses primary tape drive
  52. -h --dereference    follows symlinks
  53. --exclude=FILE;...  excludes FILE from archive
  54. -v --verbose        lists processed files, also
  55.                     shows progress for large files
  56. ]]
  57. local function addUsage(text)
  58.   return text .. "\n" .. USAGE_TEXT
  59. end
  60. local ERRORS = {
  61.   missingAction   = addUsage("Error: Missing function letter!"),
  62.   multipleActions = addUsage("Error: Multiple function letters!"),
  63.   missingFiles    = addUsage("Error: Missing file names!"),
  64.   invalidChecksum = "Error: Invalid checksum!",
  65.   noHeaderName    = "Error: No file name in header!",
  66.   invalidTarget   = "Error: Invalid target!",
  67. }
  68.  
  69.  
  70. --formats numbers and stringvalues to comply to the tar format
  71. local function formatValue(text, length, maxDigits)
  72.   if type(text) == "number" then
  73.     maxDigits = maxDigits or (length - 1) --that is default
  74.     text = ("%0"..maxDigits.."o"):format(text):sub(-maxDigits, -1)
  75.   elseif text == nil then
  76.     text = ""
  77.   end
  78.   return (text .. ("\0"):rep(length - #text)):sub(-length, -1)
  79. end
  80.  
  81. --a utility to make accessing the header easier
  82. --Only one header is accessed at a time: no need to throw tables around.
  83. local header = {}
  84. --loads a header, uses table.concat on tables, strings are taken directly
  85. function header:init(block)
  86.   if type(block) == "table" then
  87.     --combine tokens to form one 512 byte long header
  88.     block = table.concat(block, "")
  89.   elseif block == nil then
  90.     --make this header a null header
  91.     block = NULL_BLOCK
  92.   end
  93.   if #block < BLOCK_SIZE then
  94.     --add "\0"s to reach 512 bytes
  95.     block = block .. ("\0"):rep(BLOCK_SIZE - #block)
  96.   end
  97.   --remember the current block
  98.   self.block = block
  99. end
  100. --takes the given data and creates a header from it
  101. --the resulting block can be retrieved via header:getBytes()
  102. function header:assemble(data)
  103.   if #data.name > 100 then
  104.     local longName = data.name
  105.     --split at slash
  106.     local minPrefixLength = #longName - 101 --(100x suffix + 1x slash)
  107.     local splittingSlashIndex = longName:find("/", minPrefixLength + 1, true)
  108.     if splittingSlashIndex then
  109.       --can split path in 2 parts separated by a slash
  110.       data.filePrefix = longName:sub(1, splittingSlashIndex - 1)
  111.       data.name       = longName:sub(splittingSlashIndex + 1, -1)
  112.     else
  113.       --unable to split path; try to put path to the file prefix
  114.       data.filePrefix = longName
  115.       data.name       = ""
  116.     end
  117.     --checking for maximum file prefix length
  118.     assert(#data.filePrefix <= 155, "File name '"..longName.."' is too long; unable to apply ustar splitting!")
  119.     --force ustar format
  120.     data.ustarIndicator = "ustar"
  121.     data.ustarVersion = "00"
  122.   end
  123.   local tokens = {
  124.     formatValue(data.name,          100), --1
  125.     formatValue(data.mode,            8), --2
  126.     formatValue(data.owner,           8), --3
  127.     formatValue(data.group,           8), --4
  128.     formatValue(data.size,           12), --5
  129.     formatValue(data.lastModified,   12), --6
  130.     "        ",--8 spaces                 --7
  131.     formatValue(data.typeFlag,        1), --8
  132.     formatValue(data.linkName,      100), --9
  133.   }
  134.   --ustar extension?
  135.   if data.ustarIndicator then
  136.     table.insert(tokens, formatValue(data.ustarindicator,      6))
  137.     table.insert(tokens, formatValue(data.ustarversion,        2))
  138.     table.insert(tokens, formatValue(data.ownerUser,          32))
  139.     table.insert(tokens, formatValue(data.ownerGroup,         32))
  140.     table.insert(tokens, formatValue(data.deviceMajor,         8))
  141.     table.insert(tokens, formatValue(data.deviceMinor,         8))
  142.     table.insert(tokens, formatValue(data.filePrefix,        155))
  143.   end
  144.   --temporarily assemble header for checksum calculation
  145.   header:init(tokens)
  146.   --calculating checksum
  147.   tokens[7] = ("%06o\0\0"):format(header:checksum(0, BLOCK_SIZE))
  148.   --assemble final header  
  149.   header:init(tokens)
  150. end
  151. --extracts the information from the given header
  152. function header:read()
  153.   local data = {}
  154.   data.name           = self:extract      (0  , 100)
  155.   data.mode           = self:extract      (100, 8)
  156.   data.owner          = self:extractNumber(108, 8)
  157.   data.group          = self:extractNumber(116, 8)
  158.   data.size           = self:extractNumber(124, 12)
  159.   data.lastModified   = self:extractNumber(136, 12)
  160.   data.checksum       = self:extractNumber(148, 8)
  161.   data.typeFlag       = self:extract      (156, 1) or "0"
  162.   data.linkName       = self:extract      (157, 100)
  163.   data.ustarIndicator = self:extract      (257, 6)
  164.  
  165.   --There is an old format using "ustar  \0" instead of "ustar\0".."00"?
  166.   if data.ustarIndicator and data.ustarIndicator:sub(1,5) == "ustar" then
  167.     data.ustarVersion = self:extractNumber(263, 2)
  168.     data.ownerUser    = self:extract      (265, 32)
  169.     data.ownerGroup   = self:extract      (297, 32)
  170.     data.deviceMajor  = self:extractNumber(329, 8)
  171.     data.deviceMinor  = self:extractNumber(337, 8)
  172.     data.filePrefix   = self:extract      (345, 155)
  173.   end
  174.  
  175.   assert(self:verify(data.checksum), ERRORS.invalidChecksum)
  176.   --assemble raw file name, normally relative to working dir
  177.   if data.filePrefix then
  178.     data.name = data.filePrefix .. "/" .. data.name
  179.     data.filePrefix = nil
  180.   end
  181.   assert(data.name, ERRORS.noHeaderName)
  182.   return data
  183. end
  184. --returns the whole 512 bytes of the header
  185. function header:getBytes()
  186.   return header.block
  187. end
  188. --returns if the header is a null header
  189. function header:isNull()
  190.   return self.block == NULL_BLOCK
  191. end
  192. --extracts a 0 terminated string from the given area
  193. function header:extract(offset, size)
  194.   --extract size bytes from the given offset, strips every NULL character
  195.   --returns a string
  196.   return self.block:sub(1 + offset, size + offset):match("[^\0]+")
  197. end
  198. --extracts an octal number from the given area
  199. function header:extractNumber(offset, size)
  200.   --extract size bytes from the given offset
  201.   --returns the first series of octal digits converted to a number
  202.   return tonumber(self.block:sub(1 + offset, size + offset):match("[0-7]+") or "", 8)
  203. end
  204. --calculates the checksum for the given area
  205. function header:checksum(offset, size, signed)
  206.   --calculates the checksum of a given range
  207.   local sum = 0
  208.   --summarize byte for byte
  209.   for index = 1 + offset, size + offset do
  210.     if signed then
  211.       --interpretation of a signed byte: compatibility for bugged implementations
  212.       sum = sum + (self.block:byte(index) + 128) % 256 - 128
  213.     else
  214.       sum = sum + self.block:byte(index)
  215.     end
  216.   end
  217.   --modulo to take care of negative sums
  218.   --The whole reason for the signed addition is that some implementations
  219.   --used signed bytes instead of unsigned ones and therefore computed 'wrong' checksums.
  220.   return sum % 0x40000
  221. end
  222. --checks if the given checksum is valid for the loaded header
  223. function header:verify(checksum)
  224.   local checkedSums = {
  225.     [self:checksum(0, 148, false) + 256 + self:checksum(156, 356, false)] = true,
  226.     [self:checksum(0, 148, true ) + 256 + self:checksum(156, 356, true )] = true,
  227.   }
  228.   return checkedSums[checksum] or false
  229. end
  230.  
  231.  
  232. local function makeRelative(path, reference)
  233.   --The path and the reference directory must have a common reference. (e.g. root)
  234.   --The default reference is the current working directory.
  235.   reference = reference or WORKING_DIRECTORY
  236.   --1st: split paths into segments
  237.   local returnDirectory = path:sub(-1,-1) == "/" --?
  238.   path = fs.segments(path)
  239.   reference = fs.segments(reference)
  240.   --2nd: remove common directories
  241.   while path[1] and reference[1] and path[1] == reference[1] do
  242.     table.remove(path, 1)
  243.     table.remove(reference, 1)
  244.   end
  245.   --3rd: add ".."s to leave that what's left of the working directory
  246.   local path = ("../"):rep(#reference) .. table.concat(path, "/")
  247.   --4th: If there is nothing remaining, we are at the current directory.
  248.   if path == "" then
  249.     path = "."
  250.   end
  251.   return path
  252. end
  253.  
  254.  
  255. local function tarFiles(files, options, mode, ignoredObjects, isDirectoryContent)
  256.   --combines files[2], files[3], ... into files[1]
  257.   --prepare output stream
  258.   local target, closeAtExit
  259.   if type(files[1]) == "string" then
  260.     --mode = append -> overwrite trailing NULL headers
  261.     local targetFile = shell.resolve(files[1]):gsub("/$","")
  262.     ignoredObjects[targetFile] = ignoredObjects[targetFile] or true
  263.     target = assert(io.open(targetFile, mode))
  264.     closeAtExit = true
  265.   else
  266.     target = files[1]
  267.     closeAtExit = false
  268.     assert(target.write, ERRORS.invalidTarget)
  269.   end
  270.   if mode == "rb+" then --append: not working with files because io.read does not support mode "rb+"
  271.     --start from beginning of file
  272.     assert(target:seek("set", 0))
  273.     --loop over every block
  274.     --This loop implies that it is okay if there is nothing (!) after the last file block.
  275.     --It also ensures that trailing null blocks are overwritten.
  276.     for block in target:lines(BLOCK_SIZE) do
  277.       if #block < BLOCK_SIZE then
  278.         --reached end of file before block was finished
  279.         error("Missing "..(BLOCK_SIZE-#block).." bytes to finish block.")
  280.       end
  281.       --load header
  282.       header:init(block)
  283.       if header:isNull() then
  284.         --go back to the beginning of the block
  285.         assert(target:seek("cur", -BLOCK_SIZE))
  286.         --found null header -> finished with skipping
  287.         break
  288.       end
  289.       --extract size information from header
  290.       local data = header.read(header)
  291.       if data.size and data.size > 0 then
  292.         --skip file content
  293.         local skippedBytes = math.ceil(data.size / BLOCK_SIZE) * BLOCK_SIZE
  294.         assert(target:seek("cur", skippedBytes))
  295.       end
  296.     end
  297.     if options.verbose then
  298.       print("End of archive detected; appending...")
  299.     end
  300.   end
  301.   for i = 2, #files do
  302.     --prepare data
  303.     --remove trailing slashes that might come from fs.list
  304.     local file = shell.resolve(files[i]):gsub("/$","")
  305.     --determine object type, that determines how the object is handled
  306.     local isALink, linkTarget = fs.isLink(file)
  307.     local objectType
  308.     if isALink and not options.dereference then
  309.       objectType = "link" --It's a symbolic link.
  310.     else
  311.       if fs.isDirectory(file) then
  312.         objectType = "dir" --It's a directory.
  313.       else
  314.         objectType = "file" --It's a normal file.
  315.       end
  316.     end
  317.     --add directory contents before the directory
  318.     --(It makes sense if you consider that you could change the directories file permissions to be read only.)
  319.     if objectType == "dir" and ignoredObjects[file] ~= "strict" then
  320.       local list = {target}
  321.       local i = 2
  322.       for containedFile in fs.list(file) do
  323.         list[i] = fs.concat(file, containedFile)
  324.         i = i + 1
  325.       end
  326.       tarFiles(list, options, nil, ignoredObjects, true)
  327.     end
  328.     --Ignored objects are not added to the tar.
  329.     if not ignoredObjects[file] then
  330.       local data = {}
  331.       --get relative path to current directory
  332.       data.name = makeRelative(file)
  333.       --add object specific data
  334.       if objectType == "link" then
  335.         --It's a symbolic link.
  336.         data.typeFlag = "2"
  337.         data.linkName = makeRelative(linkTarget, fs.path(file)):gsub("/$","") --force relative links
  338.       else
  339.         data.lastModified = math.floor(fs.lastModified(file) / 1000) --Java returns milliseconds...
  340.         if objectType == "dir" then
  341.           --It's a directory.
  342.           data.typeFlag = "5"
  343.           data.mode = 448 --> 700 in octal -> rwx------
  344.         elseif objectType == "file" then
  345.           --It's a normal file.
  346.           data.typeFlag = "0"
  347.           data.size = fs.size(file)
  348.           data.mode = 384 --> 600 in octal -> rw-------
  349.         end
  350.       end
  351.    
  352.       --tell user what is going on
  353.       if options.verbose then
  354.         print("Adding:", data.name)
  355.       end
  356.       --assemble header
  357.       header:assemble(data)
  358.       --write header
  359.       assert(target:write(header:getBytes()))
  360.       --copy file contents
  361.       if objectType == "file" then
  362.         --open source file
  363.         local source = assert(io.open(file, "rb"))
  364.         --keep track of what has to be copied
  365.         local bytesToCopy = data.size
  366.         --init progress bar
  367.         local progressBar = auto_progress.new(bytesToCopy)
  368.         --copy file contents
  369.         for block in source:lines(BLOCK_SIZE) do
  370.           assert(target:write(block))
  371.           bytesToCopy = bytesToCopy - #block
  372.           assert(bytesToCopy >= 0, "Error: File grew while copying! Is it the output file?")
  373.           if options.verbose then
  374.             --update progress bar
  375.             progressBar.update(#block)
  376.           end
  377.           if #block < BLOCK_SIZE then
  378.             assert(target:write(("\0"):rep(BLOCK_SIZE - #block)))
  379.             break
  380.           end
  381.         end
  382.         --close source file
  383.         source:close()
  384.         if options.verbose then
  385.           --draw full progress bar
  386.           progressBar.finish()
  387.         end
  388.         assert(bytesToCopy <= 0, "Error: Could not copy file!")
  389.       end
  390.     end
  391.   end
  392.   if not isDirectoryContent then
  393.     assert(target:write(NULL_BLOCK)) --Why wasting 0.5 KiB if you can waste a full KiB? xD
  394.     assert(target:write(NULL_BLOCK)) --(But that's the standard!)
  395.   end
  396.   if closeAtExit then
  397.     target:close()
  398.   end
  399. end
  400.  
  401.  
  402.  
  403. local extractingExtractors = {
  404.   ["0"] = function(data, options) --file
  405.     --creates a file at data.file and fills it with data.size bytes
  406.     --ensure that the directory is existing
  407.     local dir = fs.path(data.file)
  408.     if not fs.exists(dir) then
  409.       fs.makeDirectory(dir)
  410.     end
  411.     --don't overwrite the file if true
  412.     local skip = false
  413.     --check for existing file
  414.     if fs.exists(data.file) then
  415.       if options.verbose then
  416.         print("File already exists!")
  417.       end
  418.       --check for options specifying what to do now...
  419.       if options["keep-old-files"] then
  420.         error("Error: Attempting to overwrite: '"..data.file.."'!")
  421.       elseif options["skip-old-files"] then
  422.         --don't overwrite
  423.         skip = true
  424.       elseif options["keep-newer-files"] and data.lastModified then
  425.         --don't overwrite when file on storage is newer
  426.         local lastModifiedOnDrive = math.floor(fs.lastModified(data.file) / 1000)
  427.         if lastModifiedOnDrive > data.lastModified then
  428.           skip = true
  429.         end
  430.       else
  431.         --default: overwrite
  432.       end
  433.       if options.verbose and not skip then
  434.         --verbose: tell user that we are overwriting
  435.         print("Overwriting...")
  436.       end
  437.     end
  438.     if skip then
  439.       --go to next header
  440.       return data.size
  441.     end
  442.    
  443.     --open target file
  444.     local target = assert(io.open(data.file, "wb"))
  445.     --set file length
  446.     local bytesToCopy = data.size
  447.     --init progress bar
  448.     local progressBar = auto_progress.new(bytesToCopy)
  449.     --create extractor function, writes min(bytesToCopy, #block) bytes to target
  450.     local function extractor(block)
  451.       --shortcut for abortion
  452.       if block == nil then
  453.         target:close()
  454.         return nil
  455.       end
  456.       --adjust block size to missing number of bytes
  457.       if #block > bytesToCopy then
  458.         block = block:sub(1, bytesToCopy)
  459.       end
  460.       --write up to BLOCK_SIZE bytes
  461.       assert(target:write(block))
  462.       --subtract copied amount of bytes from bytesToCopy
  463.       bytesToCopy = bytesToCopy - #block
  464.       if bytesToCopy <= 0 then
  465.         --close target stream when done
  466.         target:close()
  467.         if options.verbose then
  468.           --draw full progress bar
  469.           progressBar.finish()
  470.         end
  471.         --return nil to finish
  472.         return nil
  473.       else
  474.         if options.verbose then
  475.           --update progress bar
  476.           progressBar.update(#block)
  477.         end
  478.         --continue
  479.         return extractor
  480.       end
  481.     end
  482.     if bytesToCopy > 0 then
  483.       return extractor
  484.     else
  485.       target:close()
  486.     end
  487.   end,
  488.   ["2"] = function(data, options) --symlink
  489.     --ensure that the directory is existing
  490.     local dir = fs.path(data.file)
  491.     if not fs.exists(dir) then
  492.       fs.makeDirectory(dir)
  493.     end
  494.     --check for existing file
  495.     if fs.exists(data.file) then
  496.       if options.verbose then
  497.         print("File already exists!")
  498.       end
  499.       if options["keep-old-files"] then
  500.         error("Error: Attempting to overwrite: '"..data.file.."'!")
  501.       elseif options["skip-old-files"] then
  502.         return
  503.       elseif options["keep-newer-files"] and data.lastModified then
  504.         --don't overwrite when file on storage is newer
  505.         local lastModifiedOnDrive = math.floor(fs.lastModified(data.file) / 1000)
  506.         if lastModifiedOnDrive > data.lastModified then
  507.           return
  508.         end
  509.       else
  510.         --default: overwrite file
  511.       end
  512.       --delete original file
  513.       if options.verbose then
  514.         print("Overwriting...")
  515.       end
  516.       assert(fs.remove(data.file))
  517.     end
  518.     assert(fs.link(data.linkName, data.file))
  519.   end,
  520.   ["5"] = function(data, options) --directory
  521.     if not fs.isDirectory(data.file) then
  522.       assert(fs.makeDirectory(data.file))
  523.     end
  524.   end,
  525. }
  526. local listingExtractors = {
  527.   ["0"] = function(data, options) --file
  528.     --output info
  529.     print("File:", data.name)
  530.     print("Size:", data.size)
  531.     --go to next header
  532.     return data.size
  533.   end,
  534.   ["1"] = function(data, options) --hard link: unsupported, but reported
  535.     print("Hard link (unsupported):", data.name)
  536.     print("Target:", data.linkName)
  537.   end,
  538.   ["2"] = function(data, options) --symlink
  539.     print("Symbolic link:", data.name)
  540.     print("Target:", data.linkName)
  541.   end,
  542.   ["3"] = function(data, options) --device file: unsupported, but reported
  543.     print("Device File (unsupported):", data.name)
  544.   end,
  545.   ["4"] = function(data, options) --device file: unsupported, but reported
  546.     print("Device File (unsupported):", data.name)
  547.   end,
  548.   ["5"] = function(data, options) --directory
  549.     print("Directory:", data.name)
  550.   end,
  551. }
  552.  
  553.  
  554. local function untarFiles(files, options, extractorList)
  555.   --extracts the contents of every tar file given
  556.   for _,file in ipairs(files) do
  557.     --prepare input stream
  558.     local source, closeAtExit
  559.     if type(file) == "string" then
  560.       source = assert(io.open(shell.resolve(file), "rb"))
  561.       closeAtExit = true
  562.     else
  563.       source = file
  564.       closeAtExit = false
  565.       assert(source.lines, "Unknown source type.")
  566.     end
  567.     local extractor = nil
  568.     local hasDoubleNull = false
  569.     for block in source:lines(BLOCK_SIZE) do
  570.       if #block < BLOCK_SIZE then
  571.         error("Error: Unfinished Block; missing "..(BLOCK_SIZE-#block).." bytes!")
  572.       end
  573.       if extractor == nil then
  574.         --load header
  575.         header:init(block)
  576.         if header:isNull() then
  577.           --check for second null block
  578.           if source:read(BLOCK_SIZE) == NULL_BLOCK then
  579.             hasDoubleNull = true
  580.           end
  581.           --exit/close file when there is a NULL header
  582.           break
  583.         else
  584.           --read block as header
  585.           local data = header:read()
  586.           if options.verbose then
  587.             --tell user what is going on
  588.             print("Extracting:", data.name)
  589.           end
  590.           --enforcing relative paths
  591.           data.file = shell.resolve(WORKING_DIRECTORY.."/"..data.name)
  592.           --get extractor
  593.           local extractorInit = extractorList[data.typeFlag]
  594.           assert(extractorInit, "Unknown type flag \""..tostring(data.typeFlag).."\"")
  595.           extractor = extractorInit(data, options)
  596.         end
  597.       else
  598.         extractor = extractor(block)
  599.       end
  600.       if type(extractor) == "number" then
  601.         if extractor > 0 then
  602.           --adjust extractorInit to block size
  603.           local bytesToSkip = math.ceil(extractor / BLOCK_SIZE) * BLOCK_SIZE
  604.           --skip (extractorInit) bytes
  605.           assert(source:seek("cur", bytesToSkip))
  606.         end
  607.         --expect next header
  608.         extractor = nil
  609.       end
  610.     end
  611.     assert(extractor == nil, "Error: Reached end of file but expecting more data!")
  612.     if closeAtExit then
  613.       source:close()
  614.     end
  615.     if not hasDoubleNull then
  616.       print("Warning: Archive does not end with two Null blocks!")
  617.     end
  618.   end
  619. end
  620.  
  621.  
  622. --connect function parameters with actions
  623. local actions = {
  624.   c = function(files, options, ignoredObjects) --create
  625.     tarFiles(files, options, "wb", ignoredObjects)
  626.   end,
  627.   r = function(files, options, ignoredObjects) --append
  628.     tarFiles(files, options, "rb+", ignoredObjects)
  629.   end,
  630.   x = function(files, options, ignoredObjects) --extract
  631.     untarFiles(files, options, extractingExtractors)
  632.   end,
  633.   t = function(files, options, ignoredObjects) --list
  634.     untarFiles(files, options, listingExtractors)
  635.   end,
  636. }
  637. --also add some aliases
  638. actions["create"]  = actions.c
  639. actions["append"]  = actions.r
  640. actions["list"]    = actions.t
  641. actions["extract"] = actions.x
  642. actions["get"]     = actions.x
  643.  
  644. local debugEnabled = false
  645.  
  646. local function main(...)
  647.   --variables containing the processed arguments
  648.   local action, files
  649.   --prepare arguments
  650.   local params, options = shell.parse(...)
  651.   --add stacktrace to output
  652.   debugEnabled = options.debug
  653.   --quick help
  654.   if options.help then
  655.     print(USAGE_TEXT)
  656.     return
  657.   end
  658.   --determine executed function and options
  659.   for option, value in pairs(options) do
  660.     local isAction = actions[option]
  661.     if isAction then
  662.       assert(action == nil, ERRORS.multipleActions)
  663.       action = isAction
  664.       options[option] = nil
  665.     end
  666.   end
  667.   assert(action ~= nil, ERRORS.missingAction)
  668.   --prepare file names
  669.   files = params
  670.   --process options
  671.   if options.v then
  672.     options.verbose = true
  673.   end
  674.   if options.dir then
  675.     assert(options.dir ~= true and options.dir ~= "", "Error: Invalid --dir value!")
  676.     WORKING_DIRECTORY = shell.resolve(options.dir) or options.dir
  677.     assert(WORKING_DIRECTORY ~= nil and WORKING_DIRECTORY ~= "", "Error: Invalid --dir value!")
  678.   end
  679.   if options.f or options.file then
  680.     --use file for archiving
  681.     --keep file names as they are
  682.   else
  683.     --use tape
  684.     local tapeFile = {drive = component.tape_drive, pos = 0}
  685.     do
  686.       --check for Computronics bug
  687.       local endInversionBug = false
  688.       --step 1: move to the end of the tape
  689.       local movedBy = assert(tapeFile.drive.seek(tapeFile.drive.getSize()))
  690.       --step 2: check output of isEnd
  691.       if tapeFile.drive.isEnd() ~= true then
  692.         endInversionBug = true
  693.       end
  694.       --step 3: restore previous position
  695.       assert(tapeFile.drive.seek(-movedBy) == -movedBy, "Error: Tape did not return to original position after checking for isEnd bug!")
  696.      
  697.       if endInversionBug then
  698.         if options.verbose then
  699.           print("tape_drive.isEnd() bug detected; adjusting...")
  700.         end
  701.         function tapeFile:isEnd()
  702.           --This is a workaround for bugged versions of Computronics.
  703.           return (not self.drive.isEnd()) or (not self.drive.isReady())
  704.         end
  705.       else
  706.         function tapeFile:isEnd()
  707.           --This does not work in bugged versions of Computronics.
  708.           return self.drive.isEnd()
  709.         end
  710.       end
  711.     end
  712.     --create some kind of "tape stream" with limited buf sufficient functionality
  713.     function tapeFile:lines(byteCount)
  714.       return function()
  715.         return self:read(byteCount)
  716.       end
  717.     end
  718.     function tapeFile:read(byteCount)
  719.       if self:isEnd() then
  720.         return nil
  721.       end
  722.       local data = self.drive.read(byteCount)
  723.       self.pos = self.pos + #data
  724.       return data
  725.     end
  726.     function tapeFile:write(text)
  727.       self.drive.write(text)
  728.       self.pos = self.pos + #text
  729.       if self:isEnd() then
  730.         return nil, "Error: Reached end of tape!"
  731.       else
  732.         return self
  733.       end
  734.     end
  735.     function tapeFile:seek(typ, pos)
  736.       local toSeek
  737.       if typ == "set" then
  738.         toSeek = pos - self.pos
  739.       elseif typ == "cur" then
  740.         toSeek = pos
  741.       end
  742.       local movedBy = 0
  743.       if toSeek ~= 0 then
  744.         movedBy = self.drive.seek(toSeek)
  745.         self.pos = self.pos + movedBy
  746.       end
  747.       if movedBy == toSeek then
  748.         return self.pos
  749.       else
  750.         return nil, "Error: Unable to seek!"
  751.       end
  752.     end
  753.     --add tape before first file
  754.     table.insert(files, 1, tapeFile)
  755.   end
  756.   if options.h then
  757.     options.dereference = true
  758.   end
  759.  
  760.   --prepare list of ignored objects, default is the current directory and the target file if applicable
  761.   local ignoredObjects = {}
  762.   ignoredObjects[WORKING_DIRECTORY] = true
  763.   if options.exclude then
  764.     --";" is used as a separator
  765.     for excluded in options.exclude:gmatch("[^%;]+") do
  766.       ignoredObjects[shell.resolve(excluded) or excluded] = "strict"
  767.     end
  768.   end
  769.  
  770.   assert(#files > 0, ERRORS.missingFiles)
  771.   --And action!
  772.   action(files, options, ignoredObjects)
  773. end
  774.  
  775. --adding stack trace when --debug is used
  776. local function errorFormatter(msg)
  777.   msg = msg:gsub("^[^%:]+%:[^%:]+%: ","")
  778.   if debugEnabled then
  779.     --add traceback when debugging
  780.     return debug.traceback(msg, 3)
  781.   end
  782.   return msg
  783. end
  784.  
  785. local ok, msg = xpcall(main, errorFormatter, ...)
  786. if not ok then
  787.   io.stdout:write(msg)
  788. end
RAW Paste Data