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
8 changes: 8 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ unity-packager is a Go CLI tool that downloads upstream packages (git repos, NuG
- **nuget**: NuGet packages (download `.nupkg`, extract DLLs into `Plugins/`)
- **archive**: HTTP zip/tar.gz/tgz archives (e.g., Firebase Unity SDK); auto-detects Unity vs raw

## PR and Merge Policy

Before merging any PR, always wait for:
1. CI checks to pass
2. All Copilot review comments to be addressed

Do not merge until both are satisfied, unless the user explicitly instructs you to skip this.

## Build and Test

```bash
Expand Down
45 changes: 29 additions & 16 deletions internal/packager/legalfiles.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,30 @@ import (

// legalFilePatterns are base names (case-insensitive) of files that should always
// be included in the package root, regardless of exclude filters.
var legalFilePatterns = []string{
// legalFileBaseNames are the base names (without extension) of files that should
// always be included. legalFileExtensions are the extensions to match. The full
// pattern list is generated as the cross product of these two sets, plus bare names.
var legalFileBaseNames = []string{
"license",
"licence",
"license.md",
"licence.md",
"license.txt",
"licence.txt",
"license.rst",
"licence.rst",
"readme",
"readme.md",
"readme.txt",
"readme.rst",
"notice",
"notice.md",
"notice.txt",
"third-party-notices",
"third-party-notices.md",
"third-party-notices.txt",
"thirdpartynotices",
"thirdpartynotices.txt",
}

var legalFileExtensions = []string{"", ".md", ".txt", ".rst"}

var legalFilePatterns = generateLegalFilePatterns()

func generateLegalFilePatterns() []string {
var patterns []string
for _, base := range legalFileBaseNames {
for _, ext := range legalFileExtensions {
patterns = append(patterns, base+ext)
}
}
return patterns
}

// isLegalFile returns true if the filename (base name only) matches a known
Expand Down Expand Up @@ -66,6 +69,8 @@ func CopyLegalFiles(srcDir, destDir string) error {
// Don't overwrite if already present (e.g., copied by CopyFiltered)
if _, err := os.Stat(destPath); err == nil {
continue
} else if !os.IsNotExist(err) {
return err
}

srcPath := filepath.Join(srcDir, entry.Name())
Expand Down Expand Up @@ -143,10 +148,18 @@ func ExtractLegalFilesFromZip(zipPath, destDir string) error {
continue
}

destPath := filepath.Join(destDir, f.Name)
destPath := filepath.Join(destDir, filepath.Base(filepath.Clean(f.Name)))
// Guard against path traversal
rel, err := filepath.Rel(filepath.Clean(destDir), filepath.Clean(destPath))
if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(os.PathSeparator)) {
continue
}
Comment thread
klumhru marked this conversation as resolved.

// Don't overwrite
if _, err := os.Stat(destPath); err == nil {
continue
} else if !os.IsNotExist(err) {
return err
}

rc, err := f.Open()
Expand Down
32 changes: 24 additions & 8 deletions internal/packager/legalfiles_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@ import (

func TestIsLegalFile(t *testing.T) {
yes := []string{
"LICENSE", "LICENSE.md", "LICENSE.txt",
"LICENSE", "LICENSE.md", "LICENSE.txt", "LICENSE.rst",
"license", "License.md",
"LICENCE", "Licence.txt",
"README", "README.md", "Readme.txt",
"NOTICE", "NOTICE.md", "NOTICE.txt",
"ThirdPartyNotices.txt",
"LICENCE", "Licence.txt", "LICENCE.rst",
"README", "README.md", "Readme.txt", "README.rst",
"NOTICE", "NOTICE.md", "NOTICE.txt", "NOTICE.rst",
"ThirdPartyNotices.txt", "ThirdPartyNotices.md", "ThirdPartyNotices.rst",
"third-party-notices", "THIRD-PARTY-NOTICES.md",
}
for _, name := range yes {
if !isLegalFile(name) {
Expand Down Expand Up @@ -96,7 +97,9 @@ func TestCopyLegalFiles_SurvivesExcludePatterns(t *testing.T) {
os.WriteFile(filepath.Join(srcDir, "README.md"), []byte("readme"), 0644)

// CopyFiltered with *.md excluded — README.md goes to Runtime but gets excluded
CopyFiltered(srcDir, runtimeDir, []string{"*.md"})
if err := CopyFiltered(srcDir, runtimeDir, []string{"*.md"}); err != nil {
t.Fatalf("CopyFiltered: %v", err)
}

// CopyLegalFiles to package root — should still pick up README.md and LICENSE
if err := CopyLegalFiles(srcDir, destDir); err != nil {
Expand Down Expand Up @@ -208,9 +211,22 @@ func TestExtractLegalFilesFromZip(t *testing.T) {
t.Errorf("README.md content: got %q", string(data))
}

// Nested LICENSE should NOT be extracted (not at zip root)
// Only root-level files in lib/ subdir shouldn't appear at destDir root
// Nested files should NOT be extracted (not at zip root)
if _, err := os.Stat(filepath.Join(destDir, "Foo.dll")); !os.IsNotExist(err) {
t.Error("Foo.dll should not be extracted")
}

// Nested LICENSE (under lib/netstandard2.0/) should not be extracted
// Verify only the root-level files exist by checking total file count
entries, err := os.ReadDir(destDir)
if err != nil {
t.Fatalf("failed to read destination directory: %v", err)
}
if len(entries) != 2 {
names := make([]string, len(entries))
for i, e := range entries {
names[i] = e.Name()
}
t.Errorf("expected exactly 2 files (LICENSE.txt, README.md), got %d: %v", len(entries), names)
}
}
Loading