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() }