This is an automated email from the ASF dual-hosted git repository.

kezhenxu94 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/skywalking-eyes.git


The following commit(s) were added to refs/heads/main by this push:
     new 5537368  fix: compatibility issue with Node.js 24 (#252)
5537368 is described below

commit 55373684d1b70e5f8fd9fc8ec114a89ad11a56a3
Author: XL Zhao <[email protected]>
AuthorDate: Mon Dec 22 20:31:28 2025 +0800

    fix: compatibility issue with Node.js 24 (#252)
    
    closes apache/skywalking#13517
---
 pkg/deps/npm.go                              | 141 +++++++++++++++++
 pkg/deps/npm_resolver_cross_platform_test.go | 216 +++++++++++++++++++++++++++
 pkg/deps/result.go                           |   1 +
 3 files changed, 358 insertions(+)

diff --git a/pkg/deps/npm.go b/pkg/deps/npm.go
index b50c74c..0579158 100644
--- a/pkg/deps/npm.go
+++ b/pkg/deps/npm.go
@@ -26,6 +26,8 @@ import (
        "os"
        "os/exec"
        "path/filepath"
+       "regexp"
+       "runtime"
        "strings"
        "time"
 
@@ -33,6 +35,58 @@ import (
        "github.com/apache/skywalking-eyes/pkg/logger"
 )
 
+// Constants for architecture names to avoid string duplication
+const (
+       archAMD64 = "amd64"
+       archARM64 = "arm64"
+       archARM   = "arm"
+)
+
+// Cross-platform package pattern recognition (for precise matching)
+// These patterns work for both scoped (@scope/package-platform-arch) and
+// non-scoped (package-platform-arch) npm packages, as the platform/arch
+// suffix always appears at the end of the package name.
+// Examples:
+//   - Scoped:    @scope/foo-linux-x64
+//   - Non-scoped: foo-linux-x64
+//
+// regex: matches package names ending with a specific string (e.g., 
"-linux-x64")
+// os: target operating system (e.g., "linux", "darwin", "windows")
+// arch: target CPU architecture (e.g., "x64", "arm64")
+var platformPatterns = []struct {
+       regex *regexp.Regexp
+       os    string
+       arch  string
+}{
+       // Android
+       {regexp.MustCompile(`-android-arm64$`), "android", archARM64},
+       {regexp.MustCompile(`-android-arm$`), "android", archARM},
+       {regexp.MustCompile(`-android-x64$`), "android", "x64"},
+
+       // Darwin (macOS)
+       {regexp.MustCompile(`-darwin-arm64$`), "darwin", archARM64},
+       {regexp.MustCompile(`-darwin-x64$`), "darwin", "x64"},
+
+       // Linux
+       {regexp.MustCompile(`-linux-arm64-glibc$`), "linux", archARM64},
+       {regexp.MustCompile(`-linux-arm64-musl$`), "linux", archARM64},
+       {regexp.MustCompile(`-linux-arm-glibc$`), "linux", archARM},
+       {regexp.MustCompile(`-linux-arm-musl$`), "linux", archARM},
+       {regexp.MustCompile(`-linux-x64-glibc$`), "linux", "x64"},
+       {regexp.MustCompile(`-linux-x64-musl$`), "linux", "x64"},
+       {regexp.MustCompile(`-linux-x64$`), "linux", "x64"},
+       {regexp.MustCompile(`-linux-arm64$`), "linux", archARM64},
+       {regexp.MustCompile(`-linux-arm$`), "linux", archARM},
+
+       // Windows
+       {regexp.MustCompile(`-win32-arm64$`), "windows", archARM64},
+       {regexp.MustCompile(`-win32-ia32$`), "windows", "ia32"},
+       {regexp.MustCompile(`-win32-x64$`), "windows", "x64"},
+
+       // FreeBSD
+       {regexp.MustCompile(`-freebsd-x64$`), "freebsd", "x64"},
+}
+
 type NpmResolver struct {
        Resolver
 }
@@ -87,6 +141,8 @@ func (resolver *NpmResolver) Resolve(pkgFile string, config 
*ConfigDeps, report
        for _, pkg := range pkgs {
                if result := resolver.ResolvePackageLicense(pkg.Name, pkg.Path, 
config); result.LicenseSpdxID != "" {
                        report.Resolve(result)
+               } else if result.IsCrossPlatform {
+                       logger.Log.Warnf("Skipping cross-platform package %s 
(not for current platform %s %s)", pkg.Name, runtime.GOOS, runtime.GOARCH)
                } else {
                        result.LicenseSpdxID = Unknown
                        report.Skip(result)
@@ -198,6 +254,12 @@ func (resolver *NpmResolver) 
ResolvePackageLicense(pkgName, pkgPath string, conf
        result := &Result{
                Dependency: pkgName,
        }
+
+       if !resolver.isForCurrentPlatform(pkgName) {
+               result.IsCrossPlatform = true
+               return result
+       }
+
        // resolve from the package.json file
        if err := resolver.ResolvePkgFile(result, pkgPath, config); err != nil {
                result.ResolveErrors = append(result.ResolveErrors, err)
@@ -318,3 +380,82 @@ func (resolver *NpmResolver) ParsePkgFile(pkgFile string) 
(*Package, error) {
        }
        return &packageInfo, nil
 }
+
+// normalizeArch converts various architecture aliases into Go's canonical 
naming.
+func normalizeArch(arch string) string {
+       // Convert to lowercase to handle case variations (e.g., "AMD64").
+       arch = strings.ToLower(arch)
+       switch arch {
+       // x86-64 family (64-bit Intel/AMD)
+       case "x64", "x86_64", "amd64", "x86-64":
+               return archAMD64
+       // x86 32-bit family (legacy)
+       case "ia32", "x86", "386", "i386", "i686":
+               return "386"
+       // ARM 64-bit
+       case "arm64", "aarch64":
+               return archARM64
+       // ARM 32-bit
+       case "arm", "armv7", "armhf", "armv7l", "armel":
+               return archARM
+       // Unknown architecture: return as-is (alternatively, could return 
empty to indicate incompatibility).
+       default:
+               return arch
+       }
+}
+
+// analyzePackagePlatform extracts the target OS and architecture from a 
package name.
+func (resolver *NpmResolver) analyzePackagePlatform(pkgName string) (pkgOS, 
pkgArch string, partial bool) {
+       for _, pattern := range platformPatterns {
+               if pattern.regex.MatchString(pkgName) {
+                       return pattern.os, pattern.arch, false
+               }
+       }
+
+       // Detect OS-only suffixes like "-linux", "-darwin", "-win32"
+       osOnlyPatterns := []string{
+               "-linux",
+               "-darwin",
+               "-win32",
+               "-windows",
+               "-android",
+               "-freebsd",
+       }
+       for _, osSuffix := range osOnlyPatterns {
+               if strings.HasSuffix(pkgName, osSuffix) {
+                       return strings.TrimPrefix(osSuffix, "-"), "", true
+               }
+       }
+
+       return "", "", false
+}
+
+// isForCurrentPlatform checks whether the package is intended for the current 
OS and architecture.
+func (resolver *NpmResolver) isForCurrentPlatform(pkgName string) bool {
+       pkgOS, pkgArch, partial := resolver.analyzePackagePlatform(pkgName)
+
+       // OS-only package: explicitly reject with friendly error
+       if partial {
+               logger.Log.Warnf(
+                       "Package %q declares a platform without architecture. "+
+                               "Please use a full platform-arch suffix (e.g. 
-linux-x64, -darwin-arm64).",
+                       pkgName,
+               )
+               return false
+       }
+
+       // Universal package
+       if pkgOS == "" && pkgArch == "" {
+               return true
+       }
+
+       currentOS := runtime.GOOS
+       currentArch := runtime.GOARCH
+
+       return pkgOS == currentOS && resolver.isArchCompatible(pkgArch, 
currentArch)
+}
+
+// isArchCompatible determines whether the package's architecture is 
compatible with the current machine's architecture.
+func (resolver *NpmResolver) isArchCompatible(pkgArch, currentArch string) 
bool {
+       return normalizeArch(pkgArch) == normalizeArch(currentArch)
+}
diff --git a/pkg/deps/npm_resolver_cross_platform_test.go 
b/pkg/deps/npm_resolver_cross_platform_test.go
new file mode 100644
index 0000000..4c363a5
--- /dev/null
+++ b/pkg/deps/npm_resolver_cross_platform_test.go
@@ -0,0 +1,216 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package deps_test
+
+import (
+       "os"
+       "path/filepath"
+       "runtime"
+       "strings"
+       "testing"
+
+       "github.com/apache/skywalking-eyes/pkg/deps"
+)
+
+const (
+       npmLicenseMIT      = "MIT"
+       npmLicenseApache20 = "Apache-2.0"
+)
+
+// TC-NEW-001
+// Regression test: cross-platform npm binary packages must be skipped safely.
+func TestResolvePackageLicense_SkipCrossPlatformPackages(t *testing.T) {
+       resolver := &deps.NpmResolver{}
+       cfg := &deps.ConfigDeps{}
+
+       var crossPlatformPkgs []string
+       switch runtime.GOOS {
+       case "linux":
+               crossPlatformPkgs = []string{
+                       "@parcel/watcher-darwin-arm64",
+                       "@parcel/watcher-win32-x64",
+               }
+       case "darwin":
+               crossPlatformPkgs = []string{
+                       "@parcel/watcher-linux-x64",
+                       "@parcel/watcher-win32-x64",
+               }
+       default: // windows
+               crossPlatformPkgs = []string{
+                       "@parcel/watcher-linux-x64",
+               }
+       }
+
+       for _, pkg := range crossPlatformPkgs {
+               pkg := pkg // capture loop variable
+
+               t.Run(pkg+"/path-not-exist", func(t *testing.T) {
+                       // 001-A: cross-platform + path not exist
+                       result := resolver.ResolvePackageLicense(pkg, 
"/non/existent/path", cfg)
+                       if result.LicenseSpdxID != "" {
+                               t.Fatalf(
+                                       "expected empty license for 
cross-platform package %q, got %q",
+                                       pkg,
+                                       result.LicenseSpdxID,
+                               )
+                       }
+               })
+
+               t.Run(pkg+"/package-json-exists", func(t *testing.T) {
+                       // 001-B: cross-platform + package.json exists
+                       tmp := t.TempDir()
+                       err := os.WriteFile(
+                               filepath.Join(tmp, "package.json"),
+                               
[]byte("{\"name\":\"fake-cross-platform\",\"license\":\""+npmLicenseMIT+"\"}"),
+                               0o600,
+                       )
+                       if err != nil {
+                               t.Fatal(err)
+                       }
+
+                       result := resolver.ResolvePackageLicense(pkg, tmp, cfg)
+                       if result.LicenseSpdxID != "" {
+                               t.Fatalf(
+                                       "expected empty license for 
cross-platform package %q, got %q",
+                                       pkg,
+                                       result.LicenseSpdxID,
+                               )
+                       }
+               })
+
+               t.Run(pkg+"/valid-license", func(t *testing.T) {
+                       // 001-C: cross-platform + valid SPDX license
+                       tmp := t.TempDir()
+                       err := os.WriteFile(
+                               filepath.Join(tmp, "package.json"),
+                               
[]byte("{\"name\":\"fake-cross-platform\",\"license\":\""+npmLicenseApache20+"\"}"),
+                               0o600,
+                       )
+                       if err != nil {
+                               t.Fatal(err)
+                       }
+
+                       result := resolver.ResolvePackageLicense(pkg, tmp, cfg)
+                       if result.LicenseSpdxID != "" {
+                               t.Fatalf(
+                                       "expected empty license for 
cross-platform package %q, got %q",
+                                       pkg,
+                                       result.LicenseSpdxID,
+                               )
+                       }
+               })
+       }
+}
+
+// TC-NEW-002
+// Functional test: current-platform packages should be resolved normally.
+func TestResolvePackageLicense_CurrentPlatformPackages(t *testing.T) {
+       resolver := &deps.NpmResolver{}
+       cfg := &deps.ConfigDeps{}
+
+       t.Run("normal package with license field", func(t *testing.T) {
+               tmp := t.TempDir()
+               err := os.WriteFile(
+                       filepath.Join(tmp, "package.json"),
+                       
[]byte("{\"name\":\"normal-pkg\",\"license\":\""+npmLicenseApache20+"\"}"),
+                       0o600,
+               )
+               if err != nil {
+                       t.Fatal(err)
+               }
+
+               result := resolver.ResolvePackageLicense("normal-pkg", tmp, cfg)
+               if result.LicenseSpdxID != npmLicenseApache20 {
+                       t.Fatalf(
+                               "expected license %s, got %q",
+                               npmLicenseApache20,
+                               result.LicenseSpdxID,
+                       )
+               }
+       })
+
+       t.Run("package without license field", func(t *testing.T) {
+               tmp := t.TempDir()
+               err := os.WriteFile(
+                       filepath.Join(tmp, "package.json"),
+                       []byte("{\"name\":\"no-license-pkg\"}"),
+                       0o600,
+               )
+               if err != nil {
+                       t.Fatal(err)
+               }
+
+               result := resolver.ResolvePackageLicense("no-license-pkg", tmp, 
cfg)
+               if result.LicenseSpdxID != "" {
+                       t.Fatalf(
+                               "expected empty license, got %q",
+                               result.LicenseSpdxID,
+                       )
+               }
+       })
+}
+
+// TC-NEW-003
+// Stability & defensive tests: malformed inputs must never cause panic.
+func TestResolvePackageLicense_DefensiveScenarios(t *testing.T) {
+       resolver := &deps.NpmResolver{}
+       cfg := &deps.ConfigDeps{}
+
+       t.Run("non-existent path", func(_ *testing.T) {
+               _ = resolver.ResolvePackageLicense("some-pkg", 
"/definitely/not/exist", cfg)
+       })
+
+       t.Run("malformed package.json", func(t *testing.T) {
+               tmp := t.TempDir()
+               err := os.WriteFile(
+                       filepath.Join(tmp, "package.json"),
+                       []byte("{\"name\":\"bad-json\",\"license\":"),
+                       0o600,
+               )
+               if err != nil {
+                       t.Fatal(err)
+               }
+               _ = resolver.ResolvePackageLicense("bad-json", tmp, cfg)
+       })
+
+       t.Run("invalid license field type", func(t *testing.T) {
+               tmp := t.TempDir()
+               err := os.WriteFile(
+                       filepath.Join(tmp, "package.json"),
+                       []byte("{\"name\":\"weird-pkg\",\"license\":123}"),
+                       0o600,
+               )
+               if err != nil {
+                       t.Fatal(err)
+               }
+               _ = resolver.ResolvePackageLicense("weird-pkg", tmp, cfg)
+       })
+
+       t.Run("empty package name", func(_ *testing.T) {
+               _ = resolver.ResolvePackageLicense("", "/not/exist", cfg)
+       })
+
+       t.Run("overly long package name", func(_ *testing.T) {
+               longName := strings.Repeat("a", 10_000)
+               _ = resolver.ResolvePackageLicense(longName, "/not/exist", cfg)
+       })
+
+       t.Run("path traversal-like package name", func(_ *testing.T) {
+               _ = resolver.ResolvePackageLicense("../../../../etc/passwd", 
"/not/exist", cfg)
+       })
+}
diff --git a/pkg/deps/result.go b/pkg/deps/result.go
index 66f87df..1c3f645 100644
--- a/pkg/deps/result.go
+++ b/pkg/deps/result.go
@@ -38,6 +38,7 @@ type Result struct {
        LicenseSpdxID   string
        ResolveErrors   []error
        Version         string
+       IsCrossPlatform bool
 }
 
 // Report is a collection of resolved Result.

Reply via email to