| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
Zipdiff is a JVM library and Gradle plugin for generating and applying compact, binary delta patches between ZIP archives. It is designed for scenarios where you need to ship incremental updates of ZIP-based artifacts (application bundles, asset packs, plugin archives, APK-like containers, etc.) without redistributing the full archive on every release.
Zipdiff works by:
ZIP archives are notoriously unfriendly to generic binary-diff tools: reordering entries, re-compressing unchanged bytes, or shifting timestamps/permissions can cause two functionally identical archives to differ almost entirely at the byte level. Zipdiff avoids this problem by operating on the logical entry level rather than the raw container bytes:
| Module | Description |
|---|---|
| zipdiff-core | Pure JVM/Kotlin library implementing canonicalization, diffing, packaging, patch application, and signature handling. No Gradle dependency. |
| zipdiff-plugin | A Gradle plugin (io.cognotik.zipdiff-patch) that wires the core library into a build, exposing a generateZipdiffPatch task and a zipdiff { ... } DSL extension. |
Implemented by Canonicalizer and configured via CanonicalProfile.
Given an arbitrary input ZIP, Canonicalizer.canonicalize(...) produces a new ZIP where:
The result is returned as a CanonicalResult, which reports the number of entries processed and the SHA-256 hash of the canonical archive. This hash is the basis for all downstream integrity checks (patch metadata, signature verification).
Implemented by DiffGenerator.
DiffGenerator.generateDiff(baseZip, targetZip, profile) reads both (canonical) archives, compares them entry-by-entry by normalized path, and produces a sorted list of DiffEntry objects, each tagged with an EntryMode:
| Mode | Meaning |
|---|---|
| UNCHANGED | Entry content is identical between base and target (no payload carried). |
| NEW | Entry exists only in target (raw payload carried). |
| MODIFIED | Entry exists in both but content differs (delta-compressed payload). |
| DELETED | Entry exists only in base (tombstone, no payload). |
| EMPTY_FILE | Entry resolves to a zero-length file in the target. |
For MODIFIED entries, the target bytes are compressed via DeflateDictionaryEngine using the base entry's bytes as a preset DEFLATE dictionary (compressWithDict). This lets the compressor reference unchanged byte sequences from the old file, so small edits to large files can produce very small patch payloads. If dictionary-based compression doesn't actually help (e.g. for unrelated content), the engine automatically falls back to standard DEFLATE.
DEFLATE preset dictionaries are limited to a 32 KB sliding window (DeflateDictionaryEngine.MAX_DICT_SIZE); for larger base files only the trailing 32 KB is used as the dictionary.
Implemented by PatchPackager, with data model in PatchPackage.kt.
A patch package is itself a plain ZIP file (conventionally named <base>-to-<target>.patch.zp) with the following internal layout:
META-INF/version.txt # baseVersion=..., targetVersion=... META-INF/canonicalization.json # canonicalizationProfileVersion used to build the patch META-INF/canonical-zip.sha256 # expected SHA-256 of the reconstructed canonical target META-INF/signature-schemes.json # list of signature scheme identifiers included META-INF/signatures.json # (optional) serialized SignatureBlock list META-INF/entries.json # manifest describing each DIFF/ entry (path, mode, metadata) DIFF/<path> # payload for NEW / MODIFIED / EMPTY_FILE entries DIFF/<path>.tombstone # zero-length marker for DELETED entries
Implemented by PatchApplier.
PatchApplier.applyPatch(baseZip, patchPackage, outputPath):
Implemented by PatchChainApplier.
When upgrading across multiple versions (e.g. v1 → v2 → v3), PatchChainApplier.applyChain sequentially applies an ordered list of PatchPackages, using the output of each step as the base for the next, and writing only the final result to the requested outputPath (intermediate results are held in temp files that are cleaned up automatically). Errors at any step abort the chain with a ZipdiffException, and the applier refuses to run if the base and output paths are the same file.
Implemented by SignatureManager and SignatureBlock.
Zipdiff treats signing as an orthogonal, pluggable concern:
Four placement strategies are supported via PlacementRule: META_INF_ENTRY, CENTRAL_DIRECTORY_COMMENT, EXTRA_FIELD, and DEDICATED_SECTION.
Add zipdiff-core as a dependency (published as org.zipdiff:zipdiff-core), then:
import io.cognotik.zipdiff.canonical.Canonicalizer
import io.cognotik.zipdiff.diff.DiffGenerator
import io.cognotik.zipdiff.package.PatchMetadata
import io.cognotik.zipdiff.package.PatchPackager
import io.cognotik.zipdiff.patch.PatchApplier
import java.nio.file.Path
// 1. Canonicalize base and target archives
val canonicalizer = Canonicalizer()
val baseResult = canonicalizer.canonicalize(Path.of("base.zip"), Path.of("base.canonical.zip"))
val targetResult = canonicalizer.canonicalize(Path.of("target.zip"), Path.of("target.canonical.zip"))
// 2. Generate a logical diff
val diffs = DiffGenerator.generateDiff(
Path.of("base.canonical.zip"),
Path.of("target.canonical.zip"),
profile = io.cognotik.zipdiff.canonical.CanonicalProfile()
)
// 3. Package the patch
val metadata = PatchMetadata(
baseVersion = "1.0.0",
targetVersion = "1.1.0",
canonicalizationProfileVersion = "v1",
canonicalZipSha256 = targetResult.sha256Hex
)
PatchPackager.writePatchPackage(Path.of("1.0.0-to-1.1.0.patch.zp"), metadata, diffs)
// 4. Apply the patch later, on the recipient side
val patchPackage = PatchPackager.readPatchPackage(Path.of("1.0.0-to-1.1.0.patch.zp"))
PatchApplier.applyPatch(Path.of("base.canonical.zip"), patchPackage, Path.of("reconstructed-target.zip"))For multi-step upgrades, use PatchChainApplier.applyChain(baseZip, listOf(patch1, patch2, ...), outputPath).
Apply the plugin (io.cognotik.zipdiff-patch) in a project that wants to auto-generate a patch as part of its build:
plugins {
id("io.cognotik.zipdiff-patch")
}
zipdiff {
baseArchive.set(layout.projectDirectory.file("releases/app-1.0.0.zip"))
targetArchive.set(layout.projectDirectory.file("build/distributions/app-1.1.0.zip"))
baseVersion.set("1.0.0")
targetVersion.set("1.1.0")
outputDirectory.set(layout.buildDirectory.dir("zipdiff"))
// optional
canonicalProfileVersion.set("v1") // defaults to "v1"
signatureScheme.set("scheme-v1") // defaults to "scheme-v1"
fallbackOnMissingBase.set(true) // defaults to true
}This registers a generateZipdiffPatch task that:
This project uses the Gradle wrapper; no local Gradle installation is required.
./gradlew build # compiles and tests both modules
./gradlew test # runs zipdiff-core's unit tests
./gradlew publishToMavenLocal # (if publishing is configured) install artifacts locallyzipdiff-core/
src/main/kotlin/io/cognotik/zipdiff/
canonical/ # CanonicalProfile, Canonicalizer, CanonicalResult
deflate/ # DeflateDictionaryEngine (preset-dictionary DEFLATE compression)
diff/ # DiffEntry, EntryMode, DiffGenerator
exception/ # ZipdiffException, SignatureValidationException
package/ # PatchMetadata, PatchPackage, PatchPackager (.patch.zp I/O)
patch/ # PatchApplier, PatchChainApplier
signature/ # SignatureBlock, SignatureMetadata, SignatureManager
zipdiff-plugin/
src/main/kotlin/io/cognotik/zipdiff/plugin/
ZipdiffExtension.kt # `zipdiff { ... }` DSL
ZipdiffPatchPlugin.kt # Plugin entry point / task wiring
ZipdiffPatchTask.kt # `generateZipdiffPatch` task implementation
| Back | FazBrowse Home | New Git URL |