Cybersecurity-Projects/PROJECTS/beginner/simple-vulnerability-scanner/internal/requirements/writer.go

97 lines
2.2 KiB
Go

/*
©AngelaMos | 2026
writer.go
In-place updater for requirements.txt that preserves comments and original casing
Matches package names in PEP 503 normalized form while keeping the original
capitalization in the file. Writes atomically via temp file and rename.
Key exports:
UpdateFile - replaces version specifiers for the given packages in a requirements.txt
Connects to:
update.go - called when the target file ends in .txt
pypi/client.go - imports NormalizeName for case-insensitive name matching
*/
package requirements
import (
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/CarterPerez-dev/angela/internal/pypi"
)
// UpdateFile replaces version specifiers for the given packages in a
// requirements.txt file, preserving all comments and formatting
func UpdateFile(path string, updates map[string]string) error {
data, err := os.ReadFile(path) //nolint:gosec
if err != nil {
return fmt.Errorf("read %s: %w", path, err)
}
content := data
for pkg, spec := range updates {
pattern := buildPattern(pkg)
found := false
content = pattern.ReplaceAllFunc(
content,
func(match []byte) []byte {
found = true
return replaceSpec(pattern, match, spec)
},
)
if !found {
return fmt.Errorf(
"dependency %q not found in %s", pkg, path,
)
}
}
path = filepath.Clean(path)
tmp := path + ".tmp"
if err := os.WriteFile(tmp, content, 0o600); err != nil {
return fmt.Errorf("write temp: %w", err)
}
if err := os.Rename(tmp, path); err != nil {
_ = os.Remove(tmp) //nolint:errcheck
return fmt.Errorf("rename: %w", err)
}
return nil
}
func buildPattern(name string) *regexp.Regexp {
normalized := pypi.NormalizeName(name)
parts := strings.Split(normalized, "-")
for i, p := range parts {
parts[i] = regexp.QuoteMeta(p)
}
namePattern := strings.Join(parts, `[-_.]?`)
return regexp.MustCompile(
`(?im)^(` + namePattern + `)` +
`(\[[^\]]*\])?` +
`(\s*[><=!~][^\n;#]*)`,
)
}
func replaceSpec(
re *regexp.Regexp, match []byte, newSpec string,
) []byte {
groups := re.FindSubmatch(match)
if len(groups) < 4 {
return match
}
var b []byte
b = append(b, groups[1]...)
b = append(b, groups[2]...)
b = append(b, []byte(newSpec)...)
return b
}