|
| 1 | +// Package diff provides functionality for comparing Docker image contents |
| 2 | +package main |
| 3 | + |
| 4 | +import ( |
| 5 | + "fmt" |
| 6 | + "strings" |
| 7 | + "time" |
| 8 | +) |
| 9 | + |
| 10 | +// Mode specifies what attributes to compare |
| 11 | +type Mode int |
| 12 | + |
| 13 | +const ( |
| 14 | + // CompareAll includes all attributes including modification times |
| 15 | + CompareAll Mode = iota |
| 16 | + // CompareNoTimes excludes modification time comparisons |
| 17 | + CompareNoTimes |
| 18 | +) |
| 19 | + |
| 20 | +// Change represents the type of difference found |
| 21 | +type Change string |
| 22 | + |
| 23 | +const ( |
| 24 | + Added Change = "added" |
| 25 | + Removed Change = "removed" |
| 26 | + Modified Change = "modified" |
| 27 | +) |
| 28 | + |
| 29 | +// FileDiff represents a difference between two versions of a file |
| 30 | +type FileDiff struct { |
| 31 | + Path string `json:"path"` |
| 32 | + Type Change `json:"type"` |
| 33 | + OldFile FileInfo `json:"oldFile,omitempty"` |
| 34 | + NewFile FileInfo `json:"newFile,omitempty"` |
| 35 | + // Details contains human-readable descriptions of the changes |
| 36 | + Details []string `json:"details,omitempty"` |
| 37 | +} |
| 38 | + |
| 39 | +// Summary contains statistical information about the differences |
| 40 | +type Summary struct { |
| 41 | + TotalDifferences int `json:"totalDifferences"` |
| 42 | + AddedFiles int `json:"addedFiles"` |
| 43 | + RemovedFiles int `json:"removedFiles"` |
| 44 | + ModifiedFiles int `json:"modifiedFiles"` |
| 45 | +} |
| 46 | + |
| 47 | +// Result contains the complete diff information |
| 48 | +type Result struct { |
| 49 | + Differences []FileDiff `json:"differences"` |
| 50 | + Summary Summary `json:"summary"` |
| 51 | +} |
| 52 | + |
| 53 | +// FileInfo mirrors the internal inspector's FileInfo structure |
| 54 | +type FileInfo struct { |
| 55 | + Path string `json:"path"` |
| 56 | + Size int64 `json:"size"` |
| 57 | + Mode string `json:"mode"` |
| 58 | + ModTime *time.Time `json:"modTime,omitempty"` |
| 59 | + IsDir bool `json:"isDir"` |
| 60 | + SymlinkTo string `json:"symlinkTo,omitempty"` |
| 61 | + User string `json:"user"` |
| 62 | + Group string `json:"group"` |
| 63 | + MD5 string `json:"md5,omitempty"` |
| 64 | +} |
| 65 | + |
| 66 | +// Compare performs a comparison of two sets of FileInfo records |
| 67 | +func Compare(old, new []FileInfo, mode Mode) (*Result, error) { |
| 68 | + result := &Result{} |
| 69 | + |
| 70 | + // Create maps for faster lookups |
| 71 | + oldFiles := make(map[string]FileInfo) |
| 72 | + newFiles := make(map[string]FileInfo) |
| 73 | + |
| 74 | + // Skip special files and populate maps |
| 75 | + for _, f := range old { |
| 76 | + if !isSpecialFile(f.Path) { |
| 77 | + oldFiles[f.Path] = f |
| 78 | + } |
| 79 | + } |
| 80 | + for _, f := range new { |
| 81 | + if !isSpecialFile(f.Path) { |
| 82 | + newFiles[f.Path] = f |
| 83 | + } |
| 84 | + } |
| 85 | + |
| 86 | + // Find removed files |
| 87 | + for path, oldFile := range oldFiles { |
| 88 | + if _, exists := newFiles[path]; !exists { |
| 89 | + diff := FileDiff{ |
| 90 | + Path: path, |
| 91 | + Type: Removed, |
| 92 | + OldFile: oldFile, |
| 93 | + } |
| 94 | + result.Differences = append(result.Differences, diff) |
| 95 | + result.Summary.RemovedFiles++ |
| 96 | + } |
| 97 | + } |
| 98 | + |
| 99 | + // Find added and modified files |
| 100 | + for path, newFile := range newFiles { |
| 101 | + oldFile, exists := oldFiles[path] |
| 102 | + if !exists { |
| 103 | + diff := FileDiff{ |
| 104 | + Path: path, |
| 105 | + Type: Added, |
| 106 | + NewFile: newFile, |
| 107 | + } |
| 108 | + result.Differences = append(result.Differences, diff) |
| 109 | + result.Summary.AddedFiles++ |
| 110 | + continue |
| 111 | + } |
| 112 | + |
| 113 | + // Check for modifications |
| 114 | + if differences := compareFiles(oldFile, newFile, mode); len(differences) > 0 { |
| 115 | + diff := FileDiff{ |
| 116 | + Path: path, |
| 117 | + Type: Modified, |
| 118 | + OldFile: oldFile, |
| 119 | + NewFile: newFile, |
| 120 | + Details: differences, |
| 121 | + } |
| 122 | + result.Differences = append(result.Differences, diff) |
| 123 | + result.Summary.ModifiedFiles++ |
| 124 | + } |
| 125 | + } |
| 126 | + |
| 127 | + result.Summary.TotalDifferences = result.Summary.AddedFiles + |
| 128 | + result.Summary.RemovedFiles + |
| 129 | + result.Summary.ModifiedFiles |
| 130 | + |
| 131 | + return result, nil |
| 132 | +} |
| 133 | + |
| 134 | +// compareFiles returns a list of differences between two files |
| 135 | +func compareFiles(old, new FileInfo, mode Mode) []string { |
| 136 | + var differences []string |
| 137 | + |
| 138 | + // Compare basic attributes |
| 139 | + if old.Size != new.Size { |
| 140 | + differences = append(differences, |
| 141 | + fmt.Sprintf("size changed: %d -> %d", old.Size, new.Size)) |
| 142 | + } |
| 143 | + if old.Mode != new.Mode { |
| 144 | + differences = append(differences, |
| 145 | + fmt.Sprintf("permissions changed: %s -> %s", old.Mode, new.Mode)) |
| 146 | + } |
| 147 | + if old.User != new.User || old.Group != new.Group { |
| 148 | + differences = append(differences, |
| 149 | + fmt.Sprintf("ownership changed: %s:%s -> %s:%s", |
| 150 | + old.User, old.Group, new.User, new.Group)) |
| 151 | + } |
| 152 | + |
| 153 | + // Compare modification times if requested |
| 154 | + if mode == CompareAll && old.ModTime != nil && new.ModTime != nil { |
| 155 | + if !old.ModTime.Equal(*new.ModTime) { |
| 156 | + differences = append(differences, |
| 157 | + fmt.Sprintf("modification time changed: %s -> %s", |
| 158 | + old.ModTime.Format(time.RFC3339), |
| 159 | + new.ModTime.Format(time.RFC3339))) |
| 160 | + } |
| 161 | + } |
| 162 | + |
| 163 | + // Compare MD5 if available |
| 164 | + if old.MD5 != "" && new.MD5 != "" && old.MD5 != new.MD5 { |
| 165 | + differences = append(differences, "content changed (different MD5)") |
| 166 | + } |
| 167 | + |
| 168 | + return differences |
| 169 | +} |
| 170 | + |
| 171 | +// isSpecialFile returns true for files we want to ignore |
| 172 | +func isSpecialFile(path string) bool { |
| 173 | + return strings.HasPrefix(path, "/proc/") || |
| 174 | + strings.HasPrefix(path, "/sys/") || |
| 175 | + strings.HasPrefix(path, "/dev/") || |
| 176 | + path == "/etc/resolv.conf" || |
| 177 | + path == "/etc/hostname" || |
| 178 | + path == "/etc/hosts" |
| 179 | +} |
0 commit comments