diff --git a/CLAUDE.md b/CLAUDE.md index 0f56c61..dec7c0d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,10 +17,10 @@ Spec ↔ implementation conformance is machine-enforced by two tests: Tool versions are managed by [mise](https://mise.jdx.dev/) ([mise.toml](mise.toml)): Go 1.26.4, golangci-lint 2.12.2. ```bash -make check # check-tidy + lint (CI runs tests as a separate step) +make check # check-tidy + lint under GOOS=linux and GOOS=windows (CI runs tests as a separate step) make test # go test -race -failfast ./... -make lint # golangci-lint config verify + run -v -make fix # go mod tidy + golangci-lint --fix +make lint # golangci-lint config verify + run (host GOOS only, for quick local iteration) +make fix # go mod tidy + golangci-lint --fix under GOOS=linux and GOOS=windows make check-tidy # go mod tidy -diff make build # build binary to ./bin/graft make release # goreleaser release --clean diff --git a/Makefile b/Makefile index e907e7f..cb8ec2f 100644 --- a/Makefile +++ b/Makefile @@ -5,6 +5,10 @@ COMMIT ?= $(shell git rev-parse HEAD) LDFLAGS ?= -s -w -X main.version=$(VERSION) -X main.commit=$(COMMIT) NEW_FROM_REV ?= HEAD +# GOOS values that carry build-tagged source (currently just linkdir_windows.go); +# check/fix lint under each so tagged files aren't silently skipped. +LINT_GOOS ?= linux windows + .PHONY: build build: mkdir -p ./bin/ @@ -13,7 +17,7 @@ build: .PHONY: fix fix: go mod tidy - golangci-lint run --new-from-rev=$(NEW_FROM_REV) --fix ./... + for goos in $(LINT_GOOS); do GOOS=$$goos golangci-lint run --new-from-rev=$(NEW_FROM_REV) --fix ./... || exit 1; done .PHONY: lint lint: @@ -38,7 +42,9 @@ check-tidy: go mod tidy -diff .PHONY: check -check: check-tidy lint +check: check-tidy + golangci-lint config verify + for goos in $(LINT_GOOS); do GOOS=$$goos golangci-lint run --new-from-rev=$(NEW_FROM_REV) ./... || exit 1; done .PHONY: release release: diff --git a/internal/resolver/resolver_test.go b/internal/resolver/resolver_test.go index 26c1a58..2899e26 100644 --- a/internal/resolver/resolver_test.go +++ b/internal/resolver/resolver_test.go @@ -372,3 +372,34 @@ func TestResolveLatest(t *testing.T) { } }) } + +// TestResolveLatest_rejectsMalformedTags covers parseSemver's rejection rules +// (split out from TestResolveLatest to keep its cognitive complexity down). +func TestResolveLatest_rejectsMalformedTags(t *testing.T) { + t.Parallel() + + // Each of these looks close to a release tag but fails a specific + // parseSemver rule; only v1.0.0 is a valid candidate. If any rule + // regressed, one of these would parse and outrank it. + r := gittest.New(t) + r.WriteFile("f.txt", "a\n") + valid := r.Commit("v1") + r.Tag("v1.0.0") + r.Tag("v1.02.0") // leading zero + r.Tag("v1.2") // wrong segment count + r.Tag("v1.2.x") // non-digit segment + r.Tag("v1.2.0-rc") // caught earlier by the pre-release check, for good measure + + res, tag, err := resolver.ResolveLatest(t.Context(), r.URL()) + if err != nil { + t.Fatal(err) + } + + if tag != "v1.0.0" { + t.Errorf("tag = %q, want v1.0.0 (malformed tags must not outrank it)", tag) + } + + if res.Commit != valid { + t.Errorf("commit = %q, want %q", res.Commit, valid) + } +} diff --git a/internal/vendordir/linkdir_windows.go b/internal/vendordir/linkdir_windows.go index fa696e7..d26d6c6 100644 --- a/internal/vendordir/linkdir_windows.go +++ b/internal/vendordir/linkdir_windows.go @@ -17,7 +17,7 @@ import ( // privilege. The reparse point is set via DeviceIoControl (FSCTL_SET_REPARSE_POINT) // to avoid spawning cmd.exe. target must be an absolute path. func linkDir(target, link string) error { - if err := os.Mkdir(link, 0o777); err != nil { + if err := os.Mkdir(link, 0o755); err != nil { //nolint:gosec // Vendor trees are world-readable by design. return fmt.Errorf("create junction dir: %w", err) } @@ -30,6 +30,34 @@ func linkDir(target, link string) error { } func setJunction(link, target string) error { + // Junction substitute name must use the NT path prefix. + sub := windows.StringToUTF16(`\??\` + target) + sub = sub[:len(sub)-1] // strip null terminator; byte lengths below exclude it + + // REPARSE_DATA_BUFFER wire layout for IO_REPARSE_TAG_MOUNT_POINT: + // [0:4] ReparseTag = IO_REPARSE_TAG_MOUNT_POINT + // [4:6] ReparseDataLength = 8 + len(PathBuffer in bytes) + // [6:8] Reserved = 0 + // [8:10] SubstituteNameOffset = 0 + // [10:12] SubstituteNameLength = len(sub) * 2 + // [12:14] PrintNameOffset = len(sub)*2 + 2 (past sub + NUL) + // [14:16] PrintNameLength = 0 + // [16:] PathBuffer: sub (UTF-16LE) + NUL + NUL (empty print name) + // bufOverhead is the 16-byte header plus the NUL and empty print-name + // terminators (2+2 bytes) added around subBytes when building pathBufLen + // and buf further down — named so the bound check can't silently drift + // from that construction. + const bufOverhead = 16 + 2 + 2 + + // windows.MAXIMUM_REPARSE_DATA_BUFFER_SIZE (16 KiB) is the kernel's real + // cap on the whole buffer (header + PathBuffer); it's far tighter than + // the uint16 fields below could otherwise hold, so bounding subBytes + // against it also keeps every conversion in range. + subBytes := len(sub) * 2 + if subBytes > windows.MAXIMUM_REPARSE_DATA_BUFFER_SIZE-bufOverhead { + return fmt.Errorf("target path too long for a junction: %d bytes", subBytes) + } + linkPtr, err := windows.UTF16PtrFromString(link) if err != nil { return err @@ -48,20 +76,6 @@ func setJunction(link, target string) error { } defer windows.CloseHandle(h) //nolint:errcheck - // Junction substitute name must use the NT path prefix. - sub := windows.StringToUTF16(`\??\` + target) - sub = sub[:len(sub)-1] // strip null terminator; byte lengths below exclude it - - // REPARSE_DATA_BUFFER wire layout for IO_REPARSE_TAG_MOUNT_POINT: - // [0:4] ReparseTag = IO_REPARSE_TAG_MOUNT_POINT - // [4:6] ReparseDataLength = 8 + len(PathBuffer in bytes) - // [6:8] Reserved = 0 - // [8:10] SubstituteNameOffset = 0 - // [10:12] SubstituteNameLength = len(sub) * 2 - // [12:14] PrintNameOffset = len(sub)*2 + 2 (past sub + NUL) - // [14:16] PrintNameLength = 0 - // [16:] PathBuffer: sub (UTF-16LE) + NUL + NUL (empty print name) - subBytes := len(sub) * 2 pathBufLen := subBytes + 2 + 2 buf := make([]byte, 16+pathBufLen) @@ -69,14 +83,16 @@ func setJunction(link, target string) error { binary.LittleEndian.PutUint16(buf[4:], uint16(8+pathBufLen)) binary.LittleEndian.PutUint16(buf[10:], uint16(subBytes)) binary.LittleEndian.PutUint16(buf[12:], uint16(subBytes+2)) + for i, c := range sub { binary.LittleEndian.PutUint16(buf[16+i*2:], c) } var n uint32 + return windows.DeviceIoControl( h, windows.FSCTL_SET_REPARSE_POINT, - &buf[0], uint32(len(buf)), + &buf[0], uint32(len(buf)), //nolint:gosec // buf length is bounded by the pathBufLen check above nil, 0, &n, nil, ) }