Skip to content

Commit

Permalink
refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
lukaszwawrzyk committed Jul 25, 2018
1 parent a741418 commit d24f628
Show file tree
Hide file tree
Showing 2 changed files with 123 additions and 93 deletions.
97 changes: 4 additions & 93 deletions src/main/scala/com/virtuslab/zipops/Main.scala
Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@
package com.virtuslab.zipops

import java.io.RandomAccessFile
import java.nio.channels.{ Channels, FileChannel }

import net.lingala.zip4j.core.{ HeaderReader, HeaderWriter }
import net.lingala.zip4j.model.{ ZipModel, FileHeader }
import net.lingala.zip4j.model.FileHeader
import java.nio.file._
import java.util.ArrayList

import scala.collection.JavaConverters._

object Main extends App {

Expand All @@ -27,95 +20,13 @@ object Main extends App {
} else Paths.get(file)
}

def readModel(path: Path): ZipModel = {
val file = new RandomAccessFile(path.toFile, "rw")
val headerReader = new HeaderReader(file)
val model = headerReader.readAllHeaders()
file.close()
model
}

def getHeaders(model: ZipModel): Seq[FileHeader] = {
model.getCentralDirectory.getFileHeaders.asInstanceOf[ArrayList[FileHeader]].asScala
}

def printCentralDir(path: Path): Unit = {
getHeaders(readModel(path)).foreach(printHeader)
}

def removeEntries(path: Path, toRemove: Set[String]): Unit = {
val model = readModel(path)
val headers = getHeaders(model)
val clearedHeaders = headers.filterNot(header => toRemove.contains(header.getFileName))
model.getCentralDirectory.setFileHeaders(asArrayList(clearedHeaders))

val writeOffset = startOfCentralDir(model)
val channel = createChannel(path)
channel.truncate(writeOffset)
val outputStream = Channels.newOutputStream(channel.position(writeOffset))
val headerWriter = new HeaderWriter
headerWriter.finalizeZipFile(model, outputStream)
}

private def asArrayList[A](clearedHeaders: Seq[A]): ArrayList[A] = {
new ArrayList[A](clearedHeaders.asJava)
}

private def createChannel(path: Path) = {
new RandomAccessFile(path.toFile, "rw").getChannel
}

def mergeArchives(to: Path, from: Path): Unit = {
val toModel = readModel(to)
val fromModel = readModel(from)

val toChannel = createChannel(to)
val fromChannel = createChannel(from)
toChannel.truncate(startOfCentralDir(toModel))
// todo use loop (while not all transferred...)
val fromStart = toChannel.size()
val fromLength = startOfCentralDir(fromModel)
transferAll(toChannel, fromChannel, fromStart, fromLength)
fromChannel.close()

val fromHeaders = getHeaders(fromModel)
fromHeaders.foreach { header =>
// potentially offsets should be updated for each header
// not only in central directory but a valid extractor
// should not rely on that unless jar is corrupted
val currOffset = header.getOffsetLocalHeader
val newOffset = currOffset + fromStart
header.setOffsetLocalHeader(newOffset)
}
val centralDirStart = fromStart + fromLength
val toHeaders = getHeaders(toModel)
val newHeaders = toHeaders ++ fromHeaders
toModel.getCentralDirectory.setFileHeaders(asArrayList(newHeaders))
toModel.getEndCentralDirRecord.setOffsetOfStartOfCentralDir(centralDirStart)

val outputStream = Channels.newOutputStream(toChannel.position(centralDirStart))
val headerWriter = new HeaderWriter
headerWriter.finalizeZipFile(toModel, outputStream)
toChannel.close()
}

private def transferAll(to: FileChannel, from: FileChannel, startPos: Long, bytesToTransfer: Long): Unit = {
var remaining = bytesToTransfer
var offset = startPos
while (remaining > 0) {
val transferred = to.transferFrom(from, /*position =*/ offset, /*count = */ remaining)
offset += transferred
remaining -= transferred
}
}

private def startOfCentralDir(model: ZipModel) = {
model.getEndCentralDirRecord.getOffsetOfStartOfCentralDir
ZipOps.getCentralDir(path).foreach(printHeader)
}

// val path = getPath("/home/lukasz/dev/test/test.jar", copy = true)
// printCentralDir(path)
// removeEntries(path, Set("test/A1", "test/B2"))
// ZipOps.removeEntries(path, Set("test/A1", "test/B2"))
// println("AFTER")
// printCentralDir(path)

Expand All @@ -125,7 +36,7 @@ object Main extends App {
printCentralDir(to)
println("FROM")
printCentralDir(from)
mergeArchives(to, from)
ZipOps.mergeArchives(to, from)
println("AFTER")
printCentralDir(to)
}
119 changes: 119 additions & 0 deletions src/main/scala/com/virtuslab/zipops/ZipOps.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package com.virtuslab.zipops

import java.io.RandomAccessFile

import net.lingala.zip4j.core.{ HeaderReader, HeaderWriter }
import java.nio.channels.{ FileChannel, Channels }
import java.nio.file.Path
import java.util.ArrayList

import scala.collection.JavaConverters._
import net.lingala.zip4j.model.{ ZipModel, FileHeader }

object ZipOps {

def getCentralDir(path: Path): Seq[FileHeader] = {
getHeaders(readModel(path))
}

def removeEntries(path: Path, toRemove: Set[String]): Unit = {
val model = readModel(path)
removeEntriesFromCentralDir(model, toRemove)
val file = openFile(path)
truncateCentralDir(model, file)
val writeOffset = startOfCentralDir(model)
finalizeZip(model, file, writeOffset)
}

private def removeEntriesFromCentralDir(model: ZipModel, toRemove: Set[String]): Unit = {
val headers = getHeaders(model)
val clearedHeaders = headers.filterNot(header => toRemove.contains(header.getFileName))
model.getCentralDirectory.setFileHeaders(asArrayList(clearedHeaders))
}

def mergeArchives(to: Path, from: Path): Unit = {
val modelTo = readModel(to)
val modelFrom = readModel(from)

val fileTo = openFile(to)
val fileFrom = openFile(from)

truncateCentralDir(modelTo, fileTo)

// "from" starts where "to" ends
val fromStart = fileTo.size()
// "from" is as long as from the beginning till the start of central dir
val fromLength = startOfCentralDir(modelFrom)

transferAll(from = fileFrom, to = fileTo, startPos = fromStart, bytesToTransfer = fromLength)
fileFrom.close()

val mergedHeaders = mergeHeaders(modelTo, modelFrom, fromStart)
modelTo.getCentralDirectory.setFileHeaders(asArrayList(mergedHeaders))

val centralDirStart = fromStart + fromLength
modelTo.getEndCentralDirRecord.setOffsetOfStartOfCentralDir(centralDirStart)

finalizeZip(modelTo, fileTo, centralDirStart)
}

private def mergeHeaders(modelTo: ZipModel, modelFrom: ZipModel, fromStart: Long): Seq[FileHeader] = {
val fromHeaders = getHeaders(modelFrom)
fromHeaders.foreach { header =>
// potentially offsets should be updated for each header
// not only in central directory but a valid zip tool
// should not rely on that unless the file is corrupted
val currentOffset = header.getOffsetLocalHeader
val newOffset = currentOffset + fromStart
header.setOffsetLocalHeader(newOffset)
}
val toHeaders = getHeaders(modelTo)
toHeaders ++ fromHeaders
}

private def readModel(path: Path): ZipModel = {
val file = new RandomAccessFile(path.toFile, "rw")
val headerReader = new HeaderReader(file)
val model = headerReader.readAllHeaders()
file.close()
model
}

private def startOfCentralDir(model: ZipModel) = {
model.getEndCentralDirRecord.getOffsetOfStartOfCentralDir
}

private def getHeaders(model: ZipModel): Seq[FileHeader] = {
model.getCentralDirectory.getFileHeaders.asInstanceOf[ArrayList[FileHeader]].asScala
}

private def truncateCentralDir(model: ZipModel, channel: FileChannel): FileChannel = {
channel.truncate(startOfCentralDir(model))
}

private def finalizeZip(centralDir: ZipModel, channel: FileChannel, centralDirStart: Long): Unit = {
val outputStream = Channels.newOutputStream(channel.position(centralDirStart))
val headerWriter = new HeaderWriter
headerWriter.finalizeZipFile(centralDir, outputStream)
channel.close()
}

private def openFile(path: Path) = {
new RandomAccessFile(path.toFile, "rw").getChannel
}

private def transferAll(from: FileChannel, to: FileChannel, startPos: Long, bytesToTransfer: Long): Unit = {
var remaining = bytesToTransfer
var offset = startPos
while (remaining > 0) {
val transferred = to.transferFrom(from, /*position =*/ offset, /*count = */ remaining)
offset += transferred
remaining -= transferred
}
}

private def asArrayList[A](clearedHeaders: Seq[A]): ArrayList[A] = {
new ArrayList[A](clearedHeaders.asJava)
}

}

0 comments on commit d24f628

Please sign in to comment.