mirror of
https://github.com/tsolomko/SWCompression.git
synced 2026-06-23 14:56:41 +00:00
231 lines
9.7 KiB
Swift
231 lines
9.7 KiB
Swift
// Copyright (c) 2026 Timofey Solomko
|
|
// Licensed under MIT License
|
|
//
|
|
// See LICENSE for license information
|
|
|
|
import Foundation
|
|
import SWCompression
|
|
import SwiftCLI
|
|
|
|
final class TarCommand: Command {
|
|
|
|
let name = "tar"
|
|
let shortDescription = "Extracts a TAR container"
|
|
|
|
@Flag("-z", "--gz", description: "With -e: decompress with GZip first; with -c: compress container with GZip")
|
|
var gz: Bool
|
|
|
|
@Flag("-j", "--bz2", description: "With -e: decompress with BZip2 first; with -c: compress container with BZip2")
|
|
var bz2: Bool
|
|
|
|
@Flag("-x", "--xz", description: "With -e: decompress with XZ first; with -c: not supported")
|
|
var xz: Bool
|
|
|
|
@Flag("-i", "--info", description: "Print the information about of the entries in the container including their attributes")
|
|
var info: Bool
|
|
|
|
@Flag("-l", "--list", description: "Print the list of names of the entries in the container")
|
|
var list: Bool
|
|
|
|
@Key("-e", "--extract", description: "Extract a container into specified directory")
|
|
var extract: String?
|
|
|
|
@Flag("-f", "--format", description: "Print the \"format\" of a container")
|
|
var format: Bool
|
|
|
|
@Key("-c", "--create", description: "Create a new container containing the specified file/directory (recursively)")
|
|
var create: String?
|
|
|
|
@Key("--use-format", description: "Use specified TAR format when creating a container; available options: prePosix, ustar, gnu, pax")
|
|
var useFormat: TarContainer.Format?
|
|
|
|
@Flag("-v", "--verbose", description: "Print the list of extracted files and directories.")
|
|
var verbose: Bool
|
|
|
|
var optionGroups: [OptionGroup] {
|
|
return [.atMostOne($gz, $bz2, $xz), .exactlyOne($info, $list, $extract, $format, $create)]
|
|
}
|
|
|
|
@Param var input: String
|
|
|
|
func execute() throws {
|
|
if self.useFormat != nil && self.create == nil {
|
|
print("WARNING: --use-format option is ignored without -c/--create option")
|
|
}
|
|
|
|
var fileData: Data
|
|
if self.create == nil {
|
|
fileData = try Data(contentsOf: URL(fileURLWithPath: self.input),
|
|
options: .mappedIfSafe)
|
|
|
|
if gz {
|
|
fileData = try GzipArchive.unarchive(archive: fileData)
|
|
} else if bz2 {
|
|
fileData = try BZip2.decompress(data: fileData)
|
|
} else if xz {
|
|
fileData = try XZArchive.unarchive(archive: fileData)
|
|
}
|
|
} else {
|
|
fileData = Data()
|
|
}
|
|
|
|
if info || list {
|
|
if gz {
|
|
fileData = try Data(contentsOf: URL(fileURLWithPath: self.input),
|
|
options: .mappedIfSafe)
|
|
fileData = try GzipArchive.unarchive(archive: fileData)
|
|
let entries = try TarContainer.info(container: fileData)
|
|
if info {
|
|
swcomp.printInfo(entries)
|
|
} else {
|
|
swcomp.printList(entries)
|
|
}
|
|
} else if bz2 {
|
|
fileData = try Data(contentsOf: URL(fileURLWithPath: self.input),
|
|
options: .mappedIfSafe)
|
|
fileData = try BZip2.decompress(data: fileData)
|
|
let entries = try TarContainer.info(container: fileData)
|
|
if info {
|
|
swcomp.printInfo(entries)
|
|
} else {
|
|
swcomp.printList(entries)
|
|
}
|
|
} else if xz {
|
|
fileData = try Data(contentsOf: URL(fileURLWithPath: self.input),
|
|
options: .mappedIfSafe)
|
|
fileData = try XZArchive.unarchive(archive: fileData)
|
|
let entries = try TarContainer.info(container: fileData)
|
|
if info {
|
|
swcomp.printInfo(entries)
|
|
} else {
|
|
swcomp.printList(entries)
|
|
}
|
|
} else {
|
|
guard let handle = FileHandle(forReadingAtPath: self.input)
|
|
else { swcompExit(.fileHandleCannotOpen) }
|
|
|
|
var reader = TarReader(fileHandle: handle)
|
|
var isFinished = false
|
|
while !isFinished {
|
|
isFinished = try reader.process { (entry: TarEntry?) -> Bool in
|
|
guard entry != nil
|
|
else { return true }
|
|
if info {
|
|
print(entry!.info)
|
|
print("------------------\n")
|
|
} else {
|
|
print(entry!.info.name)
|
|
}
|
|
return false
|
|
}
|
|
}
|
|
try handle.close()
|
|
}
|
|
} else if let outputPath = self.extract {
|
|
guard try isValidOutputDirectory(outputPath, create: true)
|
|
else { swcompExit(.containerOutPathExistsNotDir) }
|
|
|
|
if gz {
|
|
fileData = try Data(contentsOf: URL(fileURLWithPath: self.input),
|
|
options: .mappedIfSafe)
|
|
fileData = try GzipArchive.unarchive(archive: fileData)
|
|
let entries = try TarContainer.open(container: fileData)
|
|
try swcomp.write(entries, outputPath, verbose)
|
|
} else if bz2 {
|
|
fileData = try Data(contentsOf: URL(fileURLWithPath: self.input),
|
|
options: .mappedIfSafe)
|
|
fileData = try BZip2.decompress(data: fileData)
|
|
let entries = try TarContainer.open(container: fileData)
|
|
try swcomp.write(entries, outputPath, verbose)
|
|
} else if xz {
|
|
fileData = try Data(contentsOf: URL(fileURLWithPath: self.input),
|
|
options: .mappedIfSafe)
|
|
fileData = try XZArchive.unarchive(archive: fileData)
|
|
let entries = try TarContainer.open(container: fileData)
|
|
try swcomp.write(entries, outputPath, verbose)
|
|
} else {
|
|
if verbose {
|
|
print("d = directory, f = file, l = symbolic link")
|
|
}
|
|
|
|
guard let handle = FileHandle(forReadingAtPath: self.input)
|
|
else { swcompExit(.fileHandleCannotOpen) }
|
|
var reader = TarReader(fileHandle: handle)
|
|
let fileManager = FileManager.default
|
|
let outputURL = URL(fileURLWithPath: outputPath)
|
|
var directoryAttributes = [(attributes: [FileAttributeKey: Any], path: String)]()
|
|
var isFinished = false
|
|
while !isFinished {
|
|
isFinished = try reader.process { (entry: TarEntry?) -> Bool in
|
|
guard entry != nil
|
|
else { return true }
|
|
if entry!.info.type == .directory {
|
|
directoryAttributes.append(try writeDirectory(entry!, outputURL, verbose))
|
|
} else {
|
|
try writeFile(entry!, outputURL, verbose)
|
|
}
|
|
return false
|
|
}
|
|
}
|
|
try handle.close()
|
|
|
|
for tuple in directoryAttributes {
|
|
try fileManager.setAttributes(tuple.attributes, ofItemAtPath: tuple.path)
|
|
}
|
|
}
|
|
} else if format {
|
|
let format = try TarContainer.formatOf(container: fileData)
|
|
switch format {
|
|
case .prePosix:
|
|
print("TAR format: pre-POSIX")
|
|
case .ustar:
|
|
print("TAR format: POSIX aka \"ustar\"")
|
|
case .gnu:
|
|
print("TAR format: POSIX with GNU extensions")
|
|
case .pax:
|
|
print("TAR format: PAX")
|
|
}
|
|
} else if let inputPath = self.create {
|
|
guard !xz
|
|
else { swcompExit(.tarCreateXzNotSupported) }
|
|
|
|
let fileManager = FileManager.default
|
|
|
|
guard !fileManager.fileExists(atPath: self.input)
|
|
else { swcompExit(.tarCreateOutPathExists) }
|
|
|
|
guard fileManager.fileExists(atPath: inputPath)
|
|
else { swcompExit(.tarCreateInPathDoesNotExist) }
|
|
|
|
if verbose {
|
|
print("Creating new container at \"\(self.input)\" from \"\(inputPath)\"")
|
|
print("d = directory, f = file, l = symbolic link")
|
|
}
|
|
if gz {
|
|
let entries = try TarEntry.createEntries(inputPath, verbose)
|
|
var outData = TarContainer.create(from: entries, force: self.useFormat ?? .pax)
|
|
let outputURL = URL(fileURLWithPath: self.input)
|
|
let fileName = outputURL.lastPathComponent
|
|
outData = try GzipArchive.archive(data: outData, fileName: fileName.isEmpty ? nil : fileName,
|
|
writeHeaderCRC: true)
|
|
try outData.write(to: outputURL)
|
|
} else if bz2 {
|
|
let entries = try TarEntry.createEntries(inputPath, verbose)
|
|
var outData = TarContainer.create(from: entries, force: self.useFormat ?? .pax)
|
|
let outputURL = URL(fileURLWithPath: self.input)
|
|
outData = BZip2.compress(data: outData)
|
|
try outData.write(to: outputURL)
|
|
} else {
|
|
let outputURL = URL(fileURLWithPath: self.input)
|
|
try "".write(to: outputURL, atomically: true, encoding: .utf8)
|
|
let handle = try FileHandle(forWritingTo: outputURL)
|
|
var writer = TarWriter(fileHandle: handle, force: self.useFormat ?? .pax)
|
|
try TarEntry.generateEntries(&writer, inputPath, verbose)
|
|
try writer.finalize()
|
|
try handle.close()
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|