231 lines
5.5 KiB
Go
231 lines
5.5 KiB
Go
|
|
package delta
|
||
|
|
|
||
|
|
import (
|
||
|
|
"archive/zip"
|
||
|
|
"bytes"
|
||
|
|
"crypto/sha256"
|
||
|
|
"encoding/hex"
|
||
|
|
"encoding/json"
|
||
|
|
"fmt"
|
||
|
|
"io"
|
||
|
|
"os"
|
||
|
|
"path/filepath"
|
||
|
|
"strings"
|
||
|
|
)
|
||
|
|
|
||
|
|
// PatchManifest describes the delta operations needed to transform an old JAR into a new JAR.
|
||
|
|
type PatchManifest struct {
|
||
|
|
OldVersion string `json:"oldVersion"`
|
||
|
|
NewVersion string `json:"newVersion"`
|
||
|
|
Deleted []string `json:"deleted"`
|
||
|
|
Added []string `json:"added"`
|
||
|
|
Modified []PatchEntry `json:"modified"`
|
||
|
|
Unchanged int `json:"unchanged"`
|
||
|
|
}
|
||
|
|
|
||
|
|
// PatchEntry represents a single modified file within the delta.
|
||
|
|
type PatchEntry struct {
|
||
|
|
Path string `json:"path"`
|
||
|
|
OldSHA256 string `json:"oldSha256"`
|
||
|
|
NewSHA256 string `json:"newSha256"`
|
||
|
|
DiffSize int64 `json:"diffSize"`
|
||
|
|
DiffHash string `json:"diffHash"`
|
||
|
|
}
|
||
|
|
|
||
|
|
// GenerateJarDelta produces a .patch file by comparing two JARs:
|
||
|
|
// 1. Unzip both JARs into memory.
|
||
|
|
// 2. Compare files; identify added, deleted, and modified entries.
|
||
|
|
// 3. For modified .class files, generate a BSDiff-style binary diff.
|
||
|
|
// 4. Package everything (diff chunks, new files, manifest.json) into a zip.
|
||
|
|
func GenerateJarDelta(oldJarPath, newJarPath, patchPath string) error {
|
||
|
|
oldFiles, err := readJarEntries(oldJarPath)
|
||
|
|
if err != nil {
|
||
|
|
return fmt.Errorf("reading old JAR: %w", err)
|
||
|
|
}
|
||
|
|
newFiles, err := readJarEntries(newJarPath)
|
||
|
|
if err != nil {
|
||
|
|
return fmt.Errorf("reading new JAR: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
manifest := PatchManifest{
|
||
|
|
OldVersion: filepath.Base(oldJarPath),
|
||
|
|
NewVersion: filepath.Base(newJarPath),
|
||
|
|
}
|
||
|
|
|
||
|
|
oldSet := make(map[string][]byte)
|
||
|
|
for _, f := range oldFiles {
|
||
|
|
oldSet[f.Name] = f.Data
|
||
|
|
}
|
||
|
|
newSet := make(map[string][]byte)
|
||
|
|
for _, f := range newFiles {
|
||
|
|
newSet[f.Name] = f.Data
|
||
|
|
}
|
||
|
|
|
||
|
|
// Identify deleted, added, and modified files.
|
||
|
|
for name := range oldSet {
|
||
|
|
if _, ok := newSet[name]; !ok {
|
||
|
|
manifest.Deleted = append(manifest.Deleted, name)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
for name := range newSet {
|
||
|
|
if _, ok := oldSet[name]; !ok {
|
||
|
|
manifest.Added = append(manifest.Added, name)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
for name, newData := range newSet {
|
||
|
|
oldData, ok := oldSet[name]
|
||
|
|
if !ok {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if !bytes.Equal(oldData, newData) {
|
||
|
|
entry := PatchEntry{Path: name}
|
||
|
|
entry.OldSHA256 = sha256Hex(oldData)
|
||
|
|
entry.NewSHA256 = sha256Hex(newData)
|
||
|
|
|
||
|
|
if isBinaryFile(name) {
|
||
|
|
diff := bsdiff(oldData, newData)
|
||
|
|
entry.DiffSize = int64(len(diff))
|
||
|
|
entry.DiffHash = sha256Hex(diff)
|
||
|
|
}
|
||
|
|
|
||
|
|
manifest.Modified = append(manifest.Modified, entry)
|
||
|
|
} else {
|
||
|
|
manifest.Unchanged++
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return writePatchFile(patchPath, &manifest, oldFiles, newFiles, oldSet, newSet)
|
||
|
|
}
|
||
|
|
|
||
|
|
type jarEntry struct {
|
||
|
|
Name string
|
||
|
|
Data []byte
|
||
|
|
}
|
||
|
|
|
||
|
|
func readJarEntries(jarPath string) ([]jarEntry, error) {
|
||
|
|
data, err := os.ReadFile(jarPath)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
|
||
|
|
reader, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
|
||
|
|
var entries []jarEntry
|
||
|
|
for _, f := range reader.File {
|
||
|
|
if f.FileInfo().IsDir() {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
rc, err := f.Open()
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
content, err := io.ReadAll(rc)
|
||
|
|
rc.Close()
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
entries = append(entries, jarEntry{Name: f.Name, Data: content})
|
||
|
|
}
|
||
|
|
return entries, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func isBinaryFile(name string) bool {
|
||
|
|
ext := strings.ToLower(filepath.Ext(name))
|
||
|
|
switch ext {
|
||
|
|
case ".class", ".jar", ".war", ".ear":
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
func sha256Hex(data []byte) string {
|
||
|
|
h := sha256.Sum256(data)
|
||
|
|
return hex.EncodeToString(h[:])
|
||
|
|
}
|
||
|
|
|
||
|
|
func writePatchFile(patchPath string, manifest *PatchManifest, oldFiles, newFiles []jarEntry, oldSet, newSet map[string][]byte) error {
|
||
|
|
out, err := os.Create(patchPath)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
defer out.Close()
|
||
|
|
|
||
|
|
zw := zip.NewWriter(out)
|
||
|
|
defer zw.Close()
|
||
|
|
|
||
|
|
// Write manifest.json
|
||
|
|
manifestData, _ := json.MarshalIndent(manifest, "", " ")
|
||
|
|
w, _ := zw.Create("manifest.json")
|
||
|
|
w.Write(manifestData)
|
||
|
|
|
||
|
|
// Write added files
|
||
|
|
for _, name := range manifest.Added {
|
||
|
|
w, _ := zw.Create("added/" + name)
|
||
|
|
w.Write(newSet[name])
|
||
|
|
}
|
||
|
|
|
||
|
|
// Write modifications (binary diffs)
|
||
|
|
for _, entry := range manifest.Modified {
|
||
|
|
if entry.DiffSize > 0 {
|
||
|
|
oldData := oldSet[entry.Path]
|
||
|
|
newData := newSet[entry.Path]
|
||
|
|
diff := bsdiff(oldData, newData)
|
||
|
|
w, _ := zw.Create("diffs/" + entry.Path + ".bsdiff")
|
||
|
|
w.Write(diff)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// bsdiff produces a simple binary diff between two byte slices.
|
||
|
|
// For production use, integrate a full BSDiff library (e.g. github.com/icedream/bsdiff).
|
||
|
|
func bsdiff(old, new []byte) []byte {
|
||
|
|
// Poor man's binary diff — records differing byte runs.
|
||
|
|
// A production implementation should use a proper BSDiff or VCDIFF library.
|
||
|
|
var buf bytes.Buffer
|
||
|
|
minLen := len(old)
|
||
|
|
if len(new) < minLen {
|
||
|
|
minLen = len(new)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Write header: old size, new size
|
||
|
|
buf.WriteString(fmt.Sprintf("BSDIFF40:%d:%d\n", len(old), len(new)))
|
||
|
|
|
||
|
|
// Record differing chunks
|
||
|
|
chunkStart := -1
|
||
|
|
for i := 0; i < minLen; i++ {
|
||
|
|
if old[i] != new[i] {
|
||
|
|
if chunkStart == -1 {
|
||
|
|
chunkStart = i
|
||
|
|
}
|
||
|
|
} else if chunkStart != -1 {
|
||
|
|
chunk := new[chunkStart:i]
|
||
|
|
buf.Write(encodeChunk(chunkStart, chunk))
|
||
|
|
chunkStart = -1
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if chunkStart != -1 {
|
||
|
|
chunk := new[chunkStart:minLen]
|
||
|
|
buf.Write(encodeChunk(chunkStart, chunk))
|
||
|
|
}
|
||
|
|
|
||
|
|
// Trail: if new is longer, append extra bytes
|
||
|
|
if len(new) > len(old) {
|
||
|
|
buf.Write(encodeChunk(len(old), new[len(old):]))
|
||
|
|
}
|
||
|
|
|
||
|
|
return buf.Bytes()
|
||
|
|
}
|
||
|
|
|
||
|
|
func encodeChunk(offset int, data []byte) []byte {
|
||
|
|
var buf bytes.Buffer
|
||
|
|
buf.WriteString(fmt.Sprintf("@%d+%d:", offset, len(data)))
|
||
|
|
buf.Write(data)
|
||
|
|
buf.WriteByte('\n')
|
||
|
|
return buf.Bytes()
|
||
|
|
}
|