Files

231 lines
5.5 KiB
Go
Raw Permalink Normal View History

2026-07-07 10:29:28 +08:00
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()
}