diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..a5c7c744 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.wit text eol=lf diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 7649924c..eb2bfe9f 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -12,7 +12,7 @@ jobs: build: name: Build binaries permissions: - contents: write + contents: read runs-on: ubuntu-latest strategy: @@ -25,10 +25,10 @@ jobs: steps: - name: Checkout repo - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v7 with: go-version-file: go.mod @@ -49,7 +49,7 @@ jobs: rm wit-bindgen-go - name: Upload artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: wit-bindgen-go_${{ matrix.goos }}_${{ matrix.goarch }} path: build/wit-bindgen-go-${{ matrix.goos }}-${{ matrix.goarch }}.tgz @@ -57,14 +57,16 @@ jobs: release: name: Create GitHub release + permissions: + contents: write needs: build runs-on: ubuntu-latest steps: - name: Checkout repo - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Download Artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: path: build/ merge-multiple: true # all artifacts are downloaded to this directory @@ -72,8 +74,8 @@ jobs: - name: Extract release notes run: | TAG_NAME=${GITHUB_REF#refs/tags/} - cd $GITHUB_WORKSPACE - ./scripts/extract-changelog.sh $TAG_NAME > RELEASE_NOTES.md + cd "$GITHUB_WORKSPACE" + ./scripts/extract-changelog.sh "$TAG_NAME" > RELEASE_NOTES.md cat RELEASE_NOTES.md - name: Create release @@ -83,12 +85,12 @@ jobs: ls -R build/ TAG_NAME=${GITHUB_REF#refs/tags/} if [[ "$TAG_NAME" =~ .+-pre.* ]]; then - PRERELEASE_ARGS="--prerelease --latest=false" + PRERELEASE_ARGS=(--prerelease --latest=false) else - PRERELEASE_ARGS="" + PRERELEASE_ARGS=() fi gh release create "${{ github.ref_name }}" \ --notes-file RELEASE_NOTES.md \ --title "${{ github.ref_name }}" \ - $PRERELEASE_ARGS \ + "${PRERELEASE_ARGS[@]}" \ build/* diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index f603626b..40a7a361 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -12,7 +12,11 @@ on: - 'docs/**' - 'LICENSE' +permissions: + contents: read + env: + GOTOOLCHAIN: local go-modules: ./... ./cm/... wasm-tools-version: "1.230.0" wasmtime-version: "26.0.0" @@ -25,12 +29,12 @@ jobs: timeout-minutes: 2 steps: - name: Checkout repo - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: submodules: recursive - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v7 with: go-version-file: go.mod @@ -40,19 +44,20 @@ jobs: # Test with Go test-go: name: Test with Go - runs-on: ubuntu-latest - timeout-minutes: 15 + runs-on: ${{ matrix.os }} + timeout-minutes: 20 strategy: matrix: - go-version: ["1.23", "1.24"] + os: [ubuntu-latest, windows-latest] + go-version: ["1.26", "1.27"] steps: - name: Checkout repo - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: submodules: recursive - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v7 with: go-version: ${{ matrix.go-version }} @@ -77,24 +82,21 @@ jobs: timeout-minutes: 15 strategy: matrix: - go-version: ["1.23", "1.24"] - tinygo-version: ["0.35.0", "0.36.0", "0.37.0"] - exclude: - - go-version: "1.24" - tinygo-version: "0.35.0" + go-version: ["1.26", "1.27"] + tinygo-version: ["0.43.0"] steps: - name: Checkout repo - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: submodules: recursive - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v7 with: go-version: ${{ matrix.go-version }} - name: Set up TinyGo - uses: acifani/setup-tinygo@v2 + uses: acifani/setup-tinygo@v3 with: tinygo-version: ${{ matrix.tinygo-version }} @@ -111,24 +113,21 @@ jobs: timeout-minutes: 15 strategy: matrix: - go-version: ["1.23", "1.24"] - tinygo-version: ["0.35.0", "0.36.0", "0.37.0"] - exclude: - - go-version: "1.24" - tinygo-version: "0.35.0" + go-version: ["1.26", "1.27"] + tinygo-version: ["0.43.0"] steps: - name: Checkout repo - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: submodules: recursive - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v7 with: go-version: ${{ matrix.go-version }} - name: Set up TinyGo - uses: acifani/setup-tinygo@v2 + uses: acifani/setup-tinygo@v3 with: tinygo-version: ${{ matrix.tinygo-version }} @@ -144,11 +143,7 @@ jobs: version: ${{ env.wasmtime-version }} - name: Add Go wasm exec to $PATH - run: echo "$(go env GOROOT)/misc/wasm" >> $GITHUB_PATH - - # This moved in Go 1.24: https://github.com/golang/go/issues/68024 - - name: Add Go 1.24 wasm exec to $PATH - run: echo "$(go env GOROOT)/lib/wasm" >> $GITHUB_PATH + run: echo "$(go env GOROOT)/lib/wasm" >> "$GITHUB_PATH" - name: Regenerate Go run: make tests/generated diff --git a/go.mod b/go.mod index e3cf8eae..c99bc6f2 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,10 @@ module go.bytecodealliance.org -go 1.23.0 +go 1.26.0 require ( github.com/coreos/go-semver v0.3.1 - github.com/regclient/regclient v0.8.3 + github.com/regclient/regclient v0.11.5 github.com/sergi/go-diff v1.3.1 github.com/tetratelabs/wazero v1.9.0 github.com/urfave/cli/v3 v3.3.8 @@ -15,10 +15,10 @@ require ( require ( github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect - github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/compress v1.18.7 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect - github.com/ulikunitz/xz v0.5.12 // indirect + github.com/sirupsen/logrus v1.9.4 // indirect + github.com/ulikunitz/xz v0.5.15 // indirect golang.org/x/sync v0.14.0 // indirect - golang.org/x/sys v0.33.0 // indirect + golang.org/x/sys v0.45.0 // indirect ) diff --git a/go.sum b/go.sum index 55d8806d..7708fedc 100644 --- a/go.sum +++ b/go.sum @@ -5,51 +5,54 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4= github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= +github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= +github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= -github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/compress v1.18.7 h1:aUyZsS4kH3QTKurYhAOwAHxllVPnOthb3vPfnF1Ehjw= +github.com/klauspost/compress v1.18.7/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/olareg/olareg v0.1.2 h1:75G8X6E9FUlzL/CSjgFcYfMgNzlc7CxULpUUNsZBIvI= -github.com/olareg/olareg v0.1.2/go.mod h1:TWs+N6pO1S4bdB6eerzUm/ITRQ6kw91mVf9ZYeGtw+Y= +github.com/olareg/olareg v0.2.1 h1:RPHGIaqlVWPbKAsOYUj7e2WfEEW5M7F8I6hKMrwd4jU= +github.com/olareg/olareg v0.2.1/go.mod h1:dhr8QetC7U7jJ2m93oxDhEEOKCRbPgOK1oGyKfB4QNo= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/regclient/regclient v0.8.3 h1:AFAPu/vmOYGyY22AIgzdBUKbzH+83lEpRioRYJ/reCs= -github.com/regclient/regclient v0.8.3/go.mod h1:gjQh5uBVZoo/CngchghtQh9Hx81HOMKRRDd5WPcPkbk= +github.com/regclient/regclient v0.11.5 h1:OHRsXO0F3qHGfa4HEUv+EkMH9NXNcCTBKjNzyC/UhIA= +github.com/regclient/regclient v0.11.5/go.mod h1:DZUOfIT14WFTK2Pj4vjd93avy9O4Fdpjrf9ir23TbRE= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/sudo-bmitch/oci-digest v0.1.2 h1:are0qzWTsFZGZ3Uvdi9OSztJszSWaab6iqquMEEB7rw= +github.com/sudo-bmitch/oci-digest v0.1.2/go.mod h1:SH6l5OIe0islKBZBedjiPOeET/0QwGL+/oYfQt51uQo= github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I= github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM= -github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= -github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= +github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/urfave/cli/v3 v3.3.8 h1:BzolUExliMdet9NlJ/u4m5vHSotJ3PzEqSAZ1oPMa/E= github.com/urfave/cli/v3 v3.3.8/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo= go.bytecodealliance.org/cm v0.3.0 h1:VhV+4vjZPUGCozCg9+up+FNL3YU6XR+XKghk7kQ0vFc= go.bytecodealliance.org/cm v0.3.0/go.mod h1:JD5vtVNZv7sBoQQkvBvAAVKJPhR/bqBH7yYXTItMfZI= +golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= +golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/go.work b/go.work index d89b68f7..e2e8563d 100644 --- a/go.work +++ b/go.work @@ -1,4 +1,4 @@ -go 1.23.0 +go 1.26.0 use ( . diff --git a/internal/go/gen/module.go b/internal/go/gen/module.go index 1553a6a4..4b781bc6 100644 --- a/internal/go/gen/module.go +++ b/internal/go/gen/module.go @@ -35,17 +35,12 @@ func PackagePath(dir string) (string, error) { file = filepath.Join(dir, "go.mod") info, err := os.Stat(file) if err != nil { - if dir == "/" { + parent := filepath.Dir(dir) + if parent == dir { return "", errors.New("unable to locate a go.mod file") } - // Pop up to parent dir - var rest string - dir, rest = filepath.Split(dir) - if dir == "" { - return "", errors.New("unable to locate a go.mod file") - } - dir = filepath.Clean(dir) - subdirs = path.Join(rest, subdirs) + subdirs = path.Join(filepath.Base(dir), subdirs) + dir = parent continue } if info.IsDir() { diff --git a/internal/go/gen/module_test.go b/internal/go/gen/module_test.go index 227fb496..b577f009 100644 --- a/internal/go/gen/module_test.go +++ b/internal/go/gen/module_test.go @@ -2,6 +2,7 @@ package gen import ( "os" + "path/filepath" "testing" "go.bytecodealliance.org/internal/relpath" @@ -28,3 +29,34 @@ func TestPackagePath(t *testing.T) { t.Errorf("PackagePath(%q): expected error, got nil", tmp) } } + +func TestPackagePathNestedModule(t *testing.T) { + root, err := os.MkdirTemp("", t.Name()+"-") + if err != nil { + t.Fatal(err) + } + module := filepath.Join(root, "go.mod") + internal := filepath.Join(root, "internal") + nested := filepath.Join(internal, "bindings") + // TinyGo lacks recursive removal on Windows and WASI. + t.Cleanup(func() { + for _, path := range []string{nested, internal, module, root} { + if err := os.Remove(path); err != nil && !os.IsNotExist(err) { + t.Errorf("remove test fixture %s: %v", path, err) + } + } + }) + if err := os.WriteFile(module, []byte("module example.com/plugin\n\ngo 1.26.0\n"), 0o600); err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(nested, 0o700); err != nil { + t.Fatal(err) + } + got, err := PackagePath(nested) + if err != nil { + t.Fatal(err) + } + if want := "example.com/plugin/internal/bindings"; got != want { + t.Errorf("PackagePath(%q) = %q, want %q", nested, got, want) + } +} diff --git a/internal/witcli/dir_test.go b/internal/witcli/dir_test.go index c3ad69c2..44188bdd 100644 --- a/internal/witcli/dir_test.go +++ b/internal/witcli/dir_test.go @@ -4,6 +4,7 @@ import ( "errors" "io/fs" "os" + "os/exec" "path/filepath" "runtime" "strings" @@ -11,9 +12,13 @@ import ( ) func TestFindOrCreateDirFind(t *testing.T) { - temp := t.TempDir() + temp, err := os.MkdirTemp("", t.Name()+"-") + if err != nil { + t.Fatal(err) + } dir := filepath.Join(temp, "existing") + cleanupEmptyDirs(t, dir, temp) if err := os.Mkdir(dir, os.ModeDir|os.ModePerm); err != nil { t.Fatal(err) } @@ -47,9 +52,20 @@ func TestFindOrCreateDirFind(t *testing.T) { } func TestFindOrCreateDirCreate(t *testing.T) { - temp := t.TempDir() - + temp, err := os.MkdirTemp("", t.Name()+"-") + if err != nil { + t.Fatal(err) + } + control := filepath.Join(temp, "control") dir := filepath.Join(temp, "new") + cleanupEmptyDirs(t, dir, control, temp) + if err := os.Mkdir(control, 0o755); err != nil { + t.Fatal(err) + } + controlInfo, err := os.Stat(control) + if err != nil { + t.Fatal(err) + } if _, err := os.Stat(dir); !os.IsNotExist(err) { t.Errorf("Expected %s to not exist, but it did.", dir) @@ -59,6 +75,16 @@ func TestFindOrCreateDirCreate(t *testing.T) { if err != nil { t.Fatal(err) } + if info == nil || !info.IsDir() { + t.Fatalf("FindOrCreateDir returned non-directory information: %v", info) + } + createdInfo, err := os.Stat(dir) + if err != nil { + t.Fatal(err) + } + if !createdInfo.IsDir() { + t.Fatal("FindOrCreateDir did not create a directory") + } // Skip remaining tests on incompatible platforms if runtime.Compiler == "tinygo" || strings.Contains(runtime.GOARCH, "wasm") { @@ -66,12 +92,25 @@ func TestFindOrCreateDirCreate(t *testing.T) { } gotPerm := info.Mode().Perm() - wantPerm := os.FileMode(0o755) + // Compare filesystem-applied permissions, including umask and Windows mode mapping. + wantPerm := controlInfo.Mode().Perm() if gotPerm != wantPerm { - t.Errorf("FindOrCreateDirExisting returned directory with file permissions %v; want %v", gotPerm, wantPerm) + t.Errorf("FindOrCreateDir returned directory with file permissions %v; want %v", gotPerm, wantPerm) } } +// TinyGo lacks recursive removal on Windows and WASI; remove owned leaves first. +func cleanupEmptyDirs(t *testing.T, paths ...string) { + t.Helper() + t.Cleanup(func() { + for _, path := range paths { + if err := os.Remove(path); err != nil && !errors.Is(err, fs.ErrNotExist) { + t.Errorf("remove test directory %s: %v", path, err) + } + } + }) +} + func TestFindOrCreateDirPermissionDenied(t *testing.T) { // Skip test on incompatible platforms if runtime.Compiler == "tinygo" || strings.Contains(runtime.GOARCH, "wasm") { @@ -81,14 +120,38 @@ func TestFindOrCreateDirPermissionDenied(t *testing.T) { temp := t.TempDir() protected := filepath.Join(temp, "protected") - if err := os.Mkdir(protected, os.FileMode(0o400)); err != nil { + if err := os.Mkdir(protected, 0o755); err != nil { t.Fatal(err) } + if runtime.GOOS == "windows" { + // Windows directory creation uses ACLs; chmod does not remove this permission. + icacls := filepath.Join(os.Getenv("SystemRoot"), "System32", "icacls.exe") + if output, err := exec.Command(icacls, protected, "/deny", "*S-1-1-0:(AD)").CombinedOutput(); err != nil { + t.Fatalf("deny subdirectory creation: %v\n%s", err, output) + } + t.Cleanup(func() { + if output, err := exec.Command(icacls, protected, "/remove:d", "*S-1-1-0").CombinedOutput(); err != nil { + t.Errorf("restore directory permissions: %v\n%s", err, output) + } + }) + } else { + if err := os.Chmod(protected, 0o400); err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if err := os.Chmod(protected, 0o755); err != nil { + t.Errorf("restore directory permissions: %v", err) + } + }) + } dir := filepath.Join(protected, "permission-denied") - _, err := FindOrCreateDir(dir) + info, err := FindOrCreateDir(dir) if !errors.Is(err, fs.ErrPermission) { t.Errorf("Expected creation within protected directory to fail, but it did not: %v", err) } + if info != nil { + t.Errorf("failed creation returned file information: %v", info) + } } diff --git a/wit/bindgen/testdata_test.go b/wit/bindgen/testdata_test.go index 5073c486..3ca11773 100644 --- a/wit/bindgen/testdata_test.go +++ b/wit/bindgen/testdata_test.go @@ -9,7 +9,6 @@ import ( "io/fs" "os" "os/exec" - "path" "path/filepath" "strings" "sync" @@ -86,7 +85,7 @@ func validateGeneratedGo(t *testing.T, res *wit.Resolve, origin string) { return } - dir := path.Join(generatedPath, origin) + dir := filepath.Join(generatedPath, origin) err := os.MkdirAll(dir, fs.ModePerm) if err != nil { t.Error(err) @@ -218,8 +217,12 @@ func TestGenerateTestdata(t *testing.T) { return } err := loadTestdata(func(path string, res *wit.Resolve) error { + relative, err := filepath.Rel(testdataPath, path) + if err != nil { + return err + } t.Run(path, func(t *testing.T) { - origin := strings.TrimSuffix(strings.TrimPrefix(path, testdataPath), ".wit.json") + origin := strings.TrimSuffix(relative, ".wit.json") validateGeneratedGo(t, res, origin) }) return nil diff --git a/wit/load.go b/wit/load.go index 342bc994..67da87c2 100644 --- a/wit/load.go +++ b/wit/load.go @@ -61,9 +61,9 @@ func loadWIT(path string, reader io.Reader) (*Resolve, error) { if path != "" { path = strings.TrimPrefix(path, "./") - args = append(args, path) + args = append(args, filepath.ToSlash(path)) dir := filepath.Dir(path) - fsMap[dir] = os.DirFS(dir) + fsMap[filepath.ToSlash(dir)] = os.DirFS(dir) } else { stdin = reader } @@ -71,6 +71,7 @@ func loadWIT(path string, reader io.Reader) (*Resolve, error) { if err != nil { return nil, err } + defer wasmTools.Close(ctx) stdout := &bytes.Buffer{} stderr := &bytes.Buffer{} err = wasmTools.Run(ctx, stdin, stdout, stderr, fsMap, args...) diff --git a/wit/load_test.go b/wit/load_test.go new file mode 100644 index 00000000..08a73086 --- /dev/null +++ b/wit/load_test.go @@ -0,0 +1,83 @@ +//go:build !tinygo + +package wit + +import ( + "os" + "path/filepath" + "testing" +) + +func TestLoadWITPaths(t *testing.T) { + // Relative paths require the fixture and working directory to share a volume. + root, err := os.MkdirTemp(".", ".wit-load-paths-*") + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if err := os.RemoveAll(root); err != nil { + t.Error(err) + } + }) + root, err = filepath.Abs(root) + if err != nil { + t.Fatal(err) + } + file := filepath.Join(root, "nested", "example.wit") + if err := os.MkdirAll(filepath.Dir(file), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(file, []byte("package example:paths; world example {}"), 0o644); err != nil { + t.Fatal(err) + } + cwd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + rel, err := filepath.Rel(cwd, file) + if err != nil { + t.Fatal(err) + } + for _, input := range []struct { + name string + path string + }{ + {"absolute", file}, + {"absolute slash", filepath.ToSlash(file)}, + {"relative", rel}, + {"relative slash", filepath.ToSlash(rel)}, + } { + t.Run(input.name, func(t *testing.T) { + resolved, err := LoadWIT(input.path) + if err != nil { + t.Fatal(err) + } + if len(resolved.Worlds) != 1 || resolved.Worlds[0].Name != "example" { + t.Fatalf("resolved worlds: %v", resolved.Worlds) + } + }) + } +} + +func TestLoadWITDirectoryDependencies(t *testing.T) { + root := t.TempDir() + dependency := filepath.Join(root, "deps", "dep") + if err := os.MkdirAll(dependency, 0o755); err != nil { + t.Fatal(err) + } + for name, text := range map[string]string{ + filepath.Join(root, "main.wit"): "package example:paths; world example { import example:dep/api; }", + filepath.Join(dependency, "api.wit"): "package example:dep; interface api { ping: func(); }", + } { + if err := os.WriteFile(name, []byte(text), 0o644); err != nil { + t.Fatal(err) + } + } + resolved, err := LoadWIT(root) + if err != nil { + t.Fatal(err) + } + if len(resolved.Packages) != 2 || len(resolved.Worlds) != 1 || resolved.Worlds[0].Name != "example" { + t.Fatalf("resolved packages/worlds: %v / %v", resolved.Packages, resolved.Worlds) + } +} diff --git a/wit/load_tinygo_test.go b/wit/load_tinygo_test.go new file mode 100644 index 00000000..1cd78c25 --- /dev/null +++ b/wit/load_tinygo_test.go @@ -0,0 +1,25 @@ +//go:build tinygo + +package wit + +import ( + "strings" + "testing" +) + +func TestLoadWITUnsupportedBackend(t *testing.T) { + for _, input := range []struct { + name string + load func() (*Resolve, error) + }{ + {"file", func() (*Resolve, error) { return LoadWIT("example.wit") }}, + {"reader", func() (*Resolve, error) { return DecodeWIT(strings.NewReader("package example:paths;")) }}, + } { + t.Run(input.name, func(t *testing.T) { + resolved, err := input.load() + if resolved != nil || err == nil || !strings.Contains(err.Error(), "wasm-tools disabled under TinyGo") { + t.Fatalf("expected disabled-backend error, got %v, %v", resolved, err) + } + }) + } +} diff --git a/x/cabi/realloc.go b/x/cabi/realloc.go index 6baf1333..3e4c6f8a 100644 --- a/x/cabi/realloc.go +++ b/x/cabi/realloc.go @@ -7,12 +7,15 @@ import "unsafe" // // Note: the use of uintptr assumes 32-bit pointers when compiled for wasm or wasm32. func realloc(ptr unsafe.Pointer, size, align, newsize uintptr) unsafe.Pointer { - if newsize <= size { - return unsafe.Add(ptr, offset(uintptr(ptr), align)) + if newsize == 0 { + return nil + } + if newsize <= size && offset(uintptr(ptr), align) == 0 { + return ptr } newptr := alloc(newsize, align) if size > 0 { - copy(unsafe.Slice((*byte)(newptr), newsize), unsafe.Slice((*byte)(ptr), size)) + copy(unsafe.Slice((*byte)(newptr), newsize), unsafe.Slice((*byte)(ptr), min(size, newsize))) } return newptr } @@ -20,30 +23,27 @@ func realloc(ptr unsafe.Pointer, size, align, newsize uintptr) unsafe.Pointer { // offset returns the delta between the aligned value of ptr and ptr // so it can be passed to unsafe.Add. The return value is guaranteed to be >= 0. func offset(ptr, align uintptr) uintptr { - newptr := (ptr + align - 1) &^ (align - 1) - return newptr - ptr + return -ptr & (allocationAlignment(align) - 1) } // alloc allocates a block of memory with size bytes. -// It attempts to align the allocated memory by allocating a slice of -// a type that matches the desired alignment. It aligns to 16 bytes for -// values of align other than 1, 2, 4, or 8. +// Padding allows alignment without reducing the usable allocation. +// Values of align other than 1, 2, 4, or 8 select 16-byte alignment. func alloc(size, align uintptr) unsafe.Pointer { + padding := allocationAlignment(align) - 1 + if size > uintptr(^uint(0)>>1)-padding { + panic("cabi allocation size overflows int") + } + s := make([]byte, max(size, 1)+padding) + ptr := unsafe.Pointer(unsafe.SliceData(s)) + return unsafe.Add(ptr, offset(uintptr(ptr), align)) +} + +func allocationAlignment(align uintptr) uintptr { switch align { - case 1: - s := make([]uint8, size) - return unsafe.Pointer(unsafe.SliceData(s)) - case 2: - s := make([]uint16, min(size/align, 1)) - return unsafe.Pointer(unsafe.SliceData(s)) - case 4: - s := make([]uint32, min(size/align, 1)) - return unsafe.Pointer(unsafe.SliceData(s)) - case 8: - s := make([]uint64, min(size/align, 1)) - return unsafe.Pointer(unsafe.SliceData(s)) + case 1, 2, 4, 8: + return align default: - s := make([][2]uint64, min(size/align, 1)) - return unsafe.Pointer(unsafe.SliceData(s)) + return 16 } } diff --git a/x/cabi/realloc_test.go b/x/cabi/realloc_test.go index 84d2fc8e..c8e711ae 100644 --- a/x/cabi/realloc_test.go +++ b/x/cabi/realloc_test.go @@ -1,72 +1,118 @@ package cabi import ( + "fmt" "slices" "testing" "unsafe" ) -func TestRealloc(t *testing.T) { - const threshold = 16 - - tests := []struct { - name string - ptr uintptr - size uintptr - align uintptr - newsize uintptr - want uintptr - }{ - {"nil", 0, 0, 1, 0, 0}, - {"nil with align", 0, 0, 2, 0, 0}, - {"align to 2", 1, 0, 2, 0, 2}, - {"align to 2", 1, 0, 2, 0, 2}, - {"align to 4", 1, 0, 4, 0, 4}, - {"align to 8", 3, 0, 8, 0, 8}, - {"align to 16", 9, 0, 16, 0, 16}, - {"alloc 100 bytes", 0, 0, 1, 100, sliceData(hundred)}, - {"preserve 5 bytes", stringData("hello"), 5, 1, 5, stringData("hello")}, - {"shrink 8 bytes to 4", stringData("fourfour"), 8, 1, 4, stringData("four")}, - {"expand 4 bytes to 8", stringData("zero"), 4, 1, 8, stringData("zero\u0000\u0000\u0000\u0000")}, - {"cut down lorem ipsum", stringData(lorem), 4, 1, 200, stringData(lorem[:200])}, +func TestOffset(t *testing.T) { + for _, align := range []uintptr{1, 2, 4, 8, 16} { + for _, ptr := range []uintptr{0, 1, 3, 9, 16, ^uintptr(0) - 1, ^uintptr(0)} { + want := (align - ptr%align) % align + if got := offset(ptr, align); got != want { + t.Errorf("offset(%d, %d) = %d, want %d", ptr, align, got, want) + } + } } +} - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := realloc(unsafePointer(tt.ptr), tt.size, tt.align, tt.newsize) - if (tt.want < threshold && uintptr(got) != tt.want) || (tt.want >= threshold && uintptr(got) < threshold) { - t.Errorf("realloc(%d, %d, %d, %d): %d, expected %d", - tt.ptr, tt.size, tt.align, tt.newsize, got, tt.want) - } - if uintptr(got)%tt.align != 0 { - t.Errorf("expected ptr aligned to %d, got %d", tt.align, uintptr(got)&tt.align) - } - if uintptr(got) < threshold { - return // it didn’t allocate - } - if tt.ptr == 0 { - wants := unsafe.Slice((*byte)(unsafePointer(tt.want)), tt.newsize) - gots := unsafe.Slice((*byte)(got), tt.newsize) - if slices.Compare(wants, gots) != 0 { - t.Errorf("expected %v, got %v", wants, gots) +func TestAlloc(t *testing.T) { + for _, align := range []uintptr{0, 1, 2, 3, 4, 8, 16, 32} { + wantAlign := align + if align != 1 && align != 2 && align != 4 && align != 8 { + wantAlign = 16 + } + for _, size := range []uintptr{0, 1, 3, 7, 9, 17, 100, 257} { + t.Run(fmt.Sprintf("align%d/size%d", align, size), func(t *testing.T) { + ptr := alloc(size, align) + if uintptr(ptr)%wantAlign != 0 { + t.Fatalf("allocation %p is not aligned to %d", ptr, wantAlign) } - } - }) + span := unsafe.Slice((*byte)(ptr), size) + for i := range span { + span[i] = byte(i + 1) + } + for i, got := range span { + if want := byte(i + 1); got != want { + t.Fatalf("byte %d = %d, want %d", i, got, want) + } + } + }) + } } } -var hundred = make([]byte, 100) -var lorem = `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut volutpat arcu eu est tristique suscipit. Nulla laoreet purus magna, at feugiat tortor fermentum non. Integer semper et magna id placerat. Quisque purus lorem, mollis vel convallis eu, ullamcorper sit amet justo. Duis tempus gravida lacus, vel dapibus augue. Nunc sed condimentum lacus. Cras vulputate cursus dictum. Etiam felis metus, volutpat id luctus ac, ultrices nec metus. Proin sagittis nulla a pretium sagittis. Nullam tristique sapien sed semper faucibus. Fusce condimentum nulla dui. Donec egestas nunc in blandit mollis.` - -// Appease vet, see https://github.com/golang/go/issues/58625 -func unsafePointer(p uintptr) unsafe.Pointer { - return *(*unsafe.Pointer)(unsafe.Pointer(&p)) +func TestAllocOverflow(t *testing.T) { + for _, align := range []uintptr{1, 2, 4, 8, 16} { + t.Run(fmt.Sprintf("align%d", align), func(t *testing.T) { + defer func() { + if recover() == nil { + t.Error("oversized allocation did not panic") + } + }() + alloc(^uintptr(0), align) + }) + } } -func sliceData[T any](s []T) uintptr { - return uintptr(unsafe.Pointer(unsafe.SliceData(s))) +func TestRealloc(t *testing.T) { + for _, align := range []uintptr{1, 2, 4, 8, 16} { + for _, sizes := range [][2]uintptr{{0, 0}, {0, 100}, {17, 17}, {17, 7}, {7, 100}, {17, 0}} { + size, newsize := sizes[0], sizes[1] + t.Run(fmt.Sprintf("align%d/%dto%d", align, size, newsize), func(t *testing.T) { + var ptr unsafe.Pointer + if size > 0 { + ptr = alloc(size, align) + } + original := unsafe.Slice((*byte)(ptr), size) + for i := range original { + original[i] = byte(i + 1) + } + want := slices.Clone(original[:min(size, newsize)]) + got := realloc(ptr, size, align, newsize) + if newsize == 0 { + if got != nil { + t.Fatalf("zero-sized reallocation returned %p", got) + } + return + } + if uintptr(got)%align != 0 { + t.Fatalf("reallocation %p is not aligned to %d", got, align) + } + span := unsafe.Slice((*byte)(got), newsize) + if !slices.Equal(span[:len(want)], want) { + t.Fatalf("reallocation prefix = %v, want %v", span[:len(want)], want) + } + for i := range span { + span[i] = byte(i + 1) + } + if newsize <= size && got != ptr { + t.Error("aligned shrinking allocation moved") + } + }) + } + } } -func stringData(s string) uintptr { - return uintptr(unsafe.Pointer(unsafe.StringData(s))) +func TestReallocRealignedShrink(t *testing.T) { + for _, align := range []uintptr{2, 4, 8, 16} { + t.Run(fmt.Sprintf("align%d", align), func(t *testing.T) { + ptr := unsafe.Add(alloc(33, align), 1) + original := unsafe.Slice((*byte)(ptr), 32) + for i := range original { + original[i] = byte(i + 1) + } + want := slices.Clone(original[:17]) + got := realloc(ptr, 32, align, 17) + if uintptr(got)%align != 0 { + t.Fatalf("reallocation %p is not aligned to %d", got, align) + } + span := unsafe.Slice((*byte)(got), 17) + if !slices.Equal(span, want) { + t.Fatalf("realigned allocation = %v, want %v", span, want) + } + }) + } }