Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 16 additions & 12 deletions cmd/os-image-composer/compare_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ import (

// Output format command flags
var (
prettyDiffJSON bool = true // Pretty-print JSON output
outFormat string // "text" | "json"
outMode string = "" // "full" | "diff" | "summary"
prettyDiffJSON bool = true // Pretty-print JSON output
outFormat string // "text" | "json"
outMode string = "" // "full" | "diff" | "summary"
hashImages bool = false // Skip hashing during inspection
)

// createCompareCommand creates the compare subcommand
Expand All @@ -39,6 +40,8 @@ func createCompareCommand() *cobra.Command {
"Output format: text or json")
compareCmd.Flags().StringVar(&outMode, "mode", "",
"Output mode: full, diff, or summary (default: diff for text, full for json)")
compareCmd.Flags().BoolVar(&hashImages, "hash-images", false,
"Compute SHA256 hash of images during inspection (slower but enables binary identity verification")
return compareCmd
}

Expand All @@ -62,9 +65,9 @@ func executeCompare(cmd *cobra.Command, args []string) error {
log := logger.Logger()
imageFile1 := args[0]
imageFile2 := args[1]
log.Infof("Comparing image files: %s and %s", imageFile1, imageFile2)
log.Infof("Comparing image files: (%s) & (%s)", imageFile1, imageFile2)

inspector := newInspector()
inspector := newInspector(hashImages)

image1, err1 := inspector.Inspect(imageFile1)
if err1 != nil {
Expand All @@ -87,16 +90,17 @@ func executeCompare(cmd *cobra.Command, args []string) error {
payload = &compareResult
case "diff":
payload = struct {
Equal bool `json:"equal"`
Diff imageinspect.ImageDiff `json:"diff"`
}{Equal: compareResult.Equal, Diff: compareResult.Diff}
// Equal bool `json:"equal"`
EqualityClass string `json:"equalityClass"`
Diff imageinspect.ImageDiff `json:"diff"`
}{EqualityClass: string(compareResult.Equality.Class), Diff: compareResult.Diff}
case "summary":
payload = struct {
Equal bool `json:"equal"`
Summary imageinspect.CompareSummary `json:"summary"`
}{Equal: compareResult.Equal, Summary: compareResult.Summary}
EqualityClass string `json:"equalityClass"`
Summary imageinspect.CompareSummary `json:"summary"`
}{EqualityClass: string(compareResult.Equality.Class), Summary: compareResult.Summary}
default:
return fmt.Errorf("invalid --mode %q (expected diff|summary|full)", mode)
return fmt.Errorf("invalid --mode or --format %q (expected --mode=diff|summary|full) and --format=text|json", mode)
}
return writeCompareResult(cmd, payload, prettyDiffJSON)

Expand Down
30 changes: 13 additions & 17 deletions cmd/os-image-composer/compare_cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,6 @@ func decodeJSON(t *testing.T, s string, v any) {
}
}

// ---- Tests ----

func TestResolveDefaults(t *testing.T) {
t.Run("json defaults to full when mode empty", func(t *testing.T) {
format, mode := resolveDefaults("json", "")
Expand All @@ -87,8 +85,6 @@ func TestResolveDefaults(t *testing.T) {
}

func TestCompareCommand_JSONModes_PrettyAndCompact(t *testing.T) {
// IMPORTANT: these tests assume newInspector is a package-level var func you can override.
// If it’s a normal function, change code to allow injection or adapt these tests.

origNewInspector := newInspector
t.Cleanup(func() { newInspector = origNewInspector })
Expand All @@ -100,9 +96,9 @@ func TestCompareCommand_JSONModes_PrettyAndCompact(t *testing.T) {
},
errByPath: map[string]error{},
}
newInspector = func() inspector { return fi }
newInspector = func(hash bool) inspector { return fi }

// Make a command instance to provide OutOrStdout/flags context (executeCompare uses cmd for output).
// Make a command instance to provide OutOrStdout/flags context.
cmd := &cobra.Command{}
cmd.SetArgs([]string{})

Expand All @@ -122,7 +118,7 @@ func TestCompareCommand_JSONModes_PrettyAndCompact(t *testing.T) {
// Validate it looks like ImageCompareResult (at least top-level fields).
var got struct {
SchemaVersion string `json:"schemaVersion"`
Equal bool `json:"equal"`
Equality json.RawMessage `json:"equality"`
From json.RawMessage `json:"from"`
To json.RawMessage `json:"to"`
Summary json.RawMessage `json:"summary"`
Expand All @@ -149,8 +145,8 @@ func TestCompareCommand_JSONModes_PrettyAndCompact(t *testing.T) {
}

var got struct {
Equal bool `json:"equal"`
Diff imageinspect.ImageDiff `json:"diff"`
EqualityClass imageinspect.EqualityClass `json:"equalityClass"`
Diff imageinspect.ImageDiff `json:"diff"`
}
decodeJSON(t, s, &got)
})
Expand All @@ -169,8 +165,8 @@ func TestCompareCommand_JSONModes_PrettyAndCompact(t *testing.T) {
}

var got struct {
Equal bool `json:"equal"`
Summary imageinspect.CompareSummary `json:"summary"`
EqualityClass imageinspect.EqualityClass `json:"equalityClass"`
Summary imageinspect.CompareSummary `json:"summary"`
}
decodeJSON(t, s, &got)
})
Expand All @@ -191,7 +187,7 @@ func TestCompareCommand_TextOutput(t *testing.T) {
"b.raw": img2,
},
}
newInspector = func() inspector { return fi }
newInspector = func(hash bool) inspector { return fi }

cmd := &cobra.Command{}
outFormat = "text"
Expand All @@ -203,8 +199,8 @@ func TestCompareCommand_TextOutput(t *testing.T) {
}

// Basic structure checks (don’t overfit exact wording)
if !strings.Contains(s, "Equal:") {
t.Fatalf("expected 'Equal:' header, got:\n%s", s)
if !strings.Contains(s, "Equality:") {
t.Fatalf("expected 'Equality:' header, got:\n%s", s)
}
if !strings.Contains(s, "Partition table:") {
t.Fatalf("expected partition table section, got:\n%s", s)
Expand All @@ -226,7 +222,7 @@ func TestCompareCommand_InspectorError(t *testing.T) {
"b.raw": errors.New("boom"),
},
}
newInspector = func() inspector { return fi }
newInspector = func(hash bool) inspector { return fi }

cmd := &cobra.Command{}
outFormat = "json"
Expand All @@ -249,7 +245,7 @@ func TestCompareCommand_InvalidModeErrors(t *testing.T) {
outFormat, outMode = origOutFormat, origOutMode
})

newInspector = func() inspector {
newInspector = func(hash bool) inspector {
return &fakeCompareInspector{imgByPath: map[string]*imageinspect.ImageSummary{
"a.raw": minimalImage("a.raw", 1),
"b.raw": minimalImage("b.raw", 1),
Expand All @@ -274,7 +270,7 @@ func TestCompareCommand_InvalidFormatErrors(t *testing.T) {
outFormat, outMode = origOutFormat, origOutMode
})

newInspector = func() inspector {
newInspector = func(hash bool) inspector {
return &fakeCompareInspector{imgByPath: map[string]*imageinspect.ImageSummary{
"a.raw": minimalImage("a.raw", 1),
"b.raw": minimalImage("b.raw", 1),
Expand Down
6 changes: 3 additions & 3 deletions cmd/os-image-composer/inspect_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ type inspector interface {
}

// Allow tests to inject a fake inspector.
var newInspector = func() inspector {
return imageinspect.NewDiskfsInspector() // returns *DiskfsInspector which satisfies inspector
var newInspector = func(hash bool) inspector {
return imageinspect.NewDiskfsInspector(hash) // returns *DiskfsInspector which satisfies inspector
}

// Output format command flags
Expand Down Expand Up @@ -64,7 +64,7 @@ func executeInspect(cmd *cobra.Command, args []string) error {
imageFile := args[0]
log.Infof("Inspecting image file: %s", imageFile)

inspector := newInspector()
inspector := newInspector(false)

inspectionResults, err := inspector.Inspect(imageFile)
if err != nil {
Expand Down
14 changes: 7 additions & 7 deletions cmd/os-image-composer/inspect_cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ import (
// resetInspectFlags resets inspect flags to defaults.
func resetInspectFlags() {
outputFormat = "text"
newInspector = func() inspector {
return imageinspect.NewDiskfsInspector()
newInspector = func(hash bool) inspector {
return imageinspect.NewDiskfsInspector(hash)
}
}

Expand Down Expand Up @@ -161,7 +161,7 @@ func TestExecuteInspect_DirectCall(t *testing.T) {

oldNew := newInspector
t.Cleanup(func() { newInspector = oldNew })
newInspector = func() inspector {
newInspector = func(hash bool) inspector {
return &fakeInspector{
summary: &imageinspect.ImageSummary{File: "fake.img", SizeBytes: 123},
}
Expand Down Expand Up @@ -190,7 +190,7 @@ func TestExecuteInspect_DirectCall(t *testing.T) {

oldNew := newInspector
t.Cleanup(func() { newInspector = oldNew })
newInspector = func() inspector {
newInspector = func(hash bool) inspector {
return &fakeInspector{err: errors.New("boom")}
}

Expand Down Expand Up @@ -224,7 +224,7 @@ func TestInspectCommand_OutputFormats_WithFakeInspector(t *testing.T) {
},
}

newInspector = func() inspector {
newInspector = func(hash bool) inspector {
return &fakeInspector{summary: fake}
}
defer resetInspectFlags()
Expand All @@ -243,7 +243,7 @@ func TestInspectCommand_OutputFormats_WithFakeInspector(t *testing.T) {
t.Run("JSON", func(t *testing.T) {
resetInspectFlags()

newInspector = func() inspector {
newInspector = func(hash bool) inspector {
return &fakeInspector{
summary: &imageinspect.ImageSummary{
File: "fake.img",
Expand Down Expand Up @@ -286,7 +286,7 @@ func TestInspectCommand_OutputFormats_WithFakeInspector(t *testing.T) {
})

t.Run("YAML", func(t *testing.T) {
newInspector = func() inspector {
newInspector = func(hash bool) inspector {
return &fakeInspector{
summary: &imageinspect.ImageSummary{
File: "fake.img",
Expand Down
4 changes: 4 additions & 0 deletions docs/architecture/os-image-composer-cli-specification.md
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,7 @@ os-image-composer compare [flags] IMAGE_FILE1 IMAGE_FILE2
| `--format STRING` | Output format: `text` or `json` (default: `text`) |
| `--mode STRING` | Compare mode: `diff` (partition/FS changes), `summary` (high-level counts), or `full` (complete image metadata). Default: `diff` for text, `full` for JSON |
| `--pretty` | Pretty-print JSON output (only for `--format=json`; default: `false`) |
| `--hash-images` | Perform image hashing for verifying binary identical image (default `false`) |

**Description:**

Expand Down Expand Up @@ -341,6 +342,9 @@ os-image-composer compare --format=json --mode=full --pretty image-v1.raw image-

# Compact JSON diff suitable for CI/CD automation
os-image-composer compare --format=json --mode=diff image-v1.raw image-v2.raw

# Perform comparison with image hashing enabled with details text diff
os-image-composer compare --hash-images=true image-v1.raw image-v2.raw
```

### Cache Command
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ func peSignatureInfo(f *pe.File) (signed bool, sigSize int, note string) {

// classifyBootloaderKind classifies the bootloader kind based on path and sections.
// It intentionally avoids content-string heuristics for stability.
// For BOOTX64.EFI copies/aliases, rely on SHA-inheritance post-pass.
// For `EFI/BOOT/BOOTX64.EFI` copies/aliases, rely on SHA-inheritance post-pass.
func classifyBootloaderKind(p string, sections []string) BootloaderKind {
lp := strings.ToLower(p)

Expand Down Expand Up @@ -155,7 +155,7 @@ func hasSection(secs []string, want string) bool {

// inheritBootloaderKindBySHA assigns a kind to "unknown" EFI binaries when they
// are byte-identical to another EFI binary already classified as a known kind.
// This reliably handles fallback paths like EFI/BOOT/BOOTX64.EFI.
// This reliably handles fallback paths like `EFI/BOOT/BOOTX64.EFI`.
func inheritBootloaderKindBySHA(evs []EFIBinaryEvidence) {
known := make(map[string]BootloaderKind) // sha256 -> kind

Expand Down
Loading
Loading