R3C0NF16UR3D

tar.lua

Jul 30th, 2023
122
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 26.95 KB | None | 0 0
  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: detect symbolic link cycles (-> remember already visited, resolved paths)
  13. ]]
  14. local shell = require 'shell'
  15. local fs = require 'filesystem'
  16. local component = require 'component'
  17.  
  18. local BLOCK_SIZE = 512
  19. local NULL_BLOCK = ("\0"):rep(BLOCK_SIZE)
  20. local WORKING_DIRECTORY = fs.canonical(shell.getWorkingDirectory()):gsub("/$","")
  21.  
  22. --load auto_progress library if possible
  23. local auto_progress
  24. if true then
  25. local ok
  26. ok, auto_progress = pcall(require, "mpm.auto_progress")
  27. if not ok then
  28. auto_progress = {}
  29. function auto_progress.new()
  30. --library not available, create stub
  31. return {
  32. update = function() end,
  33. finish = function() end,
  34. }
  35. end
  36. end
  37. end
  38.  
  39. --error information
  40. local USAGE_TEXT = [[
  41. Usage:
  42. tar <function letter> [other options] FILES...
  43. function letter description
  44. -c --create creates a new archive
  45. -r --append appends to existing archive
  46. -t --list lists contents from archive
  47. -x --extract --get extracts from archive
  48. other options description
  49. -f --file FILE first FILE is archive, else:
  50. uses primary tape drive
  51. --address=ADDRESS uses tape drive ADDRESS
  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. if type(options.address) == "string" then
  686. tapeFile.drive = component.proxy(assert(component.get(options.address, "tape_drive")))
  687. end
  688. assert(tapeFile.drive, "Error: No tape drive found!")
  689. assert(tapeFile.drive.type == "tape_drive", "Error: Address does not point to tape drive!")
  690. do
  691. --check for Computronics bug
  692. local endInversionBug = false
  693. --step 1: move to the end of the tape
  694. local movedBy = assert(tapeFile.drive.seek(tapeFile.drive.getSize()))
  695. --step 2: check output of isEnd
  696. if tapeFile.drive.isEnd() ~= true then
  697. endInversionBug = true
  698. end
  699. --step 3: restore previous position
  700. assert(tapeFile.drive.seek(-movedBy) == -movedBy, "Error: Tape did not return to original position after checking for isEnd bug!")
  701.  
  702. if endInversionBug then
  703. if options.verbose then
  704. print("tape_drive.isEnd() bug detected; adjusting...")
  705. end
  706. function tapeFile:isEnd()
  707. --This is a workaround for bugged versions of Computronics.
  708. return (not self.drive.isEnd()) or (not self.drive.isReady())
  709. end
  710. else
  711. function tapeFile:isEnd()
  712. --This does not work in bugged versions of Computronics.
  713. return self.drive.isEnd()
  714. end
  715. end
  716. end
  717. --create some kind of "tape stream" with limited buf sufficient functionality
  718. function tapeFile:lines(byteCount)
  719. return function()
  720. return self:read(byteCount)
  721. end
  722. end
  723. function tapeFile:read(byteCount)
  724. if self:isEnd() then
  725. return nil
  726. end
  727. local data = self.drive.read(byteCount)
  728. self.pos = self.pos + #data
  729. return data
  730. end
  731. function tapeFile:write(text)
  732. self.drive.write(text)
  733. self.pos = self.pos + #text
  734. if self:isEnd() then
  735. return nil, "Error: Reached end of tape!"
  736. else
  737. return self
  738. end
  739. end
  740. function tapeFile:seek(typ, pos)
  741. local toSeek
  742. if typ == "set" then
  743. toSeek = pos - self.pos
  744. elseif typ == "cur" then
  745. toSeek = pos
  746. end
  747. local movedBy = 0
  748. if toSeek ~= 0 then
  749. movedBy = self.drive.seek(toSeek)
  750. self.pos = self.pos + movedBy
  751. end
  752. if movedBy == toSeek then
  753. return self.pos
  754. else
  755. return nil, "Error: Unable to seek!"
  756. end
  757. end
  758. --add tape before first file
  759. table.insert(files, 1, tapeFile)
  760. end
  761. if options.h then
  762. options.dereference = true
  763. end
  764.  
  765. --prepare list of ignored objects, default is the current directory and the target file if applicable
  766. local ignoredObjects = {}
  767. ignoredObjects[WORKING_DIRECTORY] = true
  768. if options.exclude then
  769. --";" is used as a separator
  770. for excluded in options.exclude:gmatch("[^%;]+") do
  771. ignoredObjects[shell.resolve(excluded) or excluded] = "strict"
  772. end
  773. end
  774.  
  775. assert(#files > 0, ERRORS.missingFiles)
  776. --And action!
  777. action(files, options, ignoredObjects)
  778. end
  779.  
  780. --adding stack trace when --debug is used
  781. local function errorFormatter(msg)
  782. msg = msg:gsub("^[^%:]+%:[^%:]+%: ","")
  783. if debugEnabled then
  784. --add traceback when debugging
  785. return debug.traceback(msg, 3)
  786. end
  787. return msg
  788. end
  789.  
  790. local ok, msg = xpcall(main, errorFormatter, ...)
  791. if not ok then
  792. io.stdout:write(msg)
  793. end
  794.  
Advertisement
Add Comment
Please, Sign In to add comment