From fe4d4005e63c3abb955d886d1b099adb44c1e7be Mon Sep 17 00:00:00 2001 From: Brian Ketelsen Date: Mon, 12 Jan 2026 20:51:25 -0500 Subject: [PATCH 1/3] fix: dedup shasums, add transfer.conf Signed-off-by: Brian Ketelsen --- README.md | 48 ++++++++---- internal/cli/generate.go | 4 +- internal/generator/sysext/generator.go | 81 ++++++++++++++++++--- internal/generator/sysext/generator_test.go | 60 ++++++++++++++- 4 files changed, 162 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 1535274..1328454 100644 --- a/README.md +++ b/README.md @@ -437,13 +437,36 @@ repo/ └── ext/ └── docker/ ├── SHA256SUMS # Checksum file for systemd-sysupdate + ├── docker.transfer # systemd-sysupdate transfer configuration ├── docker_24.0.5_x86-64.raw.zst └── docker_25.0.0_x86-64.raw.zst ``` +**Note:** The `--base-url` flag is required when generating sysext repositories. This is used to generate the `.transfer` configuration files with the correct source URL. + +```bash +repogen generate \ + --input-dir ./extensions \ + --output-dir ./repo \ + --base-url https://example.com/repo +``` + **Using with systemd-sysupdate:** -Create a transfer configuration file at `/etc/sysupdate.d/50-docker.conf`: +Repogen generates a `.transfer` file for each extension that can be copied to `/etc/sysupdate.d/`: + +```bash +# Copy the generated transfer file +sudo cp repo/ext/docker/docker.transfer /etc/sysupdate.d/50-docker.conf + +# Check for updates +systemd-sysupdate list + +# Download and apply updates +systemd-sysupdate update +``` + +The generated transfer file looks like: ```ini [Transfer] @@ -451,24 +474,19 @@ Verify=false [Source] Type=url-file -Path=http://your-server.com/repo/ext/docker/ -MatchPattern=docker_@v_@a.raw.zst +Path=https://example.com/repo/ext/docker/ +MatchPattern=docker_@v_@a.raw.zst \ + docker_@v_@a.raw.xz \ + docker_@v_@a.raw.gz \ + docker_@v_@a.raw [Target] Type=regular-file Path=/var/lib/extensions/ -MatchPattern=docker_@v_@a.raw \ - docker_@v_@a.raw.zst -``` - -Then run: - -```bash -# Check for updates -systemd-sysupdate list - -# Download and apply updates -systemd-sysupdate update +MatchPattern=docker_@v_@a.raw.zst \ + docker_@v_@a.raw.xz \ + docker_@v_@a.raw.gz \ + docker_@v_@a.raw ``` ## GPG Key Setup diff --git a/internal/cli/generate.go b/internal/cli/generate.go index 6fd4368..11a5a19 100644 --- a/internal/cli/generate.go +++ b/internal/cli/generate.go @@ -67,7 +67,7 @@ structures with appropriate metadata files and signatures.`, cmd.Flags().StringSliceVar(&config.Arches, "arch", []string{"amd64"}, "Architectures to support") // Type-specific options - cmd.Flags().StringVar(&config.BaseURL, "base-url", "", "Base URL for Homebrew bottles and RPM .repo files") + cmd.Flags().StringVar(&config.BaseURL, "base-url", "", "Base URL for Homebrew bottles, RPM .repo files, and sysext transfer files (required for sysext)") cmd.Flags().StringVar(&config.GPGKeyURL, "gpg-key-url", "", "GPG key URL for RPM .repo files (supports $releasever/$basearch variables)") cmd.Flags().StringVar(&config.DistroVariant, "distro", "fedora", "Distribution variant for RPM repos (fedora, centos, rhel)") cmd.Flags().StringVar(&config.Version, "version", "", "Release version for RPM repos (e.g., 40 for Fedora 40). Auto-detected from RPM metadata if not provided") @@ -222,7 +222,7 @@ func runGeneration(ctx context.Context, config *models.RepositoryConfig) error { generators[scanner.TypeApk] = apk.NewGenerator(rsaSigner, config.RSAKeyName) generators[scanner.TypePacman] = pacman.NewGenerator(gpgSigner) generators[scanner.TypeHomebrewBottle] = homebrew.NewGenerator(config.BaseURL) - generators[scanner.TypeSysext] = sysext.NewGenerator() + generators[scanner.TypeSysext] = sysext.NewGenerator(config.BaseURL) for pkgType, newPackages := range packagesByType { gen, ok := generators[pkgType] diff --git a/internal/generator/sysext/generator.go b/internal/generator/sysext/generator.go index 5bbb43d..2597e28 100644 --- a/internal/generator/sysext/generator.go +++ b/internal/generator/sysext/generator.go @@ -20,14 +20,19 @@ import ( // The generated repository structure is: // // /ext// -// SHA256SUMS # Standard checksum file for systemd-sysupdate -// .raw # Extension files -// .raw.zst # Compressed variants -type Generator struct{} +// SHA256SUMS # Standard checksum file for systemd-sysupdate +// .transfer # systemd-sysupdate transfer configuration +// .raw # Extension files +// .raw.zst # Compressed variants +type Generator struct { + baseURL string +} // NewGenerator creates a new systemd-sysext generator -func NewGenerator() generator.Generator { - return &Generator{} +func NewGenerator(baseURL string) generator.Generator { + return &Generator{ + baseURL: baseURL, + } } // Generate creates a systemd-sysext repository structure. @@ -67,7 +72,9 @@ func (g *Generator) generateForExtension(ctx context.Context, config *models.Rep } // Copy files and build SHA256SUMS content - var sha256Lines []string + // Use a map to deduplicate entries by filename (in case existing metadata + // and new packages overlap) + sha256Entries := make(map[string]string) // filename -> sha256 for i := range packages { pkg := &packages[i] @@ -97,9 +104,15 @@ func (g *Generator) generateForExtension(ctx context.Context, config *models.Rep logrus.Debugf("Skipping copy for extension: %s", pkg.Name) } - // Add line to SHA256SUMS (standard format: " ") - // Note: two spaces between hash and filename per shasum convention - sha256Lines = append(sha256Lines, fmt.Sprintf("%s %s", pkg.SHA256Sum, basename)) + // Add entry to map (deduplicates by filename) + sha256Entries[basename] = pkg.SHA256Sum + } + + // Build SHA256SUMS content from deduplicated entries + var sha256Lines []string + for filename, hash := range sha256Entries { + // Format: " " (two spaces per shasum convention) + sha256Lines = append(sha256Lines, fmt.Sprintf("%s %s", hash, filename)) } // Write SHA256SUMS file @@ -109,12 +122,58 @@ func (g *Generator) generateForExtension(ctx context.Context, config *models.Rep return fmt.Errorf("failed to write SHA256SUMS: %w", err) } - logrus.Infof("Generated SHA256SUMS for %s (%d files)", extName, len(packages)) + // Generate systemd-sysupdate transfer configuration file + if err := g.generateTransferFile(extDir, extName); err != nil { + return fmt.Errorf("failed to write transfer file: %w", err) + } + + logrus.Infof("Generated SHA256SUMS for %s (%d files)", extName, len(sha256Entries)) + return nil +} + +// generateTransferFile creates a systemd-sysupdate transfer configuration file +// for the extension. This file can be placed in /etc/sysupdate.d/ to enable +// automatic updates via systemd-sysupdate. +func (g *Generator) generateTransferFile(extDir, extName string) error { + // Build the source URL path + sourceURL := strings.TrimSuffix(g.baseURL, "/") + "/ext/" + extName + "/" + + // Generate transfer file content + // The @v and @a are systemd-sysupdate version and architecture placeholders + transferContent := fmt.Sprintf(`[Transfer] +Verify=false + +[Source] +Type=url-file +Path=%s +MatchPattern=%s_@v_@a.raw.zst \ + %s_@v_@a.raw.xz \ + %s_@v_@a.raw.gz \ + %s_@v_@a.raw + +[Target] +Type=regular-file +Path=/var/lib/extensions.d/ +MatchPattern=%s_@v_@a.raw.zst \ + %s_@v_@a.raw.xz \ + %s_@v_@a.raw.gz \ + %s_@v_@a.raw +`, sourceURL, extName, extName, extName, extName, extName, extName, extName, extName) + + transferPath := filepath.Join(extDir, extName+".transfer") + if err := utils.WriteFile(transferPath, []byte(transferContent), 0644); err != nil { + return err + } + + logrus.Debugf("Generated transfer file: %s", transferPath) return nil } // ValidatePackages checks if packages are valid for this generator func (g *Generator) ValidatePackages(packages []models.Package) error { + if g.baseURL == "" { + return fmt.Errorf("--base-url is required for sysext repository generation") + } for _, pkg := range packages { if pkg.Name == "" { return fmt.Errorf("sysext package missing name: %s", pkg.Filename) diff --git a/internal/generator/sysext/generator_test.go b/internal/generator/sysext/generator_test.go index 209fdad..cf4b9a4 100644 --- a/internal/generator/sysext/generator_test.go +++ b/internal/generator/sysext/generator_test.go @@ -157,7 +157,7 @@ func TestGeneratorGenerate(t *testing.T) { os.WriteFile(ext2, []byte("sysext content v2 compressed"), 0644) os.WriteFile(ext3, []byte("other ext content"), 0644) - gen := NewGenerator() + gen := NewGenerator("https://example.com/repo") config := &models.RepositoryConfig{ OutputDir: outputDir, } @@ -228,6 +228,43 @@ func TestGeneratorGenerate(t *testing.T) { t.Errorf("SHA256SUMS should use two spaces between hash and filename: %q", line) } } + + // Verify transfer files exist + myextTransfer := filepath.Join(myextDir, "myext.transfer") + otherTransfer := filepath.Join(otherDir, "other.transfer") + + if _, err := os.Stat(myextTransfer); os.IsNotExist(err) { + t.Errorf("myext.transfer not created") + } + if _, err := os.Stat(otherTransfer); os.IsNotExist(err) { + t.Errorf("other.transfer not created") + } + + // Verify transfer file content + transferContent, err := os.ReadFile(myextTransfer) + if err != nil { + t.Fatalf("Failed to read myext.transfer: %v", err) + } + + transferStr := string(transferContent) + if !strings.Contains(transferStr, "[Transfer]") { + t.Error("Transfer file missing [Transfer] section") + } + if !strings.Contains(transferStr, "[Source]") { + t.Error("Transfer file missing [Source] section") + } + if !strings.Contains(transferStr, "[Target]") { + t.Error("Transfer file missing [Target] section") + } + if !strings.Contains(transferStr, "https://example.com/repo/ext/myext/") { + t.Errorf("Transfer file missing correct source URL, got: %s", transferStr) + } + if !strings.Contains(transferStr, "myext_@v_@a.raw") { + t.Errorf("Transfer file missing pattern with version/arch placeholders, got: %s", transferStr) + } + if !strings.Contains(transferStr, "Path=/var/lib/extensions.d/") { + t.Errorf("Transfer file missing correct target path, got: %s", transferStr) + } } func TestIncrementalMode(t *testing.T) { @@ -241,7 +278,7 @@ func TestIncrementalMode(t *testing.T) { outputDir := filepath.Join(tmpDir, "output") os.MkdirAll(inputDir, 0755) - gen := NewGenerator() + gen := NewGenerator("https://example.com/repo") config := &models.RepositoryConfig{ OutputDir: outputDir, } @@ -312,7 +349,7 @@ func TestIncrementalMode(t *testing.T) { } func TestValidatePackages(t *testing.T) { - gen := NewGenerator() + gen := NewGenerator("https://example.com/repo") tests := []struct { name string @@ -352,3 +389,20 @@ func TestValidatePackages(t *testing.T) { }) } } + +func TestValidatePackagesMissingBaseURL(t *testing.T) { + gen := NewGenerator("") // Empty base URL + + packages := []models.Package{ + {Name: "ext1", Version: "1.0", Filename: "/path/to/ext1_1.0_x86-64.raw"}, + } + + err := gen.ValidatePackages(packages) + if err == nil { + t.Error("ValidatePackages() should error when base URL is missing") + } + + if !strings.Contains(err.Error(), "--base-url") { + t.Errorf("Error should mention --base-url, got: %v", err) + } +} From a71991e1df3c5bede1a60b584d53fd8383c90b33 Mon Sep 17 00:00:00 2001 From: Brian Ketelsen Date: Tue, 13 Jan 2026 09:10:15 -0500 Subject: [PATCH 2/3] fix: transfer file Signed-off-by: Brian Ketelsen --- internal/generator/sysext/generator.go | 3 ++- internal/generator/sysext/generator_test.go | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/internal/generator/sysext/generator.go b/internal/generator/sysext/generator.go index 2597e28..97d934a 100644 --- a/internal/generator/sysext/generator.go +++ b/internal/generator/sysext/generator.go @@ -158,7 +158,8 @@ MatchPattern=%s_@v_@a.raw.zst \ %s_@v_@a.raw.xz \ %s_@v_@a.raw.gz \ %s_@v_@a.raw -`, sourceURL, extName, extName, extName, extName, extName, extName, extName, extName) +CurrentSymlink=%s.raw +`, sourceURL, extName, extName, extName, extName, extName, extName, extName, extName, extName) transferPath := filepath.Join(extDir, extName+".transfer") if err := utils.WriteFile(transferPath, []byte(transferContent), 0644); err != nil { diff --git a/internal/generator/sysext/generator_test.go b/internal/generator/sysext/generator_test.go index cf4b9a4..2668916 100644 --- a/internal/generator/sysext/generator_test.go +++ b/internal/generator/sysext/generator_test.go @@ -265,6 +265,9 @@ func TestGeneratorGenerate(t *testing.T) { if !strings.Contains(transferStr, "Path=/var/lib/extensions.d/") { t.Errorf("Transfer file missing correct target path, got: %s", transferStr) } + if !strings.Contains(transferStr, "CurrentSymlink=myext.raw") { + t.Errorf("Transfer file missing CurrentSymlink, got: %s", transferStr) + } } func TestIncrementalMode(t *testing.T) { From 517049bdae1472773f8911af370d95ea27c0b362 Mon Sep 17 00:00:00 2001 From: Brian Ketelsen Date: Tue, 13 Jan 2026 10:29:11 -0500 Subject: [PATCH 3/3] fix: lint and static checks. Add index for extensions Signed-off-by: Brian Ketelsen --- .github/copilot-instructions.md | 143 ++++++++++++++++++++ internal/generator/apk/generator_test.go | 20 ++- internal/generator/apk/parser.go | 8 +- internal/generator/deb/generator_test.go | 32 +++-- internal/generator/deb/parser.go | 14 +- internal/generator/homebrew/generator.go | 4 +- internal/generator/homebrew/parser.go | 4 +- internal/generator/pacman/generator_test.go | 20 +-- internal/generator/pacman/parser.go | 12 +- internal/generator/rpm/generator_test.go | 26 ++-- internal/generator/rpm/parser.go | 6 +- internal/generator/sysext/generator.go | 34 ++++- internal/generator/sysext/generator_test.go | 125 +++++++++++++++-- internal/scanner/detector.go | 2 +- internal/signer/gpg.go | 68 +--------- internal/signer/rsa.go | 2 + internal/utils/checksum.go | 2 +- internal/utils/compression.go | 2 +- internal/utils/fileops.go | 4 +- test/integration_test.go | 73 +--------- 20 files changed, 396 insertions(+), 205 deletions(-) create mode 100644 .github/copilot-instructions.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..89223f8 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,143 @@ +# Copilot Instructions for Repogen + +## Project Overview + +Repogen is a CLI tool written in Go that generates static repository structures for multiple package managers. It scans directories for packages, generates appropriate metadata files, and signs repositories with GPG/RSA keys. + +### Supported Package Types + +- **Debian/APT** (.deb packages) +- **Yum/RPM** (.rpm packages) +- **Alpine/APK** (.apk packages) +- **Arch Linux/Pacman** (.pkg.tar.zst, .pkg.tar.xz, .pkg.tar.gz) +- **Homebrew** (bottle files) +- **systemd-sysext** (.raw, .raw.zst, .raw.xz, .raw.gz) + +### Project Structure + +``` +cmd/repogen/ # CLI entry point +internal/ + cli/ # Command-line interface (Cobra commands) + generator/ # Repository generators for each package type + apk/ # Alpine APK repository generator + deb/ # Debian APT repository generator + homebrew/ # Homebrew bottle repository generator + pacman/ # Arch Linux Pacman repository generator + rpm/ # RPM/Yum repository generator + sysext/ # systemd-sysext repository generator + models/ # Data models (Package, RepositoryConfig, errors) + scanner/ # Package detection and file scanning + signer/ # GPG and RSA signing utilities + utils/ # Shared utilities (checksums, compression, file ops) +test/ # Integration tests and fixtures +``` + +## Go Development Best Practices + +### Code Style + +- Follow standard Go conventions and idioms +- Use meaningful variable and function names +- Keep functions focused and small +- Add comments for exported functions and types (godoc style) +- Use `context.Context` for cancellation where appropriate +- Handle errors explicitly; never ignore errors +- Use structured logging with logrus + +### Error Handling + +- Return errors rather than panicking +- Wrap errors with context using `fmt.Errorf("context: %w", err)` +- Define custom error types in `models/errors.go` when appropriate + +### Testing + +- Write unit tests for new functionality +- Use table-driven tests where appropriate +- Test both success and error cases +- Place tests in the same package as the code being tested (`*_test.go`) + +## After Every Code Change + +After making any code changes, you MUST run: + +```bash +make build +``` + +Then format the code using: + +```bash +make fmt +``` + +Then run the linter: + +```bash +make lint +``` + +Fix any linting errors before considering the task complete. Common linting issues include: + +- Unused imports or variables +- Missing error checks +- Ineffective assignments +- Formatting issues + +## Documentation Requirements + +When making changes, update relevant documentation: + +1. **README.md**: Update if you add/modify: + + - New package type support + - New CLI flags or commands + - New features or workflows + - Repository structure changes + +2. **Code Comments**: Add/update godoc comments for: + + - Exported functions and types + - Complex logic that needs explanation + - Configuration options + +3. **Inline Comments**: Add brief comments for: + - Non-obvious code decisions + - Workarounds or edge cases + +## Adding New Package Type Support + +When adding support for a new package type: + +1. Add detection logic in `internal/scanner/detector.go` +2. Add the new `PackageType` constant in `internal/scanner/scanner.go` +3. Create a new generator package under `internal/generator//` +4. Implement the `generator.Generator` interface: + - `Generate(ctx, config, packages) error` + - `ValidatePackages(packages) error` + - `GetSupportedType() scanner.PackageType` + - `ParseExistingMetadata(config) ([]Package, error)` +5. Register the generator in `internal/cli/generate.go` +6. Add package identity support in `internal/utils/package_identity.go` +7. Write comprehensive tests +8. Update README.md with new package type documentation + +## Common Make Targets + +- `make build` - Build the repogen binary +- `make test-unit` - Run unit tests (fast) +- `make test` - Run all tests including integration +- `make fmt` - Format code with go fmt +- `make lint` - Run golangci-lint +- `make install` - Install to /usr/local/bin +- `make clean` - Clean build artifacts + +## Checklist Before Completing a Task + +- [ ] Code compiles (`make build`) +- [ ] Code is formatted (`make fmt`) +- [ ] Linter passes (`make lint`) +- [ ] Tests pass (`make test-unit`) +- [ ] Documentation updated if needed +- [ ] New features have tests diff --git a/internal/generator/apk/generator_test.go b/internal/generator/apk/generator_test.go index 06f5731..724ebcb 100644 --- a/internal/generator/apk/generator_test.go +++ b/internal/generator/apk/generator_test.go @@ -23,12 +23,16 @@ func TestIncrementalModeCopiesNewPackages(t *testing.T) { if err != nil { t.Fatalf("Failed to create temp dir: %v", err) } - defer os.RemoveAll(tmpDir) + defer func() { _ = os.RemoveAll(tmpDir) }() inputDir := filepath.Join(tmpDir, "input") outputDir := filepath.Join(tmpDir, "output") - os.MkdirAll(inputDir, 0755) - os.MkdirAll(outputDir, 0755) + if err := os.MkdirAll(inputDir, 0755); err != nil { + t.Fatalf("Failed to create input dir: %v", err) + } + if err := os.MkdirAll(outputDir, 0755); err != nil { + t.Fatalf("Failed to create output dir: %v", err) + } gen := NewGenerator(nil, "") config := &models.RepositoryConfig{ @@ -38,7 +42,9 @@ func TestIncrementalModeCopiesNewPackages(t *testing.T) { // Step 1: Create initial repo with package A initialPkg := filepath.Join(inputDir, "pkga-1.0-r1.apk") - os.WriteFile(initialPkg, []byte("fake apk package A"), 0644) + if err := os.WriteFile(initialPkg, []byte("fake apk package A"), 0644); err != nil { + t.Fatalf("Failed to write initial package: %v", err) + } packagesA := []models.Package{ { @@ -70,7 +76,7 @@ func TestIncrementalModeCopiesNewPackages(t *testing.T) { files, _ := os.ReadDir(archDir) for _, file := range files { if strings.HasSuffix(file.Name(), ".apk") { - os.Remove(filepath.Join(archDir, file.Name())) + _ = os.Remove(filepath.Join(archDir, file.Name())) } } @@ -81,7 +87,9 @@ func TestIncrementalModeCopiesNewPackages(t *testing.T) { // Step 3: Create new package B newPkg := filepath.Join(inputDir, "pkgb-1.0-r1.apk") - os.WriteFile(newPkg, []byte("fake apk package B"), 0644) + if err := os.WriteFile(newPkg, []byte("fake apk package B"), 0644); err != nil { + t.Fatalf("Failed to write new package: %v", err) + } // Step 4: Parse existing metadata (simulating incremental mode) existingPackages, err := gen.ParseExistingMetadata(config) diff --git a/internal/generator/apk/parser.go b/internal/generator/apk/parser.go index 601902f..350c16d 100644 --- a/internal/generator/apk/parser.go +++ b/internal/generator/apk/parser.go @@ -55,14 +55,14 @@ func extractPKGINFO(path string) ([]byte, error) { if err != nil { return nil, err } - defer f.Close() + defer func() { _ = f.Close() }() // APK files are gzipped tar archives gr, err := gzip.NewReader(f) if err != nil { return nil, err } - defer gr.Close() + defer func() { _ = gr.Close() }() tr := tar.NewReader(gr) @@ -162,13 +162,13 @@ func parseAPKINDEX(path string) ([]models.Package, error) { if err != nil { return nil, err } - defer f.Close() + defer func() { _ = f.Close() }() gz, err := gzip.NewReader(f) if err != nil { return nil, err } - defer gz.Close() + defer func() { _ = gz.Close() }() tr := tar.NewReader(gz) diff --git a/internal/generator/deb/generator_test.go b/internal/generator/deb/generator_test.go index 1fd73ae..552cd50 100644 --- a/internal/generator/deb/generator_test.go +++ b/internal/generator/deb/generator_test.go @@ -16,7 +16,7 @@ func TestGenerateReleaseUnsigned(t *testing.T) { if err != nil { t.Fatalf("Failed to create temp dir: %v", err) } - defer os.RemoveAll(tmpDir) + defer func() { _ = os.RemoveAll(tmpDir) }() // Create generator without signer (unsigned) gen := NewGenerator(nil) @@ -33,14 +33,20 @@ func TestGenerateReleaseUnsigned(t *testing.T) { // Create required directory structure distsDir := filepath.Join(tmpDir, "dists", "testing", "main", "binary-amd64") - os.MkdirAll(distsDir, 0755) + if err := os.MkdirAll(distsDir, 0755); err != nil { + t.Fatalf("Failed to create dists dir: %v", err) + } // Create dummy Packages file packagesPath := filepath.Join(distsDir, "Packages") - os.WriteFile(packagesPath, []byte("Package: test\n"), 0644) + if err := os.WriteFile(packagesPath, []byte("Package: test\n"), 0644); err != nil { + t.Fatalf("Failed to write Packages: %v", err) + } packagesGzPath := filepath.Join(distsDir, "Packages.gz") - os.WriteFile(packagesGzPath, []byte{}, 0644) + if err := os.WriteFile(packagesGzPath, []byte{}, 0644); err != nil { + t.Fatalf("Failed to write Packages.gz: %v", err) + } // Generate repository files err = gen.Generate(context.Background(), config, []models.Package{}) @@ -95,12 +101,16 @@ func TestIncrementalModeCopiesNewPackages(t *testing.T) { if err != nil { t.Fatalf("Failed to create temp dir: %v", err) } - defer os.RemoveAll(tmpDir) + defer func() { _ = os.RemoveAll(tmpDir) }() inputDir := filepath.Join(tmpDir, "input") outputDir := filepath.Join(tmpDir, "output") - os.MkdirAll(inputDir, 0755) - os.MkdirAll(outputDir, 0755) + if err := os.MkdirAll(inputDir, 0755); err != nil { + t.Fatalf("Failed to create input dir: %v", err) + } + if err := os.MkdirAll(outputDir, 0755); err != nil { + t.Fatalf("Failed to create output dir: %v", err) + } gen := NewGenerator(nil) config := &models.RepositoryConfig{ @@ -115,7 +125,9 @@ func TestIncrementalModeCopiesNewPackages(t *testing.T) { // Step 1: Create initial repo with package A initialPkg := filepath.Join(inputDir, "pkga_1.0_amd64.deb") - os.WriteFile(initialPkg, []byte("fake deb package A"), 0644) + if err := os.WriteFile(initialPkg, []byte("fake deb package A"), 0644); err != nil { + t.Fatalf("Failed to write initial package: %v", err) + } packagesA := []models.Package{ { @@ -145,7 +157,7 @@ func TestIncrementalModeCopiesNewPackages(t *testing.T) { // Step 2: Simulate S3 sync - keep only metadata, remove package files // Remove pool directory to simulate only having metadata poolDir := filepath.Join(outputDir, "pool") - os.RemoveAll(poolDir) + _ = os.RemoveAll(poolDir) // Verify package A is gone (simulating S3 scenario) if _, err := os.Stat(pkgAPath); !os.IsNotExist(err) { @@ -154,7 +166,7 @@ func TestIncrementalModeCopiesNewPackages(t *testing.T) { // Step 3: Create new package B newPkg := filepath.Join(inputDir, "pkgb_1.0_amd64.deb") - os.WriteFile(newPkg, []byte("fake deb package B"), 0644) + _ = os.WriteFile(newPkg, []byte("fake deb package B"), 0644) // Step 4: Parse existing metadata (simulating incremental mode) existingPackages, err := gen.ParseExistingMetadata(config) diff --git a/internal/generator/deb/parser.go b/internal/generator/deb/parser.go index 721c656..c8fe4e4 100644 --- a/internal/generator/deb/parser.go +++ b/internal/generator/deb/parser.go @@ -55,7 +55,7 @@ func extractControl(path string) ([]byte, error) { if err != nil { return nil, err } - defer f.Close() + defer func() { _ = f.Close() }() // .deb files are ar archives // Skip the first 8 bytes ("!\n") @@ -83,7 +83,7 @@ func extractControl(path string) ([]byte, error) { // Parse file size (bytes 48-58, decimal) sizeStr := strings.TrimSpace(string(arHeader[48:58])) var size int64 - fmt.Sscanf(sizeStr, "%d", &size) + _, _ = fmt.Sscanf(sizeStr, "%d", &size) // Check if this is the control archive if strings.HasPrefix(filename, "control.tar") { @@ -104,7 +104,7 @@ func extractControl(path string) ([]byte, error) { // Align to 2-byte boundary if size%2 != 0 { - f.Seek(1, io.SeekCurrent) + _, _ = f.Seek(1, io.SeekCurrent) } } @@ -121,7 +121,7 @@ func extractControlFromTar(data []byte, filename string) ([]byte, error) { if err != nil { return nil, err } - defer gr.Close() + defer func() { _ = gr.Close() }() tarReader = tar.NewReader(gr) } else if strings.HasSuffix(filename, ".xz") { xr, err := xz.NewReader(bytes.NewReader(data)) @@ -274,7 +274,7 @@ func parsePackagesFile(path string) ([]models.Package, error) { if err != nil { return nil, err } - defer f.Close() + defer func() { _ = f.Close() }() return parsePackagesReader(f) } @@ -283,13 +283,13 @@ func parsePackagesGzFile(path string) ([]models.Package, error) { if err != nil { return nil, err } - defer f.Close() + defer func() { _ = f.Close() }() gz, err := gzip.NewReader(f) if err != nil { return nil, err } - defer gz.Close() + defer func() { _ = gz.Close() }() return parsePackagesReader(gz) } diff --git a/internal/generator/homebrew/generator.go b/internal/generator/homebrew/generator.go index 0ad0e1f..8ce22b7 100644 --- a/internal/generator/homebrew/generator.go +++ b/internal/generator/homebrew/generator.go @@ -236,7 +236,9 @@ func toClassName(name string) string { // Title case each word words := strings.Fields(name) for i, word := range words { - words[i] = strings.Title(strings.ToLower(word)) + if len(word) > 0 { + words[i] = strings.ToUpper(word[:1]) + strings.ToLower(word[1:]) + } } // Join without spaces diff --git a/internal/generator/homebrew/parser.go b/internal/generator/homebrew/parser.go index cdaeab5..833da79 100644 --- a/internal/generator/homebrew/parser.go +++ b/internal/generator/homebrew/parser.go @@ -45,7 +45,7 @@ func parseFormula(path string) ([]models.Package, error) { if err != nil { return nil, err } - defer f.Close() + defer func() { _ = f.Close() }() var packages []models.Package @@ -92,7 +92,7 @@ func parseFormula(path string) ([]models.Package, error) { // Reset for next bottle url = "" - sha256 = "" + // sha256 will be overwritten by next match } } } diff --git a/internal/generator/pacman/generator_test.go b/internal/generator/pacman/generator_test.go index f4c5cd5..40f489c 100644 --- a/internal/generator/pacman/generator_test.go +++ b/internal/generator/pacman/generator_test.go @@ -144,7 +144,7 @@ func TestGenerateUnsigned(t *testing.T) { if err != nil { t.Fatalf("Failed to create temp dir: %v", err) } - defer os.RemoveAll(tmpDir) + defer func() { _ = os.RemoveAll(tmpDir) }() // Create generator without signer (unsigned) gen := NewGenerator(nil) @@ -170,7 +170,7 @@ func TestGenerateUnsigned(t *testing.T) { } // Create dummy package file - os.WriteFile(packages[0].Filename, []byte("dummy"), 0644) + _ = os.WriteFile(packages[0].Filename, []byte("dummy"), 0644) // Generate repository files err = gen.Generate(context.Background(), config, packages) @@ -203,7 +203,7 @@ func TestGenerateCreatesDbFile(t *testing.T) { if err != nil { t.Fatalf("Failed to create temp dir: %v", err) } - defer os.RemoveAll(tmpDir) + defer func() { _ = os.RemoveAll(tmpDir) }() // Create generator without signer gen := NewGenerator(nil) @@ -229,7 +229,7 @@ func TestGenerateCreatesDbFile(t *testing.T) { } // Create dummy package file - os.WriteFile(packages[0].Filename, []byte("dummy"), 0644) + _ = os.WriteFile(packages[0].Filename, []byte("dummy"), 0644) // Generate repository err = gen.Generate(context.Background(), config, packages) @@ -319,12 +319,12 @@ func TestIncrementalModeCopiesNewPackages(t *testing.T) { if err != nil { t.Fatalf("Failed to create temp dir: %v", err) } - defer os.RemoveAll(tmpDir) + defer func() { _ = os.RemoveAll(tmpDir) }() inputDir := filepath.Join(tmpDir, "input") outputDir := filepath.Join(tmpDir, "output") - os.MkdirAll(inputDir, 0755) - os.MkdirAll(outputDir, 0755) + _ = os.MkdirAll(inputDir, 0755) + _ = os.MkdirAll(outputDir, 0755) gen := NewGenerator(nil) config := &models.RepositoryConfig{ @@ -335,7 +335,7 @@ func TestIncrementalModeCopiesNewPackages(t *testing.T) { // Step 1: Create initial repo with package A initialPkg := filepath.Join(inputDir, "pkga-1.0-1-x86_64.pkg.tar.zst") - os.WriteFile(initialPkg, []byte("fake pacman package A"), 0644) + _ = os.WriteFile(initialPkg, []byte("fake pacman package A"), 0644) packagesA := []models.Package{ { @@ -367,7 +367,7 @@ func TestIncrementalModeCopiesNewPackages(t *testing.T) { files, _ := os.ReadDir(archDir) for _, file := range files { if strings.HasSuffix(file.Name(), ".pkg.tar.zst") { - os.Remove(filepath.Join(archDir, file.Name())) + _ = os.Remove(filepath.Join(archDir, file.Name())) } } @@ -378,7 +378,7 @@ func TestIncrementalModeCopiesNewPackages(t *testing.T) { // Step 3: Create new package B newPkg := filepath.Join(inputDir, "pkgb-1.0-1-x86_64.pkg.tar.zst") - os.WriteFile(newPkg, []byte("fake pacman package B"), 0644) + _ = os.WriteFile(newPkg, []byte("fake pacman package B"), 0644) // Step 4: Parse existing metadata (simulating incremental mode) existingPackages, err := gen.ParseExistingMetadata(config) diff --git a/internal/generator/pacman/parser.go b/internal/generator/pacman/parser.go index 03a2eaf..39ed7b8 100644 --- a/internal/generator/pacman/parser.go +++ b/internal/generator/pacman/parser.go @@ -54,7 +54,7 @@ func extractPKGINFO(path string) ([]byte, error) { if err != nil { return nil, err } - defer f.Close() + defer func() { _ = f.Close() }() // Detect compression from extension var tarReader *tar.Reader @@ -77,7 +77,7 @@ func extractPKGINFO(path string) ([]byte, error) { if err != nil { return nil, err } - defer gr.Close() + defer func() { _ = gr.Close() }() tarReader = tar.NewReader(gr) } else if strings.HasSuffix(path, ".pkg.tar") { tarReader = tar.NewReader(f) @@ -217,7 +217,7 @@ func parsePacmanDB(dbPath string) ([]models.Package, error) { if err != nil { return nil, err } - defer f.Close() + defer func() { _ = f.Close() }() // Detect compression from extension var tarReader *tar.Reader @@ -228,7 +228,7 @@ func parsePacmanDB(dbPath string) ([]models.Package, error) { if err != nil { // If zstd fails and it's .db.tar, try uncompressed if strings.HasSuffix(dbPath, ".db.tar") { - f.Seek(0, 0) + _, _ = f.Seek(0, 0) tarReader = tar.NewReader(f) } else { return nil, err @@ -248,7 +248,7 @@ func parsePacmanDB(dbPath string) ([]models.Package, error) { if err != nil { return nil, err } - defer gr.Close() + defer func() { _ = gr.Close() }() tarReader = tar.NewReader(gr) } else { // Assume it's a symlink or uncompressed tar @@ -325,7 +325,7 @@ func parseDescFile(data []byte) (*models.Package, error) { pkg.Description = line case "CSIZE": size := int64(0) - fmt.Sscanf(line, "%d", &size) + _, _ = fmt.Sscanf(line, "%d", &size) pkg.Size = size case "MD5SUM": pkg.MD5Sum = line diff --git a/internal/generator/rpm/generator_test.go b/internal/generator/rpm/generator_test.go index bb24a7f..93601fa 100644 --- a/internal/generator/rpm/generator_test.go +++ b/internal/generator/rpm/generator_test.go @@ -22,24 +22,30 @@ func TestIncrementalModeCopiesNewPackages(t *testing.T) { if err != nil { t.Fatalf("Failed to create temp dir: %v", err) } - defer os.RemoveAll(tmpDir) + defer func() { _ = os.RemoveAll(tmpDir) }() inputDir := filepath.Join(tmpDir, "input") outputDir := filepath.Join(tmpDir, "output") - os.MkdirAll(inputDir, 0755) - os.MkdirAll(outputDir, 0755) + if err := os.MkdirAll(inputDir, 0755); err != nil { + t.Fatalf("Failed to create input dir: %v", err) + } + if err := os.MkdirAll(outputDir, 0755); err != nil { + t.Fatalf("Failed to create output dir: %v", err) + } gen := NewGenerator(nil) config := &models.RepositoryConfig{ - OutputDir: outputDir, - Version: "40", - DistroVariant: "fedora", - Arches: []string{"x86_64"}, + OutputDir: outputDir, + Version: "40", + DistroVariant: "fedora", + Arches: []string{"x86_64"}, } // Step 1: Create initial repo with package A initialPkg := filepath.Join(inputDir, "pkga-1.0-1.x86_64.rpm") - os.WriteFile(initialPkg, []byte("fake rpm package A"), 0644) + if err := os.WriteFile(initialPkg, []byte("fake rpm package A"), 0644); err != nil { + t.Fatalf("Failed to write initial package: %v", err) + } packagesA := []models.Package{ { @@ -71,7 +77,7 @@ func TestIncrementalModeCopiesNewPackages(t *testing.T) { // Step 2: Simulate S3 sync - keep only repodata, remove package files packagesDir := filepath.Join(outputDir, "40", "x86_64", "Packages") - os.RemoveAll(packagesDir) + _ = os.RemoveAll(packagesDir) // Verify package A is gone (simulating S3 scenario) if _, err := os.Stat(pkgAPath); !os.IsNotExist(err) { @@ -80,7 +86,7 @@ func TestIncrementalModeCopiesNewPackages(t *testing.T) { // Step 3: Create new package B newPkg := filepath.Join(inputDir, "pkgb-1.0-1.x86_64.rpm") - os.WriteFile(newPkg, []byte("fake rpm package B"), 0644) + _ = os.WriteFile(newPkg, []byte("fake rpm package B"), 0644) // Step 4: Parse existing metadata (simulating incremental mode) existingPackages, err := gen.ParseExistingMetadata(config) diff --git a/internal/generator/rpm/parser.go b/internal/generator/rpm/parser.go index 23cca75..5433028 100644 --- a/internal/generator/rpm/parser.go +++ b/internal/generator/rpm/parser.go @@ -28,7 +28,7 @@ func ParsePackage(path string) (*models.Package, error) { if err != nil { return nil, err } - defer f.Close() + defer func() { _ = f.Close() }() // Read RPM header rpm, err := rpmutils.ReadRpm(f) @@ -262,13 +262,13 @@ func parsePrimaryXML(archDir string) ([]models.Package, error) { if err != nil { return nil, err } - defer f.Close() + defer func() { _ = f.Close() }() gz, err := gzip.NewReader(f) if err != nil { return nil, err } - defer gz.Close() + defer func() { _ = gz.Close() }() data, err := io.ReadAll(gz) if err != nil { diff --git a/internal/generator/sysext/generator.go b/internal/generator/sysext/generator.go index 97d934a..a698ae6 100644 --- a/internal/generator/sysext/generator.go +++ b/internal/generator/sysext/generator.go @@ -6,6 +6,7 @@ import ( "fmt" "os" "path/filepath" + "sort" "strings" "github.com/ralt/repogen/internal/generator" @@ -57,6 +58,11 @@ func (g *Generator) Generate(ctx context.Context, config *models.RepositoryConfi } } + // Generate index file listing all extensions + if err := g.generateIndex(config, extPackages); err != nil { + return fmt.Errorf("failed to generate index: %w", err) + } + logrus.Info("systemd-sysext repository generated successfully") return nil } @@ -170,6 +176,32 @@ CurrentSymlink=%s.raw return nil } +// generateIndex creates an index file listing all available extensions. +// The index is a simple newline-separated list of extension names. +func (g *Generator) generateIndex(config *models.RepositoryConfig, extPackages map[string][]models.Package) error { + extDir := filepath.Join(config.OutputDir, "ext") + if err := utils.EnsureDir(extDir); err != nil { + return err + } + + // Collect extension names and sort them for consistent output + var names []string + for name := range extPackages { + names = append(names, name) + } + sort.Strings(names) + + // Write index file (one extension name per line) + indexPath := filepath.Join(extDir, "index") + indexContent := strings.Join(names, "\n") + "\n" + if err := utils.WriteFile(indexPath, []byte(indexContent), 0644); err != nil { + return err + } + + logrus.Debugf("Generated index file with %d extensions", len(names)) + return nil +} + // ValidatePackages checks if packages are valid for this generator func (g *Generator) ValidatePackages(packages []models.Package) error { if g.baseURL == "" { @@ -241,7 +273,7 @@ func parseSHA256SUMS(sha256sumsPath, extDir, extName string) ([]models.Package, if err != nil { return nil, err } - defer f.Close() + defer func() { _ = f.Close() }() var packages []models.Package scanner := bufio.NewScanner(f) diff --git a/internal/generator/sysext/generator_test.go b/internal/generator/sysext/generator_test.go index 2668916..bde10f1 100644 --- a/internal/generator/sysext/generator_test.go +++ b/internal/generator/sysext/generator_test.go @@ -95,7 +95,7 @@ func TestParsePackage(t *testing.T) { if err != nil { t.Fatalf("Failed to create temp dir: %v", err) } - defer os.RemoveAll(tmpDir) + defer func() { _ = os.RemoveAll(tmpDir) }() filePath := filepath.Join(tmpDir, tt.filename) err = os.WriteFile(filePath, []byte("fake sysext content"), 0644) @@ -141,21 +141,31 @@ func TestGeneratorGenerate(t *testing.T) { if err != nil { t.Fatalf("Failed to create temp dir: %v", err) } - defer os.RemoveAll(tmpDir) + defer func() { _ = os.RemoveAll(tmpDir) }() inputDir := filepath.Join(tmpDir, "input") outputDir := filepath.Join(tmpDir, "output") - os.MkdirAll(inputDir, 0755) - os.MkdirAll(outputDir, 0755) + if err := os.MkdirAll(inputDir, 0755); err != nil { + t.Fatalf("Failed to create input dir: %v", err) + } + if err := os.MkdirAll(outputDir, 0755); err != nil { + t.Fatalf("Failed to create output dir: %v", err) + } // Create test sysext files ext1 := filepath.Join(inputDir, "myext_1.0_x86-64.raw") ext2 := filepath.Join(inputDir, "myext_2.0_x86-64.raw.zst") ext3 := filepath.Join(inputDir, "other_1.0_x86-64.raw") - os.WriteFile(ext1, []byte("sysext content v1"), 0644) - os.WriteFile(ext2, []byte("sysext content v2 compressed"), 0644) - os.WriteFile(ext3, []byte("other ext content"), 0644) + if err := os.WriteFile(ext1, []byte("sysext content v1"), 0644); err != nil { + t.Fatalf("Failed to write ext1: %v", err) + } + if err := os.WriteFile(ext2, []byte("sysext content v2 compressed"), 0644); err != nil { + t.Fatalf("Failed to write ext2: %v", err) + } + if err := os.WriteFile(ext3, []byte("other ext content"), 0644); err != nil { + t.Fatalf("Failed to write ext3: %v", err) + } gen := NewGenerator("https://example.com/repo") config := &models.RepositoryConfig{ @@ -268,6 +278,24 @@ func TestGeneratorGenerate(t *testing.T) { if !strings.Contains(transferStr, "CurrentSymlink=myext.raw") { t.Errorf("Transfer file missing CurrentSymlink, got: %s", transferStr) } + + // Verify index file exists and contains correct extensions + indexPath := filepath.Join(outputDir, "ext", "index") + if _, err := os.Stat(indexPath); os.IsNotExist(err) { + t.Errorf("index file not created") + } + + indexContent, err := os.ReadFile(indexPath) + if err != nil { + t.Fatalf("Failed to read index file: %v", err) + } + + indexStr := string(indexContent) + // Index should be sorted alphabetically + expectedIndex := "myext\nother\n" + if indexStr != expectedIndex { + t.Errorf("index file content = %q, want %q", indexStr, expectedIndex) + } } func TestIncrementalMode(t *testing.T) { @@ -275,11 +303,13 @@ func TestIncrementalMode(t *testing.T) { if err != nil { t.Fatalf("Failed to create temp dir: %v", err) } - defer os.RemoveAll(tmpDir) + defer func() { _ = os.RemoveAll(tmpDir) }() inputDir := filepath.Join(tmpDir, "input") outputDir := filepath.Join(tmpDir, "output") - os.MkdirAll(inputDir, 0755) + if err := os.MkdirAll(inputDir, 0755); err != nil { + t.Fatalf("Failed to create input dir: %v", err) + } gen := NewGenerator("https://example.com/repo") config := &models.RepositoryConfig{ @@ -288,7 +318,9 @@ func TestIncrementalMode(t *testing.T) { // Step 1: Create initial repo with v1.0 ext1 := filepath.Join(inputDir, "myext_1.0_x86-64.raw") - os.WriteFile(ext1, []byte("sysext content v1"), 0644) + if err := os.WriteFile(ext1, []byte("sysext content v1"), 0644); err != nil { + t.Fatalf("Failed to write ext1: %v", err) + } packagesV1 := []models.Package{ {Name: "myext", Version: "1.0", Architecture: "x86-64", Filename: ext1, SHA256Sum: "abc123"}, @@ -322,7 +354,7 @@ func TestIncrementalMode(t *testing.T) { // Step 3: Add new version ext2 := filepath.Join(inputDir, "myext_2.0_x86-64.raw.zst") - os.WriteFile(ext2, []byte("sysext content v2"), 0644) + _ = os.WriteFile(ext2, []byte("sysext content v2"), 0644) packagesV2 := []models.Package{ {Name: "myext", Version: "2.0", Architecture: "x86-64", Filename: ext2, SHA256Sum: "def456"}, @@ -349,6 +381,77 @@ func TestIncrementalMode(t *testing.T) { if len(lines) != 2 { t.Errorf("Updated SHA256SUMS should have 2 lines, got %d: %s", len(lines), updatedContent) } + + // Verify index file still contains myext + indexPath := filepath.Join(outputDir, "ext", "index") + indexContent, err := os.ReadFile(indexPath) + if err != nil { + t.Fatalf("Failed to read index file: %v", err) + } + if string(indexContent) != "myext\n" { + t.Errorf("Index file content = %q, want %q", string(indexContent), "myext\n") + } +} + +func TestIndexUpdatedWithNewExtension(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "repogen-test-sysext-index-") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer func() { _ = os.RemoveAll(tmpDir) }() + + inputDir := filepath.Join(tmpDir, "input") + outputDir := filepath.Join(tmpDir, "output") + _ = os.MkdirAll(inputDir, 0755) + + gen := NewGenerator("https://example.com/repo") + config := &models.RepositoryConfig{ + OutputDir: outputDir, + } + + // Step 1: Create initial repo with one extension + ext1 := filepath.Join(inputDir, "alpha_1.0_x86-64.raw") + _ = os.WriteFile(ext1, []byte("alpha content"), 0644) + + packagesAlpha := []models.Package{ + {Name: "alpha", Version: "1.0", Architecture: "x86-64", Filename: ext1, SHA256Sum: "abc123"}, + } + + err = gen.Generate(context.Background(), config, packagesAlpha) + if err != nil { + t.Fatalf("Initial generation failed: %v", err) + } + + // Verify initial index + indexPath := filepath.Join(outputDir, "ext", "index") + initialIndex, _ := os.ReadFile(indexPath) + if string(initialIndex) != "alpha\n" { + t.Errorf("Initial index = %q, want %q", string(initialIndex), "alpha\n") + } + + // Step 2: Add a new extension (beta) that comes before alpha alphabetically + ext2 := filepath.Join(inputDir, "beta_1.0_x86-64.raw") + _ = os.WriteFile(ext2, []byte("beta content"), 0644) + + // Parse existing and combine with new + existingPackages, _ := gen.ParseExistingMetadata(config) + + packagesBeta := []models.Package{ + {Name: "beta", Version: "1.0", Architecture: "x86-64", Filename: ext2, SHA256Sum: "def456"}, + } + allPackages := append(existingPackages, packagesBeta...) + + err = gen.Generate(context.Background(), config, allPackages) + if err != nil { + t.Fatalf("Incremental generation failed: %v", err) + } + + // Verify index is updated and sorted + updatedIndex, _ := os.ReadFile(indexPath) + expectedIndex := "alpha\nbeta\n" + if string(updatedIndex) != expectedIndex { + t.Errorf("Updated index = %q, want %q", string(updatedIndex), expectedIndex) + } } func TestValidatePackages(t *testing.T) { diff --git a/internal/scanner/detector.go b/internal/scanner/detector.go index 160ab29..278f94b 100644 --- a/internal/scanner/detector.go +++ b/internal/scanner/detector.go @@ -32,7 +32,7 @@ func DetectPackageType(path string) (PackageType, error) { if err != nil { return TypeUnknown, err } - defer f.Close() + defer func() { _ = f.Close() }() // Read first 512 bytes for magic byte detection header := make([]byte, 512) diff --git a/internal/signer/gpg.go b/internal/signer/gpg.go index 7f4f941..df87e6f 100644 --- a/internal/signer/gpg.go +++ b/internal/signer/gpg.go @@ -30,13 +30,13 @@ func NewGPGSigner(keyPath, passphrase string) (*GPGSigner, error) { if err != nil { return nil, fmt.Errorf("failed to open key file: %w", err) } - defer keyFile.Close() + defer func() { _ = keyFile.Close() }() // Try to parse as armored key first entityList, err := openpgp.ReadArmoredKeyRing(keyFile) if err != nil { // Try as binary key - keyFile.Seek(0, 0) + _, _ = keyFile.Seek(0, 0) entityList, err = openpgp.ReadKeyRing(keyFile) if err != nil { return nil, fmt.Errorf("failed to read key: %w", err) @@ -85,7 +85,7 @@ func (s *GPGSigner) SignCleartext(data []byte) ([]byte, error) { if err != nil { return nil, fmt.Errorf("failed to create temp dir: %w", err) } - defer os.RemoveAll(tmpDir) + defer func() { _ = os.RemoveAll(tmpDir) }() // Import the key keyPath, err := filepath.Abs(s.keyPath) @@ -145,7 +145,7 @@ func (s *GPGSigner) SignDetachedBinary(data []byte) ([]byte, error) { if err != nil { return nil, fmt.Errorf("failed to create temp dir: %w", err) } - defer os.RemoveAll(tmpDir) + defer func() { _ = os.RemoveAll(tmpDir) }() // Import the key keyPath, err := filepath.Abs(s.keyPath) @@ -191,7 +191,7 @@ func (s *GPGSigner) SignDetachedBinaryFromFile(filePath string) ([]byte, error) if err != nil { return nil, fmt.Errorf("failed to create temp dir: %w", err) } - defer os.RemoveAll(tmpDir) + defer func() { _ = os.RemoveAll(tmpDir) }() // Import the key keyPath, err := filepath.Abs(s.keyPath) @@ -240,7 +240,7 @@ func (s *GPGSigner) GetPublicKey() ([]byte, error) { err = s.entity.Serialize(w) if err != nil { - w.Close() + _ = w.Close() return nil, err } @@ -251,62 +251,6 @@ func (s *GPGSigner) GetPublicKey() ([]byte, error) { return buf.Bytes(), nil } -// canonicalizeText implements RFC 4880 text canonicalization for signing -// Removes trailing whitespace from each line and uses CRLF line endings -func canonicalizeText(data []byte) []byte { - lines := bytes.Split(data, []byte("\n")) - var buf bytes.Buffer - - for i, line := range lines { - // Remove trailing spaces and tabs (but not the content) - line = bytes.TrimRight(line, " \t\r") - buf.Write(line) - // Add CRLF for all lines except the last empty one - if i < len(lines)-1 || len(line) > 0 { - buf.WriteString("\r\n") - } - } - - return buf.Bytes() -} - -// dashEscape performs dash-escaping required by OpenPGP cleartext signatures -// Any line starting with '-' must be prefixed with "- " (RFC 4880 section 7.1) -func dashEscape(data []byte) []byte { - lines := bytes.Split(data, []byte("\n")) - var buf bytes.Buffer - - for i, line := range lines { - // Dash-escape lines starting with '-' - if bytes.HasPrefix(line, []byte("-")) { - buf.WriteString("- ") - } - buf.Write(line) - // Add newline except for the last line if it was empty - if i < len(lines)-1 { - buf.WriteString("\n") - } - } - - return buf.Bytes() -} - -// createCleartextSignature creates a PGP cleartext signature format -func createCleartextSignature(message, signature []byte) []byte { - var buf bytes.Buffer - - buf.WriteString("-----BEGIN PGP SIGNED MESSAGE-----\n") - buf.WriteString("Hash: SHA512\n") - buf.WriteString("\n") - buf.Write(message) - if !bytes.HasSuffix(message, []byte("\n")) { - buf.WriteString("\n") - } - buf.Write(signature) - - return buf.Bytes() -} - // NewNilSigner returns a nil signer (for unsigned repositories) func NewNilSigner() Signer { return nil diff --git a/internal/signer/rsa.go b/internal/signer/rsa.go index 0af6e13..87db276 100644 --- a/internal/signer/rsa.go +++ b/internal/signer/rsa.go @@ -37,12 +37,14 @@ func NewAlpineRSASigner(keyPath, passphrase string) (*AlpineRSASigner, error) { // Check if key is encrypted var privateKey *rsa.PrivateKey + //nolint:staticcheck // SA1019: Legacy PEM encryption support for older RSA keys if x509.IsEncryptedPEMBlock(block) { if passphrase == "" { return nil, fmt.Errorf("key is encrypted but no passphrase provided") } // Decrypt the PEM block + //nolint:staticcheck // SA1019: Legacy PEM encryption support for older RSA keys decryptedData, err := x509.DecryptPEMBlock(block, []byte(passphrase)) if err != nil { return nil, fmt.Errorf("failed to decrypt key: %w", err) diff --git a/internal/utils/checksum.go b/internal/utils/checksum.go index 96fd29f..2703f57 100644 --- a/internal/utils/checksum.go +++ b/internal/utils/checksum.go @@ -26,7 +26,7 @@ func CalculateChecksums(path string) (*Checksum, error) { if err != nil { return nil, err } - defer f.Close() + defer func() { _ = f.Close() }() // Get file info for size info, err := f.Stat() diff --git a/internal/utils/compression.go b/internal/utils/compression.go index a9edbf5..f5ab10c 100644 --- a/internal/utils/compression.go +++ b/internal/utils/compression.go @@ -29,7 +29,7 @@ func GzipDecompress(data []byte) ([]byte, error) { if err != nil { return nil, err } - defer r.Close() + defer func() { _ = r.Close() }() return io.ReadAll(r) } diff --git a/internal/utils/fileops.go b/internal/utils/fileops.go index 950c0d2..6b63f09 100644 --- a/internal/utils/fileops.go +++ b/internal/utils/fileops.go @@ -22,14 +22,14 @@ func CopyFile(src, dst string) error { if err != nil { return err } - defer srcFile.Close() + defer func() { _ = srcFile.Close() }() // Create destination file dstFile, err := os.Create(dst) if err != nil { return err } - defer dstFile.Close() + defer func() { _ = dstFile.Close() }() // Copy contents if _, err := io.Copy(dstFile, srcFile); err != nil { diff --git a/test/integration_test.go b/test/integration_test.go index 43f4701..0dda1ed 100644 --- a/test/integration_test.go +++ b/test/integration_test.go @@ -1080,65 +1080,6 @@ func findInString(s, substr string) bool { return false } -// generateTestGPGKey creates a test GPG key pair for repository signing tests -func generateTestGPGKey(privateKeyPath, publicKeyPath string) error { - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - - // Create GPG batch file for unattended key generation - batchContent := ` -%no-protection -Key-Type: RSA -Key-Length: 2048 -Name-Real: Repogen Test Key -Name-Email: test@repogen.local -Expire-Date: 0 -%commit -` - - tmpDir := filepath.Dir(privateKeyPath) - batchFile := filepath.Join(tmpDir, "gpg-batch.txt") - if err := os.WriteFile(batchFile, []byte(batchContent), 0600); err != nil { - return fmt.Errorf("failed to create batch file: %w", err) - } - defer os.Remove(batchFile) - - // Generate key using gpg with temporary home directory - gpgHome := filepath.Join(tmpDir, "gpg-home") - if err := os.MkdirAll(gpgHome, 0700); err != nil { - return fmt.Errorf("failed to create GPG home: %w", err) - } - - // Generate the key - cmd := exec.CommandContext(ctx, "gpg", "--homedir", gpgHome, "--batch", "--gen-key", batchFile) - output, err := cmd.CombinedOutput() - if err != nil { - return fmt.Errorf("failed to generate GPG key: %w\nOutput: %s", err, output) - } - - // Export private key - cmd = exec.CommandContext(ctx, "gpg", "--homedir", gpgHome, "--armor", "--export-secret-keys", "test@repogen.local") - privateKey, err := cmd.Output() - if err != nil { - return fmt.Errorf("failed to export private key: %w", err) - } - if err := os.WriteFile(privateKeyPath, privateKey, 0600); err != nil { - return fmt.Errorf("failed to write private key: %w", err) - } - - // Export public key - cmd = exec.CommandContext(ctx, "gpg", "--homedir", gpgHome, "--armor", "--export", "test@repogen.local") - publicKey, err := cmd.Output() - if err != nil { - return fmt.Errorf("failed to export public key: %w", err) - } - if err := os.WriteFile(publicKeyPath, publicKey, 0644); err != nil { - return fmt.Errorf("failed to write public key: %w", err) - } - - return nil -} - // Checksum extraction and calculation helpers func extractPacmanChecksums(dbPath string) (map[string]string, error) { @@ -1163,7 +1104,7 @@ func extractPacmanChecksums(dbPath string) (map[string]string, error) { output, err := tarCmd.CombinedOutput() if err != nil { - zstdCmd.Wait() + _ = zstdCmd.Wait() return nil, fmt.Errorf("tar extraction failed: %v\nOutput: %s", err, output) } @@ -1195,7 +1136,7 @@ func extractPacmanChecksums(dbPath string) (map[string]string, error) { } // Clean up - os.RemoveAll(filepath.Dir(descFile)) + _ = os.RemoveAll(filepath.Dir(descFile)) } return checksums, nil @@ -1233,7 +1174,7 @@ func extractRPMChecksums(primaryXMLPath string) (map[string]string, error) { if err != nil { return nil, err } - defer gzFile.Close() + defer func() { _ = gzFile.Close() }() gzReader, err := exec.Command("gzip", "-d", "-c", primaryXMLPath).Output() if err != nil { @@ -1302,11 +1243,9 @@ func extractAPKChecksums(apkindexPath string) (map[string]string, error) { if strings.HasPrefix(line, "C:") { currentChecksum = strings.TrimPrefix(line, "C:") } - // P field + V field + A field make up filename - if strings.HasPrefix(line, "P:") && currentFilename == "" { - // We'll use a simpler approach - look for actual .apk files - // and match them by looking at the package name - } + // P field starts a new package entry + // We use a simpler glob-based approach below for matching + _ = currentFilename // mark as used for linter } // For simplicity, just glob the directory and match checksums