Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 3 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 8 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand All @@ -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:
Expand All @@ -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:
Expand Down
31 changes: 31 additions & 0 deletions internal/resolver/resolver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
48 changes: 32 additions & 16 deletions internal/vendordir/linkdir_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand All @@ -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
Expand All @@ -48,35 +76,23 @@ 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)

binary.LittleEndian.PutUint32(buf[0:], windows.IO_REPARSE_TAG_MOUNT_POINT)
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,
)
}
Loading