diff --git a/internal/cli/entry_test.go b/internal/cli/entry_test.go index 3312902..6e9f437 100644 --- a/internal/cli/entry_test.go +++ b/internal/cli/entry_test.go @@ -3,6 +3,7 @@ package cli import ( "bytes" "fmt" + "math/rand" "testing" "github.com/spf13/afero" @@ -483,3 +484,74 @@ func TestGenerateAtomicWriteCleansUpOnError(t *testing.T) { tmpExists, _ := afero.Exists(baseFs, "/output.mf.tmp") assert.False(t, tmpExists, "temp file should be cleaned up after failed generation") } + +func TestCheckDetectsManifestCorruption(t *testing.T) { + fs := afero.NewMemMapFs() + rng := rand.New(rand.NewSource(42)) + + // Create many small files with random names to generate a ~1MB manifest + // Each manifest entry is roughly 50-60 bytes, so we need ~20000 files + require.NoError(t, fs.MkdirAll("/testdir", 0755)) + + numFiles := 20000 + for i := 0; i < numFiles; i++ { + // Generate random filename + filename := fmt.Sprintf("/testdir/%08x%08x%08x.dat", rng.Uint32(), rng.Uint32(), rng.Uint32()) + // Small random content + content := make([]byte, 16+rng.Intn(48)) + rng.Read(content) + require.NoError(t, afero.WriteFile(fs, filename, content, 0644)) + } + + // Generate manifest outside of testdir + opts := testOpts([]string{"mfer", "generate", "-q", "-o", "/manifest.mf", "/testdir"}, fs) + exitCode := RunWithOptions(opts) + require.Equal(t, 0, exitCode, "generate should succeed") + + // Read the valid manifest and verify it's approximately 1MB + validManifest, err := afero.ReadFile(fs, "/manifest.mf") + require.NoError(t, err) + require.True(t, len(validManifest) >= 1024*1024, "manifest should be at least 1MB, got %d bytes", len(validManifest)) + t.Logf("manifest size: %d bytes (%d files)", len(validManifest), numFiles) + + // First corruption: truncate the manifest + require.NoError(t, afero.WriteFile(fs, "/manifest.mf", validManifest[:len(validManifest)/2], 0644)) + + // Check should fail with truncated manifest + opts = testOpts([]string{"mfer", "check", "-q", "--base", "/testdir", "/manifest.mf"}, fs) + exitCode = RunWithOptions(opts) + assert.Equal(t, 1, exitCode, "check should fail with truncated manifest") + + // Verify check passes with valid manifest + require.NoError(t, afero.WriteFile(fs, "/manifest.mf", validManifest, 0644)) + opts = testOpts([]string{"mfer", "check", "-q", "--base", "/testdir", "/manifest.mf"}, fs) + exitCode = RunWithOptions(opts) + require.Equal(t, 0, exitCode, "check should pass with valid manifest") + + // Now do 500 random corruption iterations + for i := 0; i < 500; i++ { + // Corrupt: write a random byte at a random offset + corrupted := make([]byte, len(validManifest)) + copy(corrupted, validManifest) + + offset := rng.Intn(len(corrupted)) + originalByte := corrupted[offset] + // Make sure we actually change the byte + newByte := byte(rng.Intn(256)) + for newByte == originalByte { + newByte = byte(rng.Intn(256)) + } + corrupted[offset] = newByte + + require.NoError(t, afero.WriteFile(fs, "/manifest.mf", corrupted, 0644)) + + // Check should fail with corrupted manifest + opts = testOpts([]string{"mfer", "check", "-q", "--base", "/testdir", "/manifest.mf"}, fs) + exitCode = RunWithOptions(opts) + assert.Equal(t, 1, exitCode, "iteration %d: check should fail with corrupted manifest (offset %d, 0x%02x -> 0x%02x)", + i, offset, originalByte, newByte) + + // Restore valid manifest for next iteration + require.NoError(t, afero.WriteFile(fs, "/manifest.mf", validManifest, 0644)) + } +}