diff --git a/cmd/spx/internal/command/export.go b/cmd/spx/internal/command/export.go index 00e46897e..12ea25612 100644 --- a/cmd/spx/internal/command/export.go +++ b/cmd/spx/internal/command/export.go @@ -54,7 +54,17 @@ func (cmd *CmdTool) Export() error { } func (cmd *CmdTool) prepareExport() error { - projectDir, _ := filepath.Abs(cmd.ProjectDir) - util.CopyDir2(filepath.Join(projectDir, "..", "assets"), filepath.Join(cmd.ProjectDir, "assets")) + if cmd.TargetAbsDir == "" { + return fmt.Errorf("stage project-local resources: logical project directory is empty") + } + sourceProjectDir := cmd.TargetAbsDir + sourceAssetDir := filepath.Join(sourceProjectDir, "assets") + destinationAssetDir := filepath.Join(cmd.ProjectDir, "assets") + if err := validateExportStage(sourceAssetDir, destinationAssetDir); err != nil { + return err + } + if err := util.CopyDir2(sourceAssetDir, destinationAssetDir); err != nil { + return fmt.Errorf("stage project-local resources from %s: %w", sourceAssetDir, err) + } return nil } diff --git a/cmd/spx/internal/command/export_staging.go b/cmd/spx/internal/command/export_staging.go new file mode 100644 index 000000000..1b4c31fac --- /dev/null +++ b/cmd/spx/internal/command/export_staging.go @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed 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 command + +import ( + "fmt" + "io/fs" + "os" + "path/filepath" + "strings" +) + +func validateExportStage(source, destination string) error { + info, err := os.Lstat(source) + if err != nil { + return fmt.Errorf("inspect project assets %q: %w", source, err) + } + if info.Mode()&os.ModeSymlink != 0 || !info.IsDir() { + return fmt.Errorf("project assets %q must be a real directory", source) + } + + source, err = filepath.Abs(source) + if err != nil { + return err + } + destination, err = filepath.Abs(destination) + if err != nil { + return err + } + if source == destination || pathWithin(destination, source) { + return fmt.Errorf("stage project assets: destination %q is inside source %q", destination, source) + } + if info, err := os.Lstat(destination); err == nil { + if info.Mode()&os.ModeSymlink != 0 { + return fmt.Errorf("stage project assets: destination %q must not be a symlink", destination) + } + } else if !os.IsNotExist(err) { + return fmt.Errorf("inspect staged project assets %q: %w", destination, err) + } + + return filepath.WalkDir(source, func(name string, entry fs.DirEntry, err error) error { + if err != nil { + return err + } + if entry.Type()&os.ModeSymlink != 0 { + return fmt.Errorf("project asset %q must not be a symlink", name) + } + if entry.IsDir() { + return nil + } + info, err := entry.Info() + if err != nil { + return err + } + if !info.Mode().IsRegular() { + return fmt.Errorf("project asset %q must be a regular file", name) + } + return nil + }) +} + +func pathWithin(name, root string) bool { + rel, err := filepath.Rel(root, name) + if err != nil { + return false + } + return rel != "." && rel != ".." && !strings.HasPrefix(rel, ".."+string(filepath.Separator)) +} diff --git a/cmd/spx/internal/command/export_test.go b/cmd/spx/internal/command/export_test.go new file mode 100644 index 000000000..5973e2097 --- /dev/null +++ b/cmd/spx/internal/command/export_test.go @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed 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 command + +import ( + "os" + "path/filepath" + "testing" +) + +func TestPrepareExportStagesAssetsFromLogicalProject(t *testing.T) { + sourceProjectDir := filepath.Join(t.TempDir(), "game") + generatedProjectDir := filepath.Join(sourceProjectDir, "project") + if err := os.MkdirAll(filepath.Join(sourceProjectDir, "assets"), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(sourceProjectDir, "assets", "index.json"), []byte(`{}`), 0o644); err != nil { + t.Fatal(err) + } + + cmd := &CmdTool{TargetAbsDir: sourceProjectDir, ProjectDir: generatedProjectDir} + if err := cmd.prepareExport(); err != nil { + t.Fatalf("prepareExport() error = %v", err) + } + if _, err := os.Stat(filepath.Join(generatedProjectDir, "assets", "index.json")); err != nil { + t.Fatalf("staged project asset: %v", err) + } +} + +func TestPrepareExportRejectsMissingLogicalProject(t *testing.T) { + cmd := &CmdTool{ProjectDir: filepath.Join(t.TempDir(), "generated")} + if err := cmd.prepareExport(); err == nil { + t.Fatal("prepareExport() accepted an empty logical project directory") + } +} + +func TestPrepareExportRejectsAssetSymlink(t *testing.T) { + root := t.TempDir() + source := filepath.Join(root, "game") + destination := filepath.Join(source, "project") + if err := os.MkdirAll(filepath.Join(source, "assets"), 0o755); err != nil { + t.Fatal(err) + } + outside := filepath.Join(t.TempDir(), "outside.txt") + if err := os.WriteFile(outside, []byte("outside"), 0o644); err != nil { + t.Fatal(err) + } + if err := os.Symlink(outside, filepath.Join(source, "assets", "linked.txt")); err != nil { + t.Skipf("symlink unavailable: %v", err) + } + + cmd := &CmdTool{TargetAbsDir: source, ProjectDir: destination} + if err := cmd.prepareExport(); err == nil { + t.Fatal("prepareExport() accepted a symlinked asset") + } +} + +func TestPrepareExportRejectsDestinationInsideSourceAssets(t *testing.T) { + source := filepath.Join(t.TempDir(), "game") + if err := os.MkdirAll(filepath.Join(source, "assets"), 0o755); err != nil { + t.Fatal(err) + } + cmd := &CmdTool{ + TargetAbsDir: source, + ProjectDir: filepath.Join(source, "assets", "generated"), + } + if err := cmd.prepareExport(); err == nil { + t.Fatal("prepareExport() accepted a destination inside the source assets") + } +} diff --git a/cmd/spx/internal/command/platform.go b/cmd/spx/internal/command/platform.go index d1dabeb2d..f12828635 100644 --- a/cmd/spx/internal/command/platform.go +++ b/cmd/spx/internal/command/platform.go @@ -15,10 +15,6 @@ func executableSuffix(goos string) string { return "" } -func goBinaryName(goos string) string { - return "go" + executableSuffix(goos) -} - func sharedLibrarySuffix(goos string) string { switch goos { case goosWindows: diff --git a/cmd/spx/internal/pack/asset_index.go b/cmd/spx/internal/pack/asset_index.go new file mode 100644 index 000000000..54ef18253 --- /dev/null +++ b/cmd/spx/internal/pack/asset_index.go @@ -0,0 +1,285 @@ +/* + * Copyright (c) 2021 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed 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 pack + +import ( + "encoding/json" + "fmt" + "os" + "path" + "path/filepath" + "slices" + + coreproject "github.com/goplus/spx/v3/internal/core/project" +) + +type packedAssetIndex struct { + Project coreproject.ProjectConfig + Sprites map[string]coreproject.SpriteConfig + Sounds map[string]coreproject.SoundConfig + Fonts map[string]coreproject.FontFamilyConfig + HasFonts bool +} + +func collectAssetPathRefs(assetRoot string) ([]assetPathRef, error) { + var refs []assetPathRef + + packed, hasPacked, err := readPackedAssetIndex(assetRoot) + if err != nil { + return nil, err + } + + if hasPacked { + refs = appendProjectAssetRefs(refs, packed.Project) + } else { + projectConfigPath := filepath.Join(assetRoot, sourceIndexName) + if _, err := os.Stat(projectConfigPath); err != nil { + if !os.IsNotExist(err) { + return nil, fmt.Errorf("stat %s: %w", projectConfigPath, err) + } + } else { + var conf coreproject.ProjectConfig + if err := readJSONFile(projectConfigPath, &conf); err != nil { + return nil, fmt.Errorf("parse %s: %w", projectConfigPath, err) + } + refs = appendProjectAssetRefs(refs, conf) + } + } + + spriteRefs, err := collectIndexedAssetRefs( + assetRoot, + "sprites", + packed.Sprites, + appendSpriteAssetRefs, + true, + ) + if err != nil { + return nil, err + } + refs = append(refs, spriteRefs...) + + soundRefs, err := collectIndexedAssetRefs( + assetRoot, + "sounds", + packed.Sounds, + appendSoundAssetRefs, + true, + ) + if err != nil { + return nil, err + } + refs = append(refs, soundRefs...) + + fontRefs, err := collectIndexedAssetRefs( + assetRoot, + "fonts", + packed.Fonts, + appendFontAssetRefs, + !hasPacked || !packed.HasFonts, + ) + if err != nil { + return nil, err + } + refs = append(refs, fontRefs...) + + return refs, nil +} + +func collectIndexedAssetRefs[T any]( + assetRoot string, + category string, + packed map[string]T, + appendRefs func([]assetPathRef, string, T) []assetPathRef, + scanSource bool, +) ([]assetPathRef, error) { + var refs []assetPathRef + + names := make([]string, 0, len(packed)) + for name := range packed { + names = append(names, name) + } + slices.Sort(names) + for _, name := range names { + refs = appendRefs(refs, path.Join(category, name), packed[name]) + } + + if !scanSource { + return refs, nil + } + + configPaths, err := filepath.Glob(filepath.Join(assetRoot, category, "*", sourceIndexName)) + if err != nil { + return nil, err + } + for _, configPath := range configPaths { + name := filepath.Base(filepath.Dir(configPath)) + if _, exists := packed[name]; exists { + continue + } + + configDir, err := relConfigDir(assetRoot, filepath.Dir(configPath)) + if err != nil { + return nil, err + } + + var conf T + if err := readJSONFile(configPath, &conf); err != nil { + return nil, fmt.Errorf("parse %s: %w", configPath, err) + } + refs = appendRefs(refs, configDir, conf) + } + + return refs, nil +} + +func appendProjectAssetRefs(refs []assetPathRef, conf coreproject.ProjectConfig) []assetPathRef { + for _, backdrop := range conf.Backdrops { + if backdrop != nil { + refs = appendAssetPathRef(refs, "", backdrop.Path) + } + } + refs = appendAssetPathRef(refs, "", conf.Bgm) + refs = appendAssetPathRef(refs, "", conf.TilemapPath) + return refs +} + +func appendSpriteAssetRefs(refs []assetPathRef, configDir string, conf coreproject.SpriteConfig) []assetPathRef { + for _, costume := range conf.Costumes { + if costume != nil { + refs = appendAssetPathRef(refs, configDir, costume.Path) + } + } + if conf.CostumeSet != nil && conf.CostumeSet.Path != "" { + refs = appendAssetPathRef(refs, configDir, conf.CostumeSet.Path) + } + if conf.CostumeMPSet != nil && conf.CostumeMPSet.Path != "" { + refs = appendAssetPathRef(refs, configDir, conf.CostumeMPSet.Path) + } + return refs +} + +func appendSoundAssetRefs(refs []assetPathRef, configDir string, conf coreproject.SoundConfig) []assetPathRef { + return appendAssetPathRef(refs, configDir, conf.Path) +} + +func appendFontAssetRefs(refs []assetPathRef, configDir string, conf coreproject.FontFamilyConfig) []assetPathRef { + for _, face := range conf.Faces { + refs = appendAssetPathRef(refs, configDir, face.Path) + } + return refs +} + +func readPackedAssetIndex(assetRoot string) (packedAssetIndex, bool, error) { + packedPath := filepath.Join(assetRoot, packedIndexName) + if _, err := os.Stat(packedPath); err != nil { + if os.IsNotExist(err) { + return packedAssetIndex{}, false, nil + } + return packedAssetIndex{}, false, fmt.Errorf("stat %s: %w", packedPath, err) + } + + var root map[string]json.RawMessage + if err := readJSONFile(packedPath, &root); err != nil { + return packedAssetIndex{}, false, fmt.Errorf("parse %s: %w", packedPath, err) + } + + sourceRoot, err := readSourceAssetIndexRoot(assetRoot) + if err != nil { + return packedAssetIndex{}, false, err + } + mergedRoot := mergePackedRootSections(root, sourceRoot) + + var packed packedAssetIndex + if err := decodePackedAssetSection(mergedRoot, &packed.Project); err != nil { + return packedAssetIndex{}, false, fmt.Errorf("parse %s root: %w", packedPath, err) + } + packed.Sprites = make(map[string]coreproject.SpriteConfig) + if err := decodePackedAssetObjects(root["sprites"], packed.Sprites); err != nil { + return packedAssetIndex{}, false, fmt.Errorf("parse %s sprites: %w", packedPath, err) + } + packed.Sounds = make(map[string]coreproject.SoundConfig) + if err := decodePackedAssetObjects(root["sounds"], packed.Sounds); err != nil { + return packedAssetIndex{}, false, fmt.Errorf("parse %s sounds: %w", packedPath, err) + } + packed.Fonts = make(map[string]coreproject.FontFamilyConfig) + _, packed.HasFonts = root["fonts"] + if err := decodePackedAssetObjects(root["fonts"], packed.Fonts); err != nil { + return packedAssetIndex{}, false, fmt.Errorf("parse %s fonts: %w", packedPath, err) + } + return packed, true, nil +} + +func readSourceAssetIndexRoot(assetRoot string) (map[string]json.RawMessage, error) { + projectConfigPath := filepath.Join(assetRoot, sourceIndexName) + if _, err := os.Stat(projectConfigPath); err != nil { + if os.IsNotExist(err) { + return nil, nil + } + return nil, fmt.Errorf("stat %s: %w", projectConfigPath, err) + } + + var root map[string]json.RawMessage + if err := readJSONFile(projectConfigPath, &root); err != nil { + return nil, fmt.Errorf("parse %s: %w", projectConfigPath, err) + } + return root, nil +} + +func mergePackedRootSections(packedRoot, sourceRoot map[string]json.RawMessage) map[string]json.RawMessage { + if len(sourceRoot) == 0 { + return packedRoot + } + + merged := make(map[string]json.RawMessage, len(sourceRoot)+len(packedRoot)) + for key, value := range sourceRoot { + merged[key] = value + } + for key, value := range packedRoot { + merged[key] = value + } + return merged +} + +func decodePackedAssetSection(root map[string]json.RawMessage, dest *coreproject.ProjectConfig) error { + if len(root) == 0 { + return nil + } + raw, err := json.Marshal(root) + if err != nil { + return err + } + return json.Unmarshal(raw, dest) +} + +func decodePackedAssetObjects[T any](raw json.RawMessage, dest map[string]T) error { + if len(raw) == 0 || string(raw) == "null" { + return nil + } + + entries := make(map[string]json.RawMessage) + if err := json.Unmarshal(raw, &entries); err != nil { + return err + } + for name, entry := range entries { + var conf T + if err := json.Unmarshal(entry, &conf); err != nil { + return fmt.Errorf("%s: %w", name, err) + } + dest[name] = conf + } + return nil +} diff --git a/cmd/spx/internal/pack/json_file.go b/cmd/spx/internal/pack/json_file.go new file mode 100644 index 000000000..303f4238a --- /dev/null +++ b/cmd/spx/internal/pack/json_file.go @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed 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 pack + +import ( + "encoding/json" + "fmt" + "io" + "os" +) + +const maxPackJSONSize = 16 << 20 + +func readJSONFile(name string, value any) error { + before, err := os.Lstat(name) + if err != nil { + return err + } + if before.Mode()&os.ModeSymlink != 0 || !before.Mode().IsRegular() { + return fmt.Errorf("%s must be a regular non-symlink file", name) + } + if before.Size() > maxPackJSONSize { + return fmt.Errorf("%s exceeds the JSON size limit", name) + } + + file, err := os.Open(name) + if err != nil { + return err + } + opened, statErr := file.Stat() + current, lstatErr := os.Lstat(name) + if statErr != nil || lstatErr != nil || current.Mode()&os.ModeSymlink != 0 || + !opened.Mode().IsRegular() || !os.SameFile(before, opened) || !os.SameFile(opened, current) { + _ = file.Close() + return fmt.Errorf("%s changed while it was opened", name) + } + + data, readErr := io.ReadAll(io.LimitReader(file, maxPackJSONSize+1)) + afterOpened, statErr := file.Stat() + afterPath, lstatErr := os.Lstat(name) + closeErr := file.Close() + if readErr != nil { + return readErr + } + if statErr != nil || lstatErr != nil || afterPath.Mode()&os.ModeSymlink != 0 || + !os.SameFile(opened, afterOpened) || !os.SameFile(afterOpened, afterPath) || + !sameStableFileMetadata(opened, afterOpened) || int64(len(data)) != opened.Size() { + return fmt.Errorf("%s changed while it was read", name) + } + if len(data) > maxPackJSONSize { + return fmt.Errorf("%s exceeds the JSON size limit", name) + } + if closeErr != nil { + return closeErr + } + return json.Unmarshal(data, value) +} diff --git a/cmd/spx/internal/pack/json_file_test.go b/cmd/spx/internal/pack/json_file_test.go new file mode 100644 index 000000000..59904f072 --- /dev/null +++ b/cmd/spx/internal/pack/json_file_test.go @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed 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 pack + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestReadJSONFileRejectsUnsafeInput(t *testing.T) { + root := t.TempDir() + large := filepath.Join(root, "large.json") + file, err := os.Create(large) + if err != nil { + t.Fatal(err) + } + if err := file.Truncate(maxPackJSONSize + 1); err != nil { + file.Close() + t.Fatal(err) + } + if err := file.Close(); err != nil { + t.Fatal(err) + } + if err := readJSONFile(large, new(any)); err == nil || !strings.Contains(err.Error(), "size limit") { + t.Fatalf("oversized JSON error = %v", err) + } + + target := filepath.Join(root, "target.json") + if err := os.WriteFile(target, []byte(`{}`), 0o600); err != nil { + t.Fatal(err) + } + link := filepath.Join(root, "link.json") + if err := os.Symlink(target, link); err != nil { + t.Skipf("symlink unavailable: %v", err) + } + if err := readJSONFile(link, new(any)); err == nil || !strings.Contains(err.Error(), "non-symlink") { + t.Fatalf("symlink JSON error = %v", err) + } +} diff --git a/cmd/spx/internal/pack/pack.go b/cmd/spx/internal/pack/pack.go index 37ac1b979..83962a540 100644 --- a/cmd/spx/internal/pack/pack.go +++ b/cmd/spx/internal/pack/pack.go @@ -18,97 +18,106 @@ package pack import ( "archive/zip" + "fmt" "io" "os" - "path" "path/filepath" "slices" "strings" "time" - - "github.com/goplus/spx/v3/cmd/spx/internal/util" ) -type DirInfos struct { +type dirInfo struct { path string info os.FileInfo - // zipPath overrides the zip entry path. - zipPath string + // zipPath overrides the zip entry path for a legacy external asset. + zipPath string + root *os.Root + rootPath string } func PackProject(baseFolder string, dstZipPath string) error { - paths := []DirInfos{} - if util.IsFileExist(dstZipPath) { - if err := os.Remove(dstZipPath); err != nil { - return err - } - } - skipDirs := map[string]struct{}{ - ".git": {}, "project": {}, - } - - file, err := os.Create(dstZipPath) + projectRoot, err := openPackRoot(baseFolder) if err != nil { return err } - zipWriter := zip.NewWriter(file) - closeZip := func(err error) error { - if closeErr := zipWriter.Close(); err == nil && closeErr != nil { - err = closeErr - } - if closeErr := file.Close(); err == nil && closeErr != nil { - err = closeErr - } + defer projectRoot.Close() + if err := validatePackDestination(dstZipPath); err != nil { return err } - - err = filepath.Walk(baseFolder, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - rel, err := filepath.Rel(baseFolder, path) - if err != nil { - return err - } - if rel == "." { - return nil - } - if strings.HasSuffix(path, ".import") { - return nil - } - parts := strings.Split(rel, string(filepath.Separator)) - if len(parts) == 1 || (len(parts) == 2 && info.IsDir()) { - if _, ok := skipDirs[info.Name()]; ok { - if info.IsDir() { - return filepath.SkipDir - } - return nil - } - } - paths = append(paths, DirInfos{path: path, info: info}) - return nil - }) + paths, err := collectProjectPaths(baseFolder, dstZipPath, projectRoot) if err != nil { - return closeZip(err) + return err + } + extAssetDir, err := validateLegacyPackInputs(baseFolder) + if err != nil { + return err } - existingZipPaths := make(map[string]struct{}, len(paths)) for _, dirInfo := range paths { existingZipPaths[zipEntryName(baseFolder, dirInfo)] = struct{}{} } - - extraPaths, err := collectExternalAssetPaths(baseFolder, existingZipPaths) + extraPaths, err := collectExternalAssetPathsWithConfig(baseFolder, existingZipPaths, &extAssetDir) if err != nil { - return closeZip(err) + return err } + defer closePackRoots(extraPaths) paths = append(paths, extraPaths...) - return closeZip(PackZip(zipWriter, baseFolder, paths)) + tempName, file, err := createPackOutput(dstZipPath) + if err != nil { + return err + } + removeTemp := true + defer func() { + if removeTemp { + _ = os.Remove(tempName) + } + }() + + zipWriter := zip.NewWriter(file) + if err := closePackOutput(zipWriter, file, packZip(zipWriter, baseFolder, paths)); err != nil { + return err + } + if err := publishPackOutput(tempName, dstZipPath); err != nil { + return err + } + removeTemp = false + return nil } -func PackZip(zipWriter *zip.Writer, baseFolder string, paths []DirInfos) error { +func packZip(zipWriter *zip.Writer, baseFolder string, paths []dirInfo) error { + baseRootPath := filepath.Clean(baseFolder) + var defaultRoot *os.Root + for i := range paths { + if paths[i].root != nil { + continue + } + if defaultRoot == nil { + var err error + defaultRoot, err = openPackRoot(baseRootPath) + if err != nil { + return err + } + } + rel, err := filepath.Rel(baseRootPath, filepath.Clean(paths[i].path)) + if err != nil { + defaultRoot.Close() + return fmt.Errorf("project entry %s: resolve path relative to base folder: %w", paths[i].path, err) + } + if rel == ".." || filepath.IsAbs(rel) || strings.HasPrefix(rel, ".."+string(filepath.Separator)) { + defaultRoot.Close() + return fmt.Errorf("project entry %s is outside base folder", paths[i].path) + } + paths[i].root = defaultRoot + paths[i].rootPath = rel + } + if defaultRoot != nil { + defer defaultRoot.Close() + } baseFolder = strings.ReplaceAll(baseFolder, "\\", "/") - slices.SortFunc(paths, func(a, b DirInfos) int { + seenNames := make(map[string]struct{}, len(paths)) + slices.SortFunc(paths, func(a, b dirInfo) int { nameA := zipEntryName(baseFolder, a) nameB := zipEntryName(baseFolder, b) if nameA < nameB { @@ -119,9 +128,18 @@ func PackZip(zipWriter *zip.Writer, baseFolder string, paths []DirInfos) error { return 0 }) for _, dirInfo := range paths { - path := dirInfo.path - path = strings.ReplaceAll(path, "\\", "/") + filePath := dirInfo.path info := dirInfo.info + current, err := dirInfo.root.Lstat(dirInfo.rootPath) + if err != nil { + return fmt.Errorf("inspect project entry %s before packing: %w", filePath, err) + } + if current.Mode()&os.ModeSymlink != 0 || (!current.IsDir() && !current.Mode().IsRegular()) || !os.SameFile(info, current) { + return fmt.Errorf("project entry %s changed after collection", filePath) + } + if current.IsDir() != info.IsDir() { + return fmt.Errorf("project entry %s changed type after collection", filePath) + } header, err := zip.FileInfoHeader(info) if err != nil { return err @@ -132,6 +150,13 @@ func PackZip(zipWriter *zip.Writer, baseFolder string, paths []DirInfos) error { if header.Name == "" { continue } + if !validZipEntryName(header.Name) { + return fmt.Errorf("project entry %s has unsafe zip name %q", filePath, header.Name) + } + if _, exists := seenNames[header.Name]; exists { + return fmt.Errorf("project entry %s duplicates zip name %q", filePath, header.Name) + } + seenNames[header.Name] = struct{}{} if info.IsDir() { header.Name += "/" _, err := zipWriter.CreateHeader(header) @@ -141,21 +166,38 @@ func PackZip(zipWriter *zip.Writer, baseFolder string, paths []DirInfos) error { continue } - fileToZip, err := os.Open(path) + fileToZip, err := dirInfo.root.Open(dirInfo.rootPath) if err != nil { return err } + opened, err := fileToZip.Stat() + if err != nil { + fileToZip.Close() + return fmt.Errorf("stat opened project entry %s: %w", filePath, err) + } + current, err = dirInfo.root.Lstat(dirInfo.rootPath) + if err != nil || !opened.Mode().IsRegular() || current.Mode()&os.ModeSymlink != 0 || !os.SameFile(info, opened) || !os.SameFile(opened, current) { + fileToZip.Close() + return fmt.Errorf("project entry %s changed while opening", filePath) + } writer, err := zipWriter.CreateHeader(header) if err != nil { fileToZip.Close() return err } - _, copyErr := io.Copy(writer, fileToZip) + copied, copyErr := io.Copy(writer, fileToZip) + afterOpened, statErr := fileToZip.Stat() + afterPath, lstatErr := dirInfo.root.Lstat(dirInfo.rootPath) closeErr := fileToZip.Close() if copyErr != nil { return copyErr } + if statErr != nil || lstatErr != nil || afterPath.Mode()&os.ModeSymlink != 0 || + !os.SameFile(opened, afterOpened) || !os.SameFile(afterOpened, afterPath) || + copied != opened.Size() || !sameStableFileMetadata(opened, afterOpened) { + return fmt.Errorf("project entry %s changed while packing", filePath) + } if closeErr != nil { return closeErr } @@ -163,75 +205,70 @@ func PackZip(zipWriter *zip.Writer, baseFolder string, paths []DirInfos) error { return nil } -func PackDirFiles(zipName string, targetDir string, directories, files []string) error { - zipFile, err := os.Create(zipName) - if err != nil { - return err +func validZipEntryName(name string) bool { + if name == "" || strings.HasPrefix(name, "/") { + return false } - zipWriter := zip.NewWriter(zipFile) - closeZip := func(err error) error { - if closeErr := zipWriter.Close(); err == nil && closeErr != nil { - err = closeErr - } - if closeErr := zipFile.Close(); err == nil && closeErr != nil { - err = closeErr + for _, segment := range strings.Split(name, "/") { + if segment == ".." { + return false } - return err } + return true +} - paths := []DirInfos{} - for _, dir := range directories { - paths, err = addDirToZip(path.Join(targetDir, dir), paths) - if err != nil { - return closeZip(err) - } +func sameStableFileMetadata(before, after os.FileInfo) bool { + return before.Mode() == after.Mode() && before.Size() == after.Size() && before.ModTime() == after.ModTime() +} + +func openPackRoot(path string) (*os.Root, error) { + before, err := os.Lstat(path) + if err != nil { + return nil, err + } + if before.Mode()&os.ModeSymlink != 0 || !before.IsDir() { + return nil, fmt.Errorf("pack root %q must be a real directory", path) + } + root, err := os.OpenRoot(path) + if err != nil { + return nil, err + } + opened, err := root.Stat(".") + if err != nil { + root.Close() + return nil, err + } + current, err := os.Lstat(path) + if err != nil { + root.Close() + return nil, err } + if !opened.IsDir() || !os.SameFile(before, opened) || current.Mode()&os.ModeSymlink != 0 || !os.SameFile(opened, current) { + root.Close() + return nil, fmt.Errorf("pack root %q changed while it was opened", path) + } + return root, nil +} - for _, file := range files { - paths, err = addFileToZip(path.Join(targetDir, file), paths) - if err != nil { - return closeZip(err) +func closePackRoots(paths []dirInfo) { + closed := make(map[*os.Root]struct{}) + for _, dirInfo := range paths { + if dirInfo.root == nil { + continue } + if _, ok := closed[dirInfo.root]; ok { + continue + } + closed[dirInfo.root] = struct{}{} + _ = dirInfo.root.Close() } - - return closeZip(PackZip(zipWriter, targetDir, paths)) } -func zipEntryName(baseFolder string, dirInfo DirInfos) string { +func zipEntryName(baseFolder string, dirInfo dirInfo) string { if dirInfo.zipPath != "" { return strings.TrimPrefix(normalizeZipPath(dirInfo.zipPath), "/") } - baseFolder = normalizeZipPath(baseFolder) name := strings.TrimPrefix(normalizeZipPath(dirInfo.path), baseFolder) return strings.TrimPrefix(name, "/") } - -func normalizeZipPath(path string) string { - return strings.ReplaceAll(path, "\\", "/") -} - -func addDirToZip(dirPath string, paths []DirInfos) ([]DirInfos, error) { - err := filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - paths = append(paths, DirInfos{path: path, info: info}) - return nil - }) - return paths, err -} - -func addFileToZip(path string, paths []DirInfos) ([]DirInfos, error) { - file, err := os.Open(path) - if err != nil { - return nil, err - } - defer file.Close() - info, err := file.Stat() - if err != nil { - return nil, err - } - paths = append(paths, DirInfos{path: path, info: info}) - return paths, nil -} diff --git a/cmd/spx/internal/pack/pack_output.go b/cmd/spx/internal/pack/pack_output.go new file mode 100644 index 000000000..eb5b78c4f --- /dev/null +++ b/cmd/spx/internal/pack/pack_output.go @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2021 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed 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 pack + +import ( + "fmt" + "os" + "path/filepath" + "runtime" +) + +func validatePackDestination(name string) error { + info, err := os.Lstat(name) + if os.IsNotExist(err) { + return nil + } + if err != nil { + return fmt.Errorf("inspect pack destination %q: %w", name, err) + } + if info.Mode()&os.ModeSymlink != 0 { + return fmt.Errorf("pack destination %q must not be a symlink", name) + } + if info.IsDir() { + return fmt.Errorf("pack destination %q must be a file", name) + } + if !info.Mode().IsRegular() { + return fmt.Errorf("pack destination %q must be a regular file", name) + } + return nil +} + +func createPackOutput(name string) (string, *os.File, error) { + dir := filepath.Dir(name) + prefix := "." + filepath.Base(name) + ".tmp-" + file, err := os.CreateTemp(dir, prefix) + if err != nil { + return "", nil, fmt.Errorf("create temporary pack output in %q: %w", dir, err) + } + if err := file.Chmod(0o644); err != nil { + _ = file.Close() + _ = os.Remove(file.Name()) + return "", nil, fmt.Errorf("set pack output mode in %q: %w", dir, err) + } + return file.Name(), file, nil +} + +func closePackOutput(writer interface{ Close() error }, file *os.File, err error) error { + if closeErr := writer.Close(); err == nil && closeErr != nil { + err = closeErr + } + if err == nil { + err = file.Sync() + } + if closeErr := file.Close(); err == nil && closeErr != nil { + err = closeErr + } + return err +} + +func publishPackOutput(tempName, destination string) error { + if err := os.Rename(tempName, destination); err == nil { + return nil + } else if runtime.GOOS != "windows" { + return fmt.Errorf("publish pack output %q: %w", destination, err) + } + + info, err := os.Lstat(destination) + if err != nil { + return fmt.Errorf("publish pack output %q: %w", destination, err) + } + if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() { + return fmt.Errorf("pack destination %q changed while publishing", destination) + } + if err := os.Remove(destination); err != nil { + return fmt.Errorf("replace pack output %q: %w", destination, err) + } + if err := os.Rename(tempName, destination); err != nil { + return fmt.Errorf("publish pack output %q: %w", destination, err) + } + return nil +} diff --git a/cmd/spx/internal/pack/pack_test.go b/cmd/spx/internal/pack/pack_test.go index 2253bf075..8f27ecd1a 100644 --- a/cmd/spx/internal/pack/pack_test.go +++ b/cmd/spx/internal/pack/pack_test.go @@ -18,7 +18,6 @@ package pack import ( "archive/zip" - "fmt" "io" "os" "path/filepath" @@ -26,138 +25,213 @@ import ( "testing" ) -func TestPackProjectIncludesSharedExternalAssets(t *testing.T) { +func TestPackProjectIncludesResourcesWithinProject(t *testing.T) { tmpDir := t.TempDir() - projectDir := filepath.Join(tmpDir, "All") - if err := os.MkdirAll(filepath.Join(projectDir, "assets", "sprites", "SpMotion"), 0755); err != nil { + projectDir := filepath.Join(tmpDir, "Game") + writeTestFile(t, filepath.Join(projectDir, "assets", "index.json"), `{ + "backdrops":[{"path":"res://res/bg.png"}], + "bgm":"../res/audio/theme.mp3", + "tilemapPath":"../res/maps/map.json", + "map":{"width":480,"height":360} +}`) + writeTestFile(t, filepath.Join(projectDir, "assets", "sprites", "Hero", "index.json"), `{ + "costumeSet":{"faceRight":180,"path":"../../../res/hero.png","nx":96} +}`) + writeTestFile(t, filepath.Join(projectDir, "assets", "sounds", "Bell", "index.json"), `{ + "path":"../../../res/audio/ring.wav" +}`) + writeTestFile(t, filepath.Join(projectDir, "assets", "fonts", "Custom", "index.json"), `{ + "faces":[{"path":"../../../res/fonts/custom.ttf"}] +}`) + writeTestFile(t, filepath.Join(projectDir, "res", "bg.png"), "bg") + writeTestFile(t, filepath.Join(projectDir, "res", "hero.png"), "hero") + writeTestFile(t, filepath.Join(projectDir, "res", "audio", "theme.mp3"), "theme") + writeTestFile(t, filepath.Join(projectDir, "res", "audio", "ring.wav"), "ring") + writeTestFile(t, filepath.Join(projectDir, "res", "maps", "map.json"), "{}") + writeTestFile(t, filepath.Join(projectDir, "res", "fonts", "custom.ttf"), "font") + + zipPath := filepath.Join(tmpDir, "game.zip") + if err := PackProject(projectDir, zipPath); err != nil { t.Fatal(err) } - if err := os.MkdirAll(filepath.Join(tmpDir, "res"), 0755); err != nil { - t.Fatal(err) + + snapshot := readZipSnapshot(t, zipPath) + for name, want := range map[string]string{ + "res/bg.png": "bg", + "res/hero.png": "hero", + "res/audio/theme.mp3": "theme", + "res/audio/ring.wav": "ring", + "res/maps/map.json": "{}", + "res/fonts/custom.ttf": "font", + } { + if got := snapshot.contents[name]; strings.TrimSpace(got) != strings.TrimSpace(want) { + t.Fatalf("%s content = %q, want %q", name, got, want) + } + } + for name := range snapshot.counts { + if strings.HasPrefix(name, "../") { + t.Fatalf("unexpected zip entry %q", name) + } } +} - if err := os.WriteFile(filepath.Join(projectDir, "assets", "index.json"), []byte(`{"map":{"width":480,"height":360}}`), 0644); err != nil { +func TestPackProjectRejectsMissingAssetIndex(t *testing.T) { + tmpDir := t.TempDir() + projectDir := filepath.Join(tmpDir, "Game") + writeTestFile(t, filepath.Join(projectDir, "main.spx"), "onStart => {}") + if err := os.MkdirAll(filepath.Join(projectDir, "assets"), 0o755); err != nil { t.Fatal(err) } - if err := os.WriteFile(filepath.Join(projectDir, "assets", "sprites", "SpMotion", "index.json"), []byte(`{ - "costumeSet": { - "faceRight": 180, - "path": "../../../../res/monkey.png", - "nx": 96 - } -}`), 0644); err != nil { - t.Fatal(err) + + zipPath := filepath.Join(tmpDir, "game.zip") + err := PackProject(projectDir, zipPath) + if err == nil || !strings.Contains(err.Error(), "contains neither") { + t.Fatalf("PackProject() error = %v, want missing-index rejection", err) } - if err := os.WriteFile(filepath.Join(tmpDir, "res", "monkey.png"), []byte("png"), 0644); err != nil { - t.Fatal(err) + if _, statErr := os.Stat(zipPath); !os.IsNotExist(statErr) { + t.Fatalf("output created after missing-index rejection: stat error %v", statErr) } +} + +func TestPackProjectRejectsMalformedAssetIndex(t *testing.T) { + tmpDir := t.TempDir() + projectDir := filepath.Join(tmpDir, "Game") + writeTestFile(t, filepath.Join(projectDir, "assets", "index.json"), "{not-json") zipPath := filepath.Join(tmpDir, "game.zip") - if err := PackProject(projectDir, zipPath); err != nil { + err := PackProject(projectDir, zipPath) + if err == nil || !strings.Contains(err.Error(), "validate asset indexes") { + t.Fatalf("PackProject() error = %v, want malformed-index rejection", err) + } + if _, statErr := os.Stat(zipPath); !os.IsNotExist(statErr) { + t.Fatalf("output created after malformed-index rejection: stat error %v", statErr) + } +} + +func TestPackProjectRejectsMalformedConfigWithoutAssets(t *testing.T) { + tmpDir := t.TempDir() + projectDir := filepath.Join(tmpDir, "Game") + writeTestFile(t, filepath.Join(projectDir, ".config"), "{not-json") + + zipPath := filepath.Join(tmpDir, "game.zip") + err := PackProject(projectDir, zipPath) + if err == nil || !strings.Contains(err.Error(), "parse project config") { + t.Fatalf("PackProject() error = %v, want malformed-config rejection", err) + } + if _, statErr := os.Stat(zipPath); !os.IsNotExist(statErr) { + t.Fatalf("output created after malformed-config rejection: stat error %v", statErr) + } +} + +func TestPackProjectKeepsExistingOutputWhenValidationFails(t *testing.T) { + projectDir := filepath.Join(t.TempDir(), "Game") + writeTestFile(t, filepath.Join(projectDir, "assets", "index.json"), "{not-json") + zipPath := filepath.Join(projectDir, "game.zip") + old := []byte("previous output") + if err := os.WriteFile(zipPath, old, 0o600); err != nil { t.Fatal(err) } - snapshot := readZipSnapshot(t, zipPath) - if snapshot.counts["res/monkey.png"] != 1 { - t.Fatalf("res/monkey.png count = %d, want 1", snapshot.counts["res/monkey.png"]) + if err := PackProject(projectDir, zipPath); err == nil { + t.Fatal("PackProject() succeeded for malformed input") } - if snapshot.contents["res/monkey.png"] != "png" { - t.Fatalf("res/monkey.png content = %q, want %q", snapshot.contents["res/monkey.png"], "png") + got, err := os.ReadFile(zipPath) + if err != nil { + t.Fatal(err) } - for name := range snapshot.counts { - if strings.HasPrefix(name, "../") { - t.Fatalf("unexpected zip entry %q", name) - } + if string(got) != string(old) { + t.Fatalf("existing output changed to %q", got) } } -func TestPackProjectCoversExternalAssetVariants(t *testing.T) { +func TestPackProjectDoesNotPackItsOutput(t *testing.T) { + projectDir := filepath.Join(t.TempDir(), "Game") + writeTestFile(t, filepath.Join(projectDir, "assets", "index.json"), `{}`) + zipPath := filepath.Join(projectDir, "game.zip") + if err := os.WriteFile(zipPath, []byte("stale"), 0o600); err != nil { + t.Fatal(err) + } + + if err := PackProject(projectDir, zipPath); err != nil { + t.Fatal(err) + } + if _, exists := readZipSnapshot(t, zipPath).contents["game.zip"]; exists { + t.Fatal("pack output was included in itself") + } +} + +func TestPackProjectIncludesLegacySharedResource(t *testing.T) { tmpDir := t.TempDir() projectDir := filepath.Join(tmpDir, "Game") - escapeDir := filepath.Join(filepath.Dir(tmpDir), filepath.Base(tmpDir)+"-escape") - t.Cleanup(func() { - _ = os.RemoveAll(escapeDir) - }) - - writeTestFile(t, filepath.Join(projectDir, ".config"), `{"extasset":"custom_asset"}`) - writeTestFile(t, filepath.Join(projectDir, "assets", "index.json"), fmt.Sprintf(`{ - "backdrops": [ - {"path":"../../shared/bg.png"}, - {"path":"../../../%s/ignored.png"} - ], - "bgm":"../../shared/audio/theme.mp3", - "tilemapPath":"../../shared/maps/map.json", + writeTestFile(t, filepath.Join(projectDir, "assets", "index.json"), `{ + "backdrops":[{"path":"../../shared/bg.png"}], "map":{"width":480,"height":360} -}`, filepath.Base(escapeDir))) - writeTestFile(t, filepath.Join(projectDir, "assets", "sprites", "Hero", "index.json"), `{ - "costumes":[ - {"path":"../../../../custom_asset/shared.png"} - ], - "costumeSet":{ - "faceRight":180, - "path":"../../../../custom_asset/hero.png", - "nx":96 - } -}`) - writeTestFile(t, filepath.Join(projectDir, "assets", "sounds", "Bell", "index.json"), `{ - "path":"../../../../shared/audio/ring.wav" }`) - writeTestFile(t, filepath.Join(projectDir, "extasset", "shared.png"), "local") - writeTestFile(t, filepath.Join(tmpDir, "shared", "bg.png"), "bg") - writeTestFile(t, filepath.Join(tmpDir, "shared", "audio", "theme.mp3"), "theme") - writeTestFile(t, filepath.Join(tmpDir, "shared", "audio", "ring.wav"), "ring") - writeTestFile(t, filepath.Join(tmpDir, "shared", "maps", "map.json"), "{}") - writeTestFile(t, filepath.Join(tmpDir, "custom_asset", "hero.png"), "hero") - writeTestFile(t, filepath.Join(tmpDir, "custom_asset", "shared.png"), "external-duplicate") - writeTestFile(t, filepath.Join(escapeDir, "ignored.png"), "ignored") zipPath := filepath.Join(tmpDir, "game.zip") if err := PackProject(projectDir, zipPath); err != nil { t.Fatal(err) } + if got := readZipSnapshot(t, zipPath).contents["shared/bg.png"]; got != "bg" { + t.Fatalf("shared/bg.png content = %q, want bg", got) + } +} - snapshot := readZipSnapshot(t, zipPath) - assertZipEntryContent(t, snapshot, "shared/bg.png", "bg") - assertZipEntryContent(t, snapshot, "shared/audio/theme.mp3", "theme") - assertZipEntryContent(t, snapshot, "shared/audio/ring.wav", "ring") - assertZipEntryContent(t, snapshot, "shared/maps/map.json", "{}") - assertZipEntryContent(t, snapshot, "extasset/hero.png", "hero") - assertZipEntryContent(t, snapshot, "extasset/shared.png", "local") +func TestPackProjectRejectsResourceThroughSymlinkOutsideProject(t *testing.T) { + tmpDir := t.TempDir() + projectDir := filepath.Join(tmpDir, "Game") + externalDir := filepath.Join(tmpDir, "external") + writeTestFile(t, filepath.Join(projectDir, "assets", "index.json"), `{ + "backdrops":[{"path":"../linked/bg.png"}], + "map":{"width":480,"height":360} +}`) + writeTestFile(t, filepath.Join(externalDir, "bg.png"), "bg") + if err := os.Symlink(externalDir, filepath.Join(projectDir, "linked")); err != nil { + t.Skipf("symlink unavailable: %v", err) + } - if snapshot.counts["extasset/shared.png"] != 1 { - t.Fatalf("extasset/shared.png count = %d, want 1", snapshot.counts["extasset/shared.png"]) + err := PackProject(projectDir, filepath.Join(tmpDir, "game.zip")) + if err == nil || !strings.Contains(err.Error(), "symlink") { + t.Fatalf("PackProject() error = %v, want no-follow rejection", err) } - if _, exists := snapshot.counts[filepath.Base(escapeDir)+"/ignored.png"]; exists { - t.Fatalf("unexpected escaped asset %q in zip", filepath.Base(escapeDir)+"/ignored.png") +} + +func TestPackProjectAcceptsPackedOnlyConfig(t *testing.T) { + tmpDir := t.TempDir() + projectDir := filepath.Join(tmpDir, "Game") + writeTestFile(t, filepath.Join(projectDir, "main.spx"), "onStart => {}") + writeTestFile(t, filepath.Join(projectDir, "assets", "index_pack.json"), `{"zorder":[]}`) + + zipPath := filepath.Join(tmpDir, "game.zip") + if err := PackProject(projectDir, zipPath); err != nil { + t.Fatal(err) } - for name := range snapshot.counts { - if strings.HasPrefix(name, "../") { - t.Fatalf("unexpected zip entry %q", name) - } + if got := readZipSnapshot(t, zipPath).counts["assets/index_pack.json"]; got != 1 { + t.Fatalf("assets/index_pack.json count = %d, want 1", got) } } -func TestPackProjectFailsOnMissingExternalAsset(t *testing.T) { +func TestPackProjectIncludesLegacyExtAssetConfig(t *testing.T) { tmpDir := t.TempDir() projectDir := filepath.Join(tmpDir, "Game") - + writeTestFile(t, filepath.Join(projectDir, ".config"), `{"extasset":"custom_asset"}`) writeTestFile(t, filepath.Join(projectDir, "assets", "index.json"), `{ - "backdrops":[{"path":"../../shared/missing.png"}], + "backdrops":[{"path":"../../custom_asset/bg.png"}], "map":{"width":480,"height":360} }`) + writeTestFile(t, filepath.Join(tmpDir, "custom_asset", "bg.png"), "external-bg") zipPath := filepath.Join(tmpDir, "game.zip") - err := PackProject(projectDir, zipPath) - if err == nil { - t.Fatal("PackProject() error = nil, want missing external asset error") + if err := PackProject(projectDir, zipPath); err != nil { + t.Fatal(err) } - if !strings.Contains(err.Error(), "missing.png") { - t.Fatalf("PackProject() error = %q, want mention of missing.png", err) + if got := readZipSnapshot(t, zipPath).contents["extasset/bg.png"]; got != "external-bg" { + t.Fatalf("extasset/bg.png content = %q, want external-bg", got) } } -func TestPackProjectIncludesExternalAssetsFromPackedConfigFallback(t *testing.T) { +func TestPackProjectIncludesExternalAssetFromPackedConfig(t *testing.T) { tmpDir := t.TempDir() projectDir := filepath.Join(tmpDir, "Game") @@ -176,33 +250,31 @@ func TestPackProjectIncludesExternalAssetsFromPackedConfigFallback(t *testing.T) "sounds":{ "Bell":{"path":"../../../../shared/audio/ring.wav"} }, - "fonts":{ - "Custom":{"faces":[{"path":"../../../../shared/fonts/custom.ttf"}]} - } + "fonts":{"Custom":{"faces":[{"path":"../../../../shared/fonts/custom.ttf"}]}} }`) writeTestFile(t, filepath.Join(tmpDir, "shared", "bg.jpg"), "bg") writeTestFile(t, filepath.Join(tmpDir, "shared", "hero.png"), "hero") writeTestFile(t, filepath.Join(tmpDir, "shared", "audio", "ring.wav"), "ring") writeTestFile(t, filepath.Join(tmpDir, "shared", "fonts", "custom.ttf"), "font") - // A packed fonts catalog is authoritative; a stale source-only family must - // not make packing fail or add an undeclared external font. - writeTestFile(t, filepath.Join(projectDir, "assets", "fonts", "Stale", "index.json"), `{ - "faces":[{"path":"../../../../shared/fonts/missing.ttf"}] -}`) zipPath := filepath.Join(tmpDir, "game.zip") if err := PackProject(projectDir, zipPath); err != nil { t.Fatal(err) } - snapshot := readZipSnapshot(t, zipPath) - assertZipEntryContent(t, snapshot, "shared/bg.jpg", "bg") - assertZipEntryContent(t, snapshot, "shared/hero.png", "hero") - assertZipEntryContent(t, snapshot, "shared/audio/ring.wav", "ring") - assertZipEntryContent(t, snapshot, "shared/fonts/custom.ttf", "font") + for name, want := range map[string]string{ + "shared/bg.jpg": "bg", + "shared/hero.png": "hero", + "shared/audio/ring.wav": "ring", + "shared/fonts/custom.ttf": "font", + } { + if got := snapshot.contents[name]; got != want { + t.Fatalf("%s content = %q, want %q", name, got, want) + } + } } -func TestPackProjectIncludesExternalAssetsFromSourceRootWhenPackedRootMissing(t *testing.T) { +func TestPackProjectIncludesExternalAssetFromSourceRootWhenPackedRootMissing(t *testing.T) { tmpDir := t.TempDir() projectDir := filepath.Join(tmpDir, "Game") @@ -230,17 +302,97 @@ func TestPackProjectIncludesExternalAssetsFromSourceRootWhenPackedRootMissing(t writeTestFile(t, filepath.Join(tmpDir, "shared", "audio", "theme.mp3"), "theme") writeTestFile(t, filepath.Join(tmpDir, "shared", "hero.png"), "hero") writeTestFile(t, filepath.Join(tmpDir, "shared", "fonts", "source.ttf"), "source-font") - zipPath := filepath.Join(tmpDir, "game.zip") if err := PackProject(projectDir, zipPath); err != nil { t.Fatal(err) } - snapshot := readZipSnapshot(t, zipPath) - assertZipEntryContent(t, snapshot, "shared/bg.jpg", "bg") - assertZipEntryContent(t, snapshot, "shared/audio/theme.mp3", "theme") - assertZipEntryContent(t, snapshot, "shared/hero.png", "hero") - assertZipEntryContent(t, snapshot, "shared/fonts/source.ttf", "source-font") + for name, want := range map[string]string{ + "shared/bg.jpg": "bg", + "shared/audio/theme.mp3": "theme", + "shared/hero.png": "hero", + "shared/fonts/source.ttf": "source-font", + } { + if got := snapshot.contents[name]; got != want { + t.Fatalf("%s content = %q, want %q", name, got, want) + } + } +} + +func TestPackZipRejectsFileReplacedAfterCollection(t *testing.T) { + tmpDir := t.TempDir() + filePath := filepath.Join(tmpDir, "asset.txt") + writeTestFile(t, filePath, "inside") + info, err := os.Lstat(filePath) + if err != nil { + t.Fatal(err) + } + externalPath := filepath.Join(t.TempDir(), "external.txt") + writeTestFile(t, externalPath, "outside") + if err := os.Remove(filePath); err != nil { + t.Fatal(err) + } + if err := os.Symlink(externalPath, filePath); err != nil { + t.Skipf("symlink unavailable: %v", err) + } + + var output strings.Builder + zipWriter := zip.NewWriter(&output) + err = packZip(zipWriter, tmpDir, []dirInfo{{path: filePath, info: info}}) + _ = zipWriter.Close() + if err == nil || !strings.Contains(err.Error(), "changed after collection") { + t.Fatalf("PackZip() error = %v, want replaced-file rejection", err) + } +} + +func TestPackZipRejectsExternalParentReplacedByOutsideSymlink(t *testing.T) { + tmpDir := t.TempDir() + projectDir := filepath.Join(tmpDir, "Game") + writeTestFile(t, filepath.Join(projectDir, "assets", "index.json"), `{ + "backdrops":[{"path":"../../shared/bg.png"}], + "map":{"width":480,"height":360} +}`) + sharedDir := filepath.Join(tmpDir, "shared") + writeTestFile(t, filepath.Join(sharedDir, "bg.png"), "inside") + + extraPaths, err := collectExternalAssetPathsWithConfig(projectDir, nil, nil) + if err != nil { + t.Fatal(err) + } + defer closePackRoots(extraPaths) + if len(extraPaths) != 1 { + t.Fatalf("collected %d external paths, want 1", len(extraPaths)) + } + + outsideDir := filepath.Join(t.TempDir(), "shared") + if err := os.Rename(sharedDir, outsideDir); err != nil { + t.Fatal(err) + } + if err := os.Symlink(outsideDir, sharedDir); err != nil { + t.Skipf("symlink unavailable: %v", err) + } + + zipPath := filepath.Join(tmpDir, "game.zip") + zipFile, err := os.Create(zipPath) + if err != nil { + t.Fatal(err) + } + zipWriter := zip.NewWriter(zipFile) + packErr := packZip(zipWriter, projectDir, extraPaths) + closeZipErr := zipWriter.Close() + closeFileErr := zipFile.Close() + if packErr == nil { + t.Fatalf("PackZip() succeeded after external parent escaped root") + } + if closeZipErr != nil { + t.Fatal(closeZipErr) + } + if closeFileErr != nil { + t.Fatal(closeFileErr) + } + if snapshot := readZipSnapshot(t, zipPath); snapshot.counts["shared/bg.png"] != 0 { + t.Fatalf("escaped external asset was packed: %+v", snapshot.contents) + } } type zipSnapshot struct { @@ -293,17 +445,6 @@ func readZipFile(file *zip.File) (string, error) { return string(data), nil } -func assertZipEntryContent(t *testing.T, snapshot zipSnapshot, name, want string) { - t.Helper() - - if snapshot.counts[name] != 1 { - t.Fatalf("%s count = %d, want 1", name, snapshot.counts[name]) - } - if got := snapshot.contents[name]; got != want { - t.Fatalf("%s content = %q, want %q", name, got, want) - } -} - func writeTestFile(t *testing.T, filePath string, content string) { t.Helper() diff --git a/cmd/spx/internal/pack/project_paths.go b/cmd/spx/internal/pack/project_paths.go index 99c9848ab..8edba6921 100644 --- a/cmd/spx/internal/pack/project_paths.go +++ b/cmd/spx/internal/pack/project_paths.go @@ -17,7 +17,6 @@ package pack import ( - "encoding/json" "fmt" "os" "path" @@ -25,55 +24,48 @@ import ( "strings" spxfs "github.com/goplus/spx/v3/fs" - coreproject "github.com/goplus/spx/v3/internal/core/project" ) const ( - projectConfigName = ".config" - packedIndexName = "index_pack.json" - // engineExtAssetDir is the extasset zip root. - engineExtAssetDir = "extasset" - // sharedAssetEscapeDepth is the minimum "../" depth. + projectConfigName = ".config" + packDirName = "assets" + sourceIndexName = "index.json" + packedIndexName = "index_pack.json" + engineExtAssetDir = "extasset" sharedAssetEscapeDepth = 2 ) -type assetProjectConfig struct { - // ExtAsset is the external asset directory. - ExtAsset string `json:"extasset"` -} - type assetPathRef struct { configDir string path string } -type packedAssetIndex struct { - Project coreproject.ProjectConfig - Sprites map[string]coreproject.SpriteConfig - Sounds map[string]coreproject.SoundConfig - Fonts map[string]coreproject.FontFamilyConfig - HasFonts bool -} - -// collectExternalAssetPaths matches runtime asset lookup. -func collectExternalAssetPaths(baseFolder string, existingZipPaths map[string]struct{}) ([]DirInfos, error) { - assetRoot := filepath.Join(baseFolder, "assets") - info, err := os.Stat(assetRoot) - if os.IsNotExist(err) || (err == nil && !info.IsDir()) { +func collectExternalAssetPathsWithConfig(baseFolder string, existingZipPaths map[string]struct{}, configuredExtAssetDir *string) (extraPaths []dirInfo, err error) { + assetRoot := filepath.Join(baseFolder, packDirName) + info, err := os.Lstat(assetRoot) + if os.IsNotExist(err) { return nil, nil } if err != nil { return nil, err } + if info.Mode()&os.ModeSymlink != 0 || !info.IsDir() { + return nil, fmt.Errorf("projectassets: PackDir %q must be a real directory", packDirName) + } refs, err := collectAssetPathRefs(assetRoot) if err != nil { - return nil, err + return nil, fmt.Errorf("projectassets: validate asset indexes: %w", err) } - - extAssetDir, err := readExtAssetDir(baseFolder) - if err != nil { - return nil, err + extAssetDir := "" + if configuredExtAssetDir != nil { + extAssetDir = *configuredExtAssetDir + } else { + extAssetDir, err = readExtAssetDir(baseFolder) + if err != nil { + configPath := filepath.Join(baseFolder, projectConfigName) + return nil, fmt.Errorf("projectpolicy: parse project config %q: %w", configPath, err) + } } seen := make(map[string]struct{}, len(existingZipPaths)) @@ -83,8 +75,13 @@ func collectExternalAssetPaths(baseFolder string, existingZipPaths map[string]st assetRoot = cleanFilesystemPath(assetRoot) compatibilityRoot := sharedAssetCompatibilityRoot(assetRoot) + var externalRoot *os.Root + defer func() { + if err != nil && externalRoot != nil { + _ = externalRoot.Close() + } + }() - var extraPaths []DirInfos for _, ref := range refs { normalized := normalizeConfigPath(ref.configDir, ref.path) sourcePath, zipPath, ok := resolveExternalAssetPath(assetRoot, compatibilityRoot, extAssetDir, normalized) @@ -95,263 +92,58 @@ func collectExternalAssetPaths(baseFolder string, existingZipPaths map[string]st continue } - info, err := os.Stat(sourcePath) + if externalRoot == nil { + externalRoot, err = openPackRoot(compatibilityRoot) + if err != nil { + return nil, err + } + } + info, rootPath, err := inspectExternalAssetPath(externalRoot, compatibilityRoot, sourcePath) if err != nil { return nil, fmt.Errorf("stat external asset %s referenced by %q: %w", sourcePath, normalized, err) } - if info.IsDir() { - return nil, fmt.Errorf("external asset %s referenced by %q is a directory", sourcePath, normalized) - } seen[zipPath] = struct{}{} - extraPaths = append(extraPaths, DirInfos{path: sourcePath, info: info, zipPath: zipPath}) + extraPaths = append(extraPaths, dirInfo{ + path: sourcePath, info: info, zipPath: zipPath, + root: externalRoot, rootPath: rootPath, + }) } - return extraPaths, nil } -func collectAssetPathRefs(assetRoot string) ([]assetPathRef, error) { - var refs []assetPathRef - - packed, hasPacked, err := readPackedAssetIndex(assetRoot) +func inspectExternalAssetPath(root *os.Root, rootPath, name string) (os.FileInfo, string, error) { + rel, err := filepath.Rel(rootPath, name) if err != nil { - return nil, err + return nil, "", err } - - if hasPacked { - refs = appendProjectAssetRefs(refs, packed.Project) - } else { - projectConfigPath := filepath.Join(assetRoot, "index.json") - if _, err := os.Stat(projectConfigPath); err != nil { - if !os.IsNotExist(err) { - return nil, fmt.Errorf("stat %s: %w", projectConfigPath, err) - } - } else { - var conf coreproject.ProjectConfig - if err := readJSONFile(projectConfigPath, &conf); err != nil { - return nil, fmt.Errorf("parse %s: %w", projectConfigPath, err) - } - refs = appendProjectAssetRefs(refs, conf) - } + rel = filepath.Clean(rel) + if rel == "." || rel == ".." || filepath.IsAbs(rel) || strings.HasPrefix(rel, ".."+string(filepath.Separator)) { + return nil, "", fmt.Errorf("path is outside the compatibility root") } - spriteRefs, err := collectIndexedAssetRefs( - assetRoot, - "sprites", - packed.Sprites, - appendSpriteAssetRefs, - true, - ) - if err != nil { - return nil, err - } - refs = append(refs, spriteRefs...) - - soundRefs, err := collectIndexedAssetRefs( - assetRoot, - "sounds", - packed.Sounds, - appendSoundAssetRefs, - true, - ) - if err != nil { - return nil, err - } - refs = append(refs, soundRefs...) - - fontRefs, err := collectIndexedAssetRefs( - assetRoot, - "fonts", - packed.Fonts, - appendFontAssetRefs, - !hasPacked || !packed.HasFonts, - ) - if err != nil { - return nil, err - } - refs = append(refs, fontRefs...) - - return refs, nil -} - -func collectIndexedAssetRefs[T any]( - assetRoot string, - category string, - packed map[string]T, - appendRefs func([]assetPathRef, string, T) []assetPathRef, - scanSource bool, -) ([]assetPathRef, error) { - var refs []assetPathRef - - for name, conf := range packed { - refs = appendRefs(refs, path.Join(category, name), conf) - } - - if !scanSource { - return refs, nil - } - - configPaths, err := filepath.Glob(filepath.Join(assetRoot, category, "*", "index.json")) - if err != nil { - return nil, err - } - for _, configPath := range configPaths { - name := filepath.Base(filepath.Dir(configPath)) - if _, exists := packed[name]; exists { - continue - } - - configDir, err := relConfigDir(assetRoot, filepath.Dir(configPath)) + parts := strings.Split(rel, string(filepath.Separator)) + for i := range parts { + candidate := filepath.Join(parts[:i+1]...) + info, err := root.Lstat(candidate) if err != nil { - return nil, err + return nil, "", err } - - var conf T - if err := readJSONFile(configPath, &conf); err != nil { - return nil, fmt.Errorf("parse %s: %w", configPath, err) + if info.Mode()&os.ModeSymlink != 0 { + return nil, "", fmt.Errorf("must be a regular non-symlink path (symlink at %q)", candidate) } - refs = appendRefs(refs, configDir, conf) - } - - return refs, nil -} - -func appendProjectAssetRefs(refs []assetPathRef, conf coreproject.ProjectConfig) []assetPathRef { - for _, backdrop := range conf.Backdrops { - if backdrop != nil { - refs = appendAssetPathRef(refs, "", backdrop.Path) - } - } - refs = appendAssetPathRef(refs, "", conf.Bgm) - refs = appendAssetPathRef(refs, "", conf.TilemapPath) - return refs -} - -func appendSpriteAssetRefs(refs []assetPathRef, configDir string, conf coreproject.SpriteConfig) []assetPathRef { - for _, costume := range conf.Costumes { - if costume != nil { - refs = appendAssetPathRef(refs, configDir, costume.Path) - } - } - if conf.CostumeSet != nil && conf.CostumeSet.Path != "" { - refs = appendAssetPathRef(refs, configDir, conf.CostumeSet.Path) - } - if conf.CostumeMPSet != nil && conf.CostumeMPSet.Path != "" { - refs = appendAssetPathRef(refs, configDir, conf.CostumeMPSet.Path) - } - return refs -} - -func appendSoundAssetRefs(refs []assetPathRef, configDir string, conf coreproject.SoundConfig) []assetPathRef { - return appendAssetPathRef(refs, configDir, conf.Path) -} - -func appendFontAssetRefs(refs []assetPathRef, configDir string, conf coreproject.FontFamilyConfig) []assetPathRef { - for _, face := range conf.Faces { - refs = appendAssetPathRef(refs, configDir, face.Path) - } - return refs -} - -func readPackedAssetIndex(assetRoot string) (packedAssetIndex, bool, error) { - packedPath := filepath.Join(assetRoot, packedIndexName) - if _, err := os.Stat(packedPath); err != nil { - if os.IsNotExist(err) { - return packedAssetIndex{}, false, nil - } - return packedAssetIndex{}, false, fmt.Errorf("stat %s: %w", packedPath, err) - } - - var root map[string]json.RawMessage - if err := readJSONFile(packedPath, &root); err != nil { - return packedAssetIndex{}, false, fmt.Errorf("parse %s: %w", packedPath, err) - } - - sourceRoot, err := readSourceAssetIndexRoot(assetRoot) - if err != nil { - return packedAssetIndex{}, false, err - } - mergedRoot := mergePackedRootSections(root, sourceRoot) - - var packed packedAssetIndex - if err := decodePackedAssetSection(mergedRoot, &packed.Project); err != nil { - return packedAssetIndex{}, false, fmt.Errorf("parse %s root: %w", packedPath, err) - } - packed.Sprites = make(map[string]coreproject.SpriteConfig) - if err := decodePackedAssetObjects(root["sprites"], packed.Sprites); err != nil { - return packedAssetIndex{}, false, fmt.Errorf("parse %s sprites: %w", packedPath, err) - } - packed.Sounds = make(map[string]coreproject.SoundConfig) - if err := decodePackedAssetObjects(root["sounds"], packed.Sounds); err != nil { - return packedAssetIndex{}, false, fmt.Errorf("parse %s sounds: %w", packedPath, err) - } - packed.Fonts = make(map[string]coreproject.FontFamilyConfig) - _, packed.HasFonts = root["fonts"] - if err := decodePackedAssetObjects(root["fonts"], packed.Fonts); err != nil { - return packedAssetIndex{}, false, fmt.Errorf("parse %s fonts: %w", packedPath, err) - } - return packed, true, nil -} - -func readSourceAssetIndexRoot(assetRoot string) (map[string]json.RawMessage, error) { - projectConfigPath := filepath.Join(assetRoot, "index.json") - if _, err := os.Stat(projectConfigPath); err != nil { - if os.IsNotExist(err) { - return nil, nil + if i < len(parts)-1 { + if !info.IsDir() { + return nil, "", fmt.Errorf("path component %q is not a directory", candidate) + } + continue } - return nil, fmt.Errorf("stat %s: %w", projectConfigPath, err) - } - - var root map[string]json.RawMessage - if err := readJSONFile(projectConfigPath, &root); err != nil { - return nil, fmt.Errorf("parse %s: %w", projectConfigPath, err) - } - return root, nil -} - -func mergePackedRootSections(packedRoot map[string]json.RawMessage, sourceRoot map[string]json.RawMessage) map[string]json.RawMessage { - if len(sourceRoot) == 0 { - return packedRoot - } - - merged := make(map[string]json.RawMessage, len(sourceRoot)+len(packedRoot)) - for key, value := range sourceRoot { - merged[key] = value - } - for key, value := range packedRoot { - merged[key] = value - } - return merged -} - -func decodePackedAssetSection(root map[string]json.RawMessage, dest *coreproject.ProjectConfig) error { - if len(root) == 0 { - return nil - } - raw, err := json.Marshal(root) - if err != nil { - return err - } - return json.Unmarshal(raw, dest) -} - -func decodePackedAssetObjects[T any](raw json.RawMessage, dest map[string]T) error { - if len(raw) == 0 || string(raw) == "null" { - return nil - } - - entries := make(map[string]json.RawMessage) - if err := json.Unmarshal(raw, &entries); err != nil { - return err - } - for name, entry := range entries { - var conf T - if err := json.Unmarshal(entry, &conf); err != nil { - return fmt.Errorf("%s: %w", name, err) + if !info.Mode().IsRegular() { + return nil, "", fmt.Errorf("must be a regular non-symlink file") } - dest[name] = conf + return info, rel, nil } - return nil + return nil, "", fmt.Errorf("empty external asset path") } func appendAssetPathRef(refs []assetPathRef, configDir, relPath string) []assetPathRef { @@ -361,7 +153,6 @@ func appendAssetPathRef(refs []assetPathRef, configDir, relPath string) []assetP return append(refs, assetPathRef{configDir: configDir, path: relPath}) } -// resolveExternalAssetPath resolves external assets for packing. func resolveExternalAssetPath(assetRoot, compatibilityRoot, extAssetDir, relPath string) (string, string, bool) { if relPath == "" || strings.HasPrefix(relPath, "/") { return "", "", false @@ -381,7 +172,6 @@ func resolveExternalAssetPath(assetRoot, compatibilityRoot, extAssetDir, relPath if isWithinRoot(sourcePath, assetRoot) { return "", "", false } - // Allow legacy shared assets outside assets/. if leadingParentCount(relPath) < sharedAssetEscapeDepth || !isWithinRoot(sourcePath, compatibilityRoot) { return "", "", false } @@ -397,7 +187,6 @@ func resolveExternalAssetPath(assetRoot, compatibilityRoot, extAssetDir, relPath return sourcePath, zipPath, true } -// rewriteExtAssetZipPath rewrites extasset paths for the zip. func rewriteExtAssetZipPath(relPath, extAssetDir string) string { if extAssetDir == "" { return "" @@ -406,47 +195,21 @@ func rewriteExtAssetZipPath(relPath, extAssetDir string) string { segments := strings.Split(cleanFilesystemPath(relPath), "/") leadingParents := 0 for i, segment := range segments { - if segment == "" { + switch { + case segment == "": continue - } - if segment == ".." { + case segment == "..": leadingParents++ - continue - } - if segment != extAssetDir || leadingParents == 0 { + case segment != extAssetDir || leadingParents == 0: return "" + default: + suffix := filepath.Join(segments[i+1:]...) + return normalizeZipPath(filepath.Join(engineExtAssetDir, suffix)) } - - suffix := filepath.Join(segments[i+1:]...) - return normalizeZipPath(filepath.Join(engineExtAssetDir, suffix)) } - return "" } -func readExtAssetDir(baseFolder string) (string, error) { - configPath := filepath.Join(baseFolder, projectConfigName) - if _, err := os.Stat(configPath); os.IsNotExist(err) { - return "", nil - } else if err != nil { - return "", err - } - - var conf assetProjectConfig - if err := readJSONFile(configPath, &conf); err != nil { - return "", fmt.Errorf("parse %s: %w", configPath, err) - } - return conf.ExtAsset, nil -} - -func readJSONFile(filePath string, v any) error { - data, err := os.ReadFile(filePath) - if err != nil { - return err - } - return json.Unmarshal(data, v) -} - func relConfigDir(assetRoot, configDir string) (string, error) { rel, err := filepath.Rel(assetRoot, configDir) if err != nil { @@ -469,16 +232,16 @@ func normalizeConfigPath(configDir, relPath string) string { return path.Clean(path.Join(configDir, relPath)) } -func cleanFilesystemPath(path string) string { - return normalizeZipPath(filepath.Clean(path)) +func cleanFilesystemPath(name string) string { + return normalizeZipPath(filepath.Clean(name)) } func sharedAssetCompatibilityRoot(assetRoot string) string { return cleanFilesystemPath(filepath.Join(assetRoot, "..", "..")) } -func isWithinRoot(path, root string) bool { - rel, err := filepath.Rel(root, path) +func isWithinRoot(name, root string) bool { + rel, err := filepath.Rel(root, name) if err != nil { return false } @@ -497,3 +260,7 @@ func leadingParentCount(relPath string) int { } return count } + +func normalizeZipPath(name string) string { + return strings.ReplaceAll(name, "\\", "/") +} diff --git a/cmd/spx/internal/pack/project_validation.go b/cmd/spx/internal/pack/project_validation.go new file mode 100644 index 000000000..8820e1e67 --- /dev/null +++ b/cmd/spx/internal/pack/project_validation.go @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2021 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed 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 pack + +import ( + "fmt" + "os" + "path/filepath" +) + +type assetProjectConfig struct { + ExtAsset string `json:"extasset"` +} + +func validateLegacyPackInputs(baseFolder string) (string, error) { + rootInfo, err := os.Lstat(baseFolder) + if err != nil { + return "", fmt.Errorf("pack: inspect project directory %q: %w", baseFolder, err) + } + if rootInfo.Mode()&os.ModeSymlink != 0 || !rootInfo.IsDir() { + return "", fmt.Errorf("pack: project directory %q must be a real directory", baseFolder) + } + + extAssetDir, err := validateLegacyProjectConfig(baseFolder) + if err != nil { + return "", err + } + + assetRoot := filepath.Join(baseFolder, packDirName) + assetInfo, err := os.Lstat(assetRoot) + if err != nil { + if os.IsNotExist(err) { + return "", fmt.Errorf("projectassets: PackDir %q is missing", packDirName) + } + return "", fmt.Errorf("projectassets: inspect PackDir %q: %w", packDirName, err) + } + if assetInfo.Mode()&os.ModeSymlink != 0 || !assetInfo.IsDir() { + return "", fmt.Errorf("projectassets: PackDir %q must be a real directory", packDirName) + } + + hasIndex := false + for _, name := range []string{sourceIndexName, packedIndexName} { + indexPath := filepath.Join(assetRoot, name) + info, statErr := os.Lstat(indexPath) + if os.IsNotExist(statErr) { + continue + } + if statErr != nil { + return "", fmt.Errorf("projectassets: inspect %q: %w", indexPath, statErr) + } + if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() { + return "", fmt.Errorf("projectassets: asset index %q must be a regular non-symlink file", indexPath) + } + hasIndex = true + } + if !hasIndex { + return "", fmt.Errorf("projectassets: PackDir %q contains neither %q nor %q", packDirName, sourceIndexName, packedIndexName) + } + + return extAssetDir, nil +} + +func validateLegacyProjectConfig(baseFolder string) (string, error) { + configPath := filepath.Join(baseFolder, projectConfigName) + info, err := os.Lstat(configPath) + if os.IsNotExist(err) { + return "", nil + } + if err != nil { + return "", fmt.Errorf("projectpolicy: inspect project config %q: %w", configPath, err) + } + if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() { + return "", fmt.Errorf("projectpolicy: project config %q must be a regular non-symlink file", configPath) + } + extAssetDir, err := readExtAssetDir(baseFolder) + if err != nil { + return "", fmt.Errorf("projectpolicy: parse project config %q: %w", configPath, err) + } + return extAssetDir, nil +} + +func readExtAssetDir(baseFolder string) (string, error) { + configPath := filepath.Join(baseFolder, projectConfigName) + if _, err := os.Stat(configPath); os.IsNotExist(err) { + return "", nil + } else if err != nil { + return "", err + } + + var conf assetProjectConfig + if err := readJSONFile(configPath, &conf); err != nil { + return "", fmt.Errorf("parse %s: %w", configPath, err) + } + return conf.ExtAsset, nil +} diff --git a/cmd/spx/internal/pack/project_walk.go b/cmd/spx/internal/pack/project_walk.go new file mode 100644 index 000000000..f0a963f0a --- /dev/null +++ b/cmd/spx/internal/pack/project_walk.go @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2021 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed 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 pack + +import ( + "fmt" + "os" + "path/filepath" + "strings" +) + +func collectProjectPaths(baseFolder, destination string, projectRoot *os.Root) ([]dirInfo, error) { + destination, err := filepath.Abs(destination) + if err != nil { + return nil, err + } + var destinationInfo os.FileInfo + if info, err := os.Lstat(destination); err == nil { + destinationInfo = info + } else if !os.IsNotExist(err) { + return nil, err + } + skipDirs := map[string]struct{}{".git": {}, "project": {}} + paths := make([]dirInfo, 0) + err = filepath.Walk(baseFolder, func(name string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.Mode()&os.ModeSymlink != 0 { + return fmt.Errorf("project entry %s must not be a symlink", name) + } + if !info.IsDir() && !info.Mode().IsRegular() { + return fmt.Errorf("project entry %s must be a regular file or directory", name) + } + absolute, err := filepath.Abs(name) + if err != nil { + return err + } + if absolute == destination || (destinationInfo != nil && os.SameFile(info, destinationInfo)) { + return nil + } + rel, err := filepath.Rel(baseFolder, name) + if err != nil { + return err + } + if rel == "." { + return nil + } + if strings.HasSuffix(name, ".import") { + return nil + } + parts := strings.Split(rel, string(filepath.Separator)) + if len(parts) == 1 || (len(parts) == 2 && info.IsDir()) { + if _, ok := skipDirs[info.Name()]; ok { + if info.IsDir() { + return filepath.SkipDir + } + return nil + } + } + paths = append(paths, dirInfo{path: name, info: info, root: projectRoot, rootPath: rel}) + return nil + }) + return paths, err +} diff --git a/test/All/assets/index.json b/test/All/assets/index.json index cd811a7c3..5ff727eda 100644 --- a/test/All/assets/index.json +++ b/test/All/assets/index.json @@ -2,11 +2,11 @@ "backdrops": [ { "name": "backdrop1", - "path": "../../shared-assets/lake.jpg" + "path": "shared-assets/lake.jpg" }, { "name": "backdrop2", - "path": "../../shared-assets/bg.jpg" + "path": "shared-assets/bg.jpg" } ], "map": { diff --git a/test/shared-assets/bg.jpg b/test/All/assets/shared-assets/bg.jpg similarity index 100% rename from test/shared-assets/bg.jpg rename to test/All/assets/shared-assets/bg.jpg diff --git a/test/shared-assets/lake.jpg b/test/All/assets/shared-assets/lake.jpg old mode 100755 new mode 100644 similarity index 100% rename from test/shared-assets/lake.jpg rename to test/All/assets/shared-assets/lake.jpg diff --git a/test/shared-assets/monkey.png b/test/All/assets/shared-assets/monkey.png old mode 100755 new mode 100644 similarity index 100% rename from test/shared-assets/monkey.png rename to test/All/assets/shared-assets/monkey.png diff --git a/test/All/assets/sprites/SpControl/index.json b/test/All/assets/sprites/SpControl/index.json index 01e84a802..e739494c1 100644 --- a/test/All/assets/sprites/SpControl/index.json +++ b/test/All/assets/sprites/SpControl/index.json @@ -1,7 +1,7 @@ { "costumeSet": { "faceRight": 180, - "path": "../../../../shared-assets/monkey.png", + "path": "../../shared-assets/monkey.png", "nx": 96 }, "costumeIndex": 0, diff --git a/test/All/assets/sprites/SpEffect/index.json b/test/All/assets/sprites/SpEffect/index.json index 01e84a802..e739494c1 100644 --- a/test/All/assets/sprites/SpEffect/index.json +++ b/test/All/assets/sprites/SpEffect/index.json @@ -1,7 +1,7 @@ { "costumeSet": { "faceRight": 180, - "path": "../../../../shared-assets/monkey.png", + "path": "../../shared-assets/monkey.png", "nx": 96 }, "costumeIndex": 0, diff --git a/test/All/assets/sprites/SpEvent/index.json b/test/All/assets/sprites/SpEvent/index.json index 01e84a802..e739494c1 100644 --- a/test/All/assets/sprites/SpEvent/index.json +++ b/test/All/assets/sprites/SpEvent/index.json @@ -1,7 +1,7 @@ { "costumeSet": { "faceRight": 180, - "path": "../../../../shared-assets/monkey.png", + "path": "../../shared-assets/monkey.png", "nx": 96 }, "costumeIndex": 0, diff --git a/test/All/assets/sprites/SpLook/index.json b/test/All/assets/sprites/SpLook/index.json index 01e84a802..e739494c1 100644 --- a/test/All/assets/sprites/SpLook/index.json +++ b/test/All/assets/sprites/SpLook/index.json @@ -1,7 +1,7 @@ { "costumeSet": { "faceRight": 180, - "path": "../../../../shared-assets/monkey.png", + "path": "../../shared-assets/monkey.png", "nx": 96 }, "costumeIndex": 0, diff --git a/test/All/assets/sprites/SpMotion/index.json b/test/All/assets/sprites/SpMotion/index.json index 01e84a802..e739494c1 100644 --- a/test/All/assets/sprites/SpMotion/index.json +++ b/test/All/assets/sprites/SpMotion/index.json @@ -1,7 +1,7 @@ { "costumeSet": { "faceRight": 180, - "path": "../../../../shared-assets/monkey.png", + "path": "../../shared-assets/monkey.png", "nx": 96 }, "costumeIndex": 0, diff --git a/test/All/assets/sprites/SpOperator/index.json b/test/All/assets/sprites/SpOperator/index.json index 01e84a802..e739494c1 100644 --- a/test/All/assets/sprites/SpOperator/index.json +++ b/test/All/assets/sprites/SpOperator/index.json @@ -1,7 +1,7 @@ { "costumeSet": { "faceRight": 180, - "path": "../../../../shared-assets/monkey.png", + "path": "../../shared-assets/monkey.png", "nx": 96 }, "costumeIndex": 0, diff --git a/test/All/assets/sprites/SpPen/index.json b/test/All/assets/sprites/SpPen/index.json index 01e84a802..e739494c1 100644 --- a/test/All/assets/sprites/SpPen/index.json +++ b/test/All/assets/sprites/SpPen/index.json @@ -1,7 +1,7 @@ { "costumeSet": { "faceRight": 180, - "path": "../../../../shared-assets/monkey.png", + "path": "../../shared-assets/monkey.png", "nx": 96 }, "costumeIndex": 0, diff --git a/test/All/assets/sprites/SpSensing/index.json b/test/All/assets/sprites/SpSensing/index.json index 01e84a802..e739494c1 100644 --- a/test/All/assets/sprites/SpSensing/index.json +++ b/test/All/assets/sprites/SpSensing/index.json @@ -1,7 +1,7 @@ { "costumeSet": { "faceRight": 180, - "path": "../../../../shared-assets/monkey.png", + "path": "../../shared-assets/monkey.png", "nx": 96 }, "costumeIndex": 0, diff --git a/test/All/assets/sprites/SpSound/index.json b/test/All/assets/sprites/SpSound/index.json index 01e84a802..e739494c1 100644 --- a/test/All/assets/sprites/SpSound/index.json +++ b/test/All/assets/sprites/SpSound/index.json @@ -1,7 +1,7 @@ { "costumeSet": { "faceRight": 180, - "path": "../../../../shared-assets/monkey.png", + "path": "../../shared-assets/monkey.png", "nx": 96 }, "costumeIndex": 0, diff --git a/test/All/assets/sprites/SpUnique/index.json b/test/All/assets/sprites/SpUnique/index.json index 01e84a802..e739494c1 100644 --- a/test/All/assets/sprites/SpUnique/index.json +++ b/test/All/assets/sprites/SpUnique/index.json @@ -1,7 +1,7 @@ { "costumeSet": { "faceRight": 180, - "path": "../../../../shared-assets/monkey.png", + "path": "../../shared-assets/monkey.png", "nx": 96 }, "costumeIndex": 0, diff --git a/test/All/assets/sprites/SpVariable/index.json b/test/All/assets/sprites/SpVariable/index.json index 01e84a802..e739494c1 100644 --- a/test/All/assets/sprites/SpVariable/index.json +++ b/test/All/assets/sprites/SpVariable/index.json @@ -1,7 +1,7 @@ { "costumeSet": { "faceRight": 180, - "path": "../../../../shared-assets/monkey.png", + "path": "../../shared-assets/monkey.png", "nx": 96 }, "costumeIndex": 0, diff --git a/test/AnimationOnStartOnPlayAudio/assets/index.json b/test/AnimationOnStartOnPlayAudio/assets/index.json index ac7682b62..5e13121fa 100644 --- a/test/AnimationOnStartOnPlayAudio/assets/index.json +++ b/test/AnimationOnStartOnPlayAudio/assets/index.json @@ -2,7 +2,7 @@ "backdrops": [ { "name": "backdrop1", - "path": "../../shared-assets/lake.jpg" + "path": "shared-assets/lake.jpg" } ], "zorder": [ diff --git a/test/shared-assets/chomp.wav b/test/AnimationOnStartOnPlayAudio/assets/shared-assets/chomp.wav similarity index 100% rename from test/shared-assets/chomp.wav rename to test/AnimationOnStartOnPlayAudio/assets/shared-assets/chomp.wav diff --git a/test/AnimationOnStartOnPlayAudio/assets/shared-assets/lake.jpg b/test/AnimationOnStartOnPlayAudio/assets/shared-assets/lake.jpg new file mode 100644 index 000000000..b17679709 Binary files /dev/null and b/test/AnimationOnStartOnPlayAudio/assets/shared-assets/lake.jpg differ diff --git a/test/AnimationOnStartOnPlayAudio/assets/shared-assets/monkey.png b/test/AnimationOnStartOnPlayAudio/assets/shared-assets/monkey.png new file mode 100644 index 000000000..f5269f36a Binary files /dev/null and b/test/AnimationOnStartOnPlayAudio/assets/shared-assets/monkey.png differ diff --git a/test/shared-assets/monkey_clap.mp3 b/test/AnimationOnStartOnPlayAudio/assets/shared-assets/monkey_clap.mp3 old mode 100755 new mode 100644 similarity index 100% rename from test/shared-assets/monkey_clap.mp3 rename to test/AnimationOnStartOnPlayAudio/assets/shared-assets/monkey_clap.mp3 diff --git a/test/AnimationOnStartOnPlayAudio/assets/sounds/chomp/index.json b/test/AnimationOnStartOnPlayAudio/assets/sounds/chomp/index.json index 28c36ad66..cbfc8f917 100644 --- a/test/AnimationOnStartOnPlayAudio/assets/sounds/chomp/index.json +++ b/test/AnimationOnStartOnPlayAudio/assets/sounds/chomp/index.json @@ -1,5 +1,5 @@ { - "path": "../../../../shared-assets/chomp.wav", + "path": "../../shared-assets/chomp.wav", "rate": 11025, "sampleCount": 2912 } diff --git a/test/AnimationOnStartOnPlayAudio/assets/sounds/clap/index.json b/test/AnimationOnStartOnPlayAudio/assets/sounds/clap/index.json index f61633060..1f7403aaf 100644 --- a/test/AnimationOnStartOnPlayAudio/assets/sounds/clap/index.json +++ b/test/AnimationOnStartOnPlayAudio/assets/sounds/clap/index.json @@ -1,5 +1,5 @@ { - "path": "../../../../shared-assets/monkey_clap.mp3", + "path": "../../shared-assets/monkey_clap.mp3", "rate": 11025, "sampleCount": 2912 } diff --git a/test/AnimationOnStartOnPlayAudio/assets/sprites/Monkey/index.json b/test/AnimationOnStartOnPlayAudio/assets/sprites/Monkey/index.json index 647136e77..a544230ba 100644 --- a/test/AnimationOnStartOnPlayAudio/assets/sprites/Monkey/index.json +++ b/test/AnimationOnStartOnPlayAudio/assets/sprites/Monkey/index.json @@ -1,7 +1,7 @@ { "costumeSet": { "faceRight": 180, - "path": "../../../../shared-assets/monkey.png", + "path": "../../shared-assets/monkey.png", "nx": 96 }, "costumeIndex": 42, diff --git a/test/Bananas/assets/index.json b/test/Bananas/assets/index.json index f8d888b75..196cac245 100644 --- a/test/Bananas/assets/index.json +++ b/test/Bananas/assets/index.json @@ -2,7 +2,7 @@ "backdrops": [ { "name": "backdrop1", - "path": "../../shared-assets/lake.jpg" + "path": "shared-assets/lake.jpg" } ], "map": { diff --git a/test/shared-assets/banana.png b/test/Bananas/assets/shared-assets/banana.png old mode 100755 new mode 100644 similarity index 100% rename from test/shared-assets/banana.png rename to test/Bananas/assets/shared-assets/banana.png diff --git a/test/Bananas/assets/shared-assets/lake.jpg b/test/Bananas/assets/shared-assets/lake.jpg new file mode 100644 index 000000000..b17679709 Binary files /dev/null and b/test/Bananas/assets/shared-assets/lake.jpg differ diff --git a/test/Bananas/assets/shared-assets/monkey.png b/test/Bananas/assets/shared-assets/monkey.png new file mode 100644 index 000000000..f5269f36a Binary files /dev/null and b/test/Bananas/assets/shared-assets/monkey.png differ diff --git a/test/shared-assets/red.png b/test/Bananas/assets/shared-assets/red.png similarity index 100% rename from test/shared-assets/red.png rename to test/Bananas/assets/shared-assets/red.png diff --git a/test/Bananas/assets/sprites/Banana/index.json b/test/Bananas/assets/sprites/Banana/index.json index 463827f0c..f32388cdd 100644 --- a/test/Bananas/assets/sprites/Banana/index.json +++ b/test/Bananas/assets/sprites/Banana/index.json @@ -1,7 +1,7 @@ { "costumeSet": { "faceRight": 180, - "path": "../../../../shared-assets/banana.png", + "path": "../../shared-assets/banana.png", "nx": 58 }, "costumeIndex": 0, diff --git a/test/Bananas/assets/sprites/Monkey/index.json b/test/Bananas/assets/sprites/Monkey/index.json index 44bd40975..99808849d 100644 --- a/test/Bananas/assets/sprites/Monkey/index.json +++ b/test/Bananas/assets/sprites/Monkey/index.json @@ -1,7 +1,7 @@ { "costumeSet": { "faceRight": 180, - "path": "../../../../shared-assets/monkey.png", + "path": "../../shared-assets/monkey.png", "nx": 96 }, "costumeIndex": 0, diff --git a/test/Bananas/assets/sprites/Red/index.json b/test/Bananas/assets/sprites/Red/index.json index ede134b76..36dad7b1f 100644 --- a/test/Bananas/assets/sprites/Red/index.json +++ b/test/Bananas/assets/sprites/Red/index.json @@ -3,7 +3,7 @@ { "bitmapResolution": 2, "name": "red", - "path": "../../../../shared-assets/red.png", + "path": "../../shared-assets/red.png", "x": 72, "y": 17 } diff --git a/test/Bananas/runweb.sh b/test/Bananas/runweb.sh index 030a308fa..5ba4fa15f 100644 --- a/test/Bananas/runweb.sh +++ b/test/Bananas/runweb.sh @@ -3,7 +3,7 @@ mkdir res/ GOEXPERIMENT=noregabi GOOS=js GOARCH=wasm go build --tags canvas -o test.wasm cp -f "$(go env GOROOT)/lib/wasm/wasm_exec.js" ./go.wasm.exec.js cp -f "$(go env GOROOT)/lib/wasm/wasm_exec.html" ./go.wasm.exec.html -cp -f -p ../shared-assets/* ./res/ +cp -f -p ./assets/shared-assets/* ./res/ echo '// test.go diff --git a/test/Camera/assets/index.json b/test/Camera/assets/index.json index a9d181504..7b8228549 100644 --- a/test/Camera/assets/index.json +++ b/test/Camera/assets/index.json @@ -6,7 +6,7 @@ "backdrops": [ { "name": "backdrop1", - "path": "../../shared-assets/bg.jpg" + "path": "shared-assets/bg.jpg" } ], "camera": { diff --git a/test/Camera/assets/shared-assets/bg.jpg b/test/Camera/assets/shared-assets/bg.jpg new file mode 100644 index 000000000..a583b2ab9 Binary files /dev/null and b/test/Camera/assets/shared-assets/bg.jpg differ diff --git a/test/Camera/assets/shared-assets/chomp.wav b/test/Camera/assets/shared-assets/chomp.wav new file mode 100644 index 000000000..0af89632d Binary files /dev/null and b/test/Camera/assets/shared-assets/chomp.wav differ diff --git a/test/shared-assets/crocodile.png b/test/Camera/assets/shared-assets/crocodile.png old mode 100755 new mode 100644 similarity index 100% rename from test/shared-assets/crocodile.png rename to test/Camera/assets/shared-assets/crocodile.png diff --git a/test/Camera/assets/shared-assets/monkey.png b/test/Camera/assets/shared-assets/monkey.png new file mode 100644 index 000000000..f5269f36a Binary files /dev/null and b/test/Camera/assets/shared-assets/monkey.png differ diff --git a/test/Camera/assets/shared-assets/monkey_clap.mp3 b/test/Camera/assets/shared-assets/monkey_clap.mp3 new file mode 100644 index 000000000..0b6a9c4d4 Binary files /dev/null and b/test/Camera/assets/shared-assets/monkey_clap.mp3 differ diff --git a/test/Camera/assets/sounds/chomp/index.json b/test/Camera/assets/sounds/chomp/index.json index f340a502c..c4526a22c 100644 --- a/test/Camera/assets/sounds/chomp/index.json +++ b/test/Camera/assets/sounds/chomp/index.json @@ -1,5 +1,5 @@ { - "path": "../../../../shared-assets/chomp.wav", + "path": "../../shared-assets/chomp.wav", "rate": 11025, "sampleCount": 2912 } \ No newline at end of file diff --git a/test/Camera/assets/sounds/clap/index.json b/test/Camera/assets/sounds/clap/index.json index 29b896f68..b38e7eb8b 100644 --- a/test/Camera/assets/sounds/clap/index.json +++ b/test/Camera/assets/sounds/clap/index.json @@ -1,3 +1,3 @@ { - "path": "../../../../shared-assets/monkey_clap.mp3" + "path": "../../shared-assets/monkey_clap.mp3" } \ No newline at end of file diff --git a/test/Camera/assets/sprites/Crocodile/index.json b/test/Camera/assets/sprites/Crocodile/index.json index 35f8f2726..cfc429762 100644 --- a/test/Camera/assets/sprites/Crocodile/index.json +++ b/test/Camera/assets/sprites/Crocodile/index.json @@ -1,6 +1,6 @@ { "costumeSet": { - "path": "../../../../shared-assets/crocodile.png", + "path": "../../shared-assets/crocodile.png", "nx": 5, "items": [ { diff --git a/test/Camera/assets/sprites/Monkey/index.json b/test/Camera/assets/sprites/Monkey/index.json index 224d44500..8af175e45 100644 --- a/test/Camera/assets/sprites/Monkey/index.json +++ b/test/Camera/assets/sprites/Monkey/index.json @@ -1,7 +1,7 @@ { "costumeSet": { "faceRight": 180, - "path": "../../../../shared-assets/monkey.png", + "path": "../../shared-assets/monkey.png", "nx": 96 }, "costumeIndex": 0, diff --git a/test/CameraTouching/assets/index.json b/test/CameraTouching/assets/index.json index c6bdc6d51..b2cac0252 100644 --- a/test/CameraTouching/assets/index.json +++ b/test/CameraTouching/assets/index.json @@ -6,7 +6,7 @@ "backdrops": [ { "name": "backdrop1", - "path": "../../shared-assets/bg.jpg" + "path": "shared-assets/bg.jpg" } ], "zorder": [ diff --git a/test/CameraTouching/assets/shared-assets/bg.jpg b/test/CameraTouching/assets/shared-assets/bg.jpg new file mode 100644 index 000000000..a583b2ab9 Binary files /dev/null and b/test/CameraTouching/assets/shared-assets/bg.jpg differ diff --git a/test/CameraTouching/assets/shared-assets/chomp.wav b/test/CameraTouching/assets/shared-assets/chomp.wav new file mode 100644 index 000000000..0af89632d Binary files /dev/null and b/test/CameraTouching/assets/shared-assets/chomp.wav differ diff --git a/test/CameraTouching/assets/shared-assets/crocodile.png b/test/CameraTouching/assets/shared-assets/crocodile.png new file mode 100644 index 000000000..e0780f141 Binary files /dev/null and b/test/CameraTouching/assets/shared-assets/crocodile.png differ diff --git a/test/CameraTouching/assets/shared-assets/monkey.png b/test/CameraTouching/assets/shared-assets/monkey.png new file mode 100644 index 000000000..f5269f36a Binary files /dev/null and b/test/CameraTouching/assets/shared-assets/monkey.png differ diff --git a/test/CameraTouching/assets/shared-assets/monkey_clap.mp3 b/test/CameraTouching/assets/shared-assets/monkey_clap.mp3 new file mode 100644 index 000000000..0b6a9c4d4 Binary files /dev/null and b/test/CameraTouching/assets/shared-assets/monkey_clap.mp3 differ diff --git a/test/CameraTouching/assets/sounds/chomp/index.json b/test/CameraTouching/assets/sounds/chomp/index.json index f340a502c..c4526a22c 100644 --- a/test/CameraTouching/assets/sounds/chomp/index.json +++ b/test/CameraTouching/assets/sounds/chomp/index.json @@ -1,5 +1,5 @@ { - "path": "../../../../shared-assets/chomp.wav", + "path": "../../shared-assets/chomp.wav", "rate": 11025, "sampleCount": 2912 } \ No newline at end of file diff --git a/test/CameraTouching/assets/sounds/clap/index.json b/test/CameraTouching/assets/sounds/clap/index.json index 29b896f68..b38e7eb8b 100644 --- a/test/CameraTouching/assets/sounds/clap/index.json +++ b/test/CameraTouching/assets/sounds/clap/index.json @@ -1,3 +1,3 @@ { - "path": "../../../../shared-assets/monkey_clap.mp3" + "path": "../../shared-assets/monkey_clap.mp3" } \ No newline at end of file diff --git a/test/CameraTouching/assets/sprites/Crocodile/index.json b/test/CameraTouching/assets/sprites/Crocodile/index.json index 7fc4042e9..b561e2944 100644 --- a/test/CameraTouching/assets/sprites/Crocodile/index.json +++ b/test/CameraTouching/assets/sprites/Crocodile/index.json @@ -1,7 +1,7 @@ { "costumeSet": { "bitmapResolution": 2, - "path": "../../../../shared-assets/crocodile.png", + "path": "../../shared-assets/crocodile.png", "nx": 5, "items": [ { diff --git a/test/CameraTouching/assets/sprites/Monkey/index.json b/test/CameraTouching/assets/sprites/Monkey/index.json index 152887c5c..b367b9c42 100644 --- a/test/CameraTouching/assets/sprites/Monkey/index.json +++ b/test/CameraTouching/assets/sprites/Monkey/index.json @@ -2,7 +2,7 @@ "costumeSet": { "bitmapResolution": 2, "faceRight": 180, - "path": "../../../../shared-assets/monkey.png", + "path": "../../shared-assets/monkey.png", "nx": 96 }, "costumeIndex": 0, diff --git a/test/Hello/assets/index.json b/test/Hello/assets/index.json index d3977ad9e..a95db7966 100644 --- a/test/Hello/assets/index.json +++ b/test/Hello/assets/index.json @@ -2,7 +2,7 @@ "backdrops": [ { "name": "backdrop1", - "path": "../../shared-assets/lake.jpg" + "path": "shared-assets/lake.jpg" } ], "zorder": [ diff --git a/test/Hello/assets/shared-assets/crocodile.png b/test/Hello/assets/shared-assets/crocodile.png new file mode 100644 index 000000000..e0780f141 Binary files /dev/null and b/test/Hello/assets/shared-assets/crocodile.png differ diff --git a/test/Hello/assets/shared-assets/lake.jpg b/test/Hello/assets/shared-assets/lake.jpg new file mode 100644 index 000000000..b17679709 Binary files /dev/null and b/test/Hello/assets/shared-assets/lake.jpg differ diff --git a/test/Hello/assets/shared-assets/monkey.png b/test/Hello/assets/shared-assets/monkey.png new file mode 100644 index 000000000..f5269f36a Binary files /dev/null and b/test/Hello/assets/shared-assets/monkey.png differ diff --git a/test/Hello/assets/sprites/Crocodile/index.json b/test/Hello/assets/sprites/Crocodile/index.json index 57b323dda..2dcb7c203 100644 --- a/test/Hello/assets/sprites/Crocodile/index.json +++ b/test/Hello/assets/sprites/Crocodile/index.json @@ -1,6 +1,6 @@ { "costumeSet": { - "path": "../../../../shared-assets/crocodile.png", + "path": "../../shared-assets/crocodile.png", "nx": 5, "items": [ { diff --git a/test/Hello/assets/sprites/Monkey/index.json b/test/Hello/assets/sprites/Monkey/index.json index 2d02ba4ca..336c2a798 100644 --- a/test/Hello/assets/sprites/Monkey/index.json +++ b/test/Hello/assets/sprites/Monkey/index.json @@ -1,7 +1,7 @@ { "costumeSet": { "faceRight": 180, - "path": "../../../../shared-assets/monkey.png", + "path": "../../shared-assets/monkey.png", "nx": 96 }, "costumeIndex": 0, diff --git a/test/Measure/assets/index.json b/test/Measure/assets/index.json index 635316db0..0bda735fa 100644 --- a/test/Measure/assets/index.json +++ b/test/Measure/assets/index.json @@ -2,7 +2,7 @@ "backdrops": [ { "name": "backdrop1", - "path": "../../shared-assets/lake.jpg" + "path": "shared-assets/lake.jpg" } ], "zorder": [ diff --git a/test/Measure/assets/shared-assets/lake.jpg b/test/Measure/assets/shared-assets/lake.jpg new file mode 100644 index 000000000..b17679709 Binary files /dev/null and b/test/Measure/assets/shared-assets/lake.jpg differ diff --git a/test/Measure/assets/shared-assets/monkey.png b/test/Measure/assets/shared-assets/monkey.png new file mode 100644 index 000000000..f5269f36a Binary files /dev/null and b/test/Measure/assets/shared-assets/monkey.png differ diff --git a/test/Measure/assets/sprites/Monkey/index.json b/test/Measure/assets/sprites/Monkey/index.json index 04026d41f..b98fab2e5 100644 --- a/test/Measure/assets/sprites/Monkey/index.json +++ b/test/Measure/assets/sprites/Monkey/index.json @@ -1,7 +1,7 @@ { "costumeSet": { "faceRight": 180, - "path": "../../../../shared-assets/monkey.png", + "path": "../../shared-assets/monkey.png", "nx": 96 }, "costumeIndex": 0, diff --git a/test/MiniMapCamera/assets/shared-assets/crocodile.png b/test/MiniMapCamera/assets/shared-assets/crocodile.png new file mode 100644 index 000000000..e0780f141 Binary files /dev/null and b/test/MiniMapCamera/assets/shared-assets/crocodile.png differ diff --git a/test/MiniMapCamera/assets/shared-assets/monkey.png b/test/MiniMapCamera/assets/shared-assets/monkey.png new file mode 100644 index 000000000..f5269f36a Binary files /dev/null and b/test/MiniMapCamera/assets/shared-assets/monkey.png differ diff --git a/test/MiniMapCamera/assets/sprites/Crocodile/index.json b/test/MiniMapCamera/assets/sprites/Crocodile/index.json index f5da0bf02..9d6ee0d61 100644 --- a/test/MiniMapCamera/assets/sprites/Crocodile/index.json +++ b/test/MiniMapCamera/assets/sprites/Crocodile/index.json @@ -1,6 +1,6 @@ { "costumeSet": { - "path": "../../../../shared-assets/crocodile.png", + "path": "../../shared-assets/crocodile.png", "nx": 5, "items": [ { diff --git a/test/MiniMapCamera/assets/sprites/Monkey/index.json b/test/MiniMapCamera/assets/sprites/Monkey/index.json index 3cc4b0fb3..2411a3e4e 100644 --- a/test/MiniMapCamera/assets/sprites/Monkey/index.json +++ b/test/MiniMapCamera/assets/sprites/Monkey/index.json @@ -1,7 +1,7 @@ { "costumeSet": { "faceRight": 180, - "path": "../../../../shared-assets/monkey.png", + "path": "../../shared-assets/monkey.png", "nx": 96 }, "costumeIndex": 0, diff --git a/test/MonkeyAndCrocodile/assets/index.json b/test/MonkeyAndCrocodile/assets/index.json index e63d04522..3328d06e5 100644 --- a/test/MonkeyAndCrocodile/assets/index.json +++ b/test/MonkeyAndCrocodile/assets/index.json @@ -2,7 +2,7 @@ "backdrops": [ { "name": "backdrop1", - "path": "../../shared-assets/lake.jpg" + "path": "shared-assets/lake.jpg" } ], "zorder": [ diff --git a/test/MonkeyAndCrocodile/assets/shared-assets/chomp.wav b/test/MonkeyAndCrocodile/assets/shared-assets/chomp.wav new file mode 100644 index 000000000..0af89632d Binary files /dev/null and b/test/MonkeyAndCrocodile/assets/shared-assets/chomp.wav differ diff --git a/test/MonkeyAndCrocodile/assets/shared-assets/crocodile.png b/test/MonkeyAndCrocodile/assets/shared-assets/crocodile.png new file mode 100644 index 000000000..e0780f141 Binary files /dev/null and b/test/MonkeyAndCrocodile/assets/shared-assets/crocodile.png differ diff --git a/test/MonkeyAndCrocodile/assets/shared-assets/lake.jpg b/test/MonkeyAndCrocodile/assets/shared-assets/lake.jpg new file mode 100644 index 000000000..b17679709 Binary files /dev/null and b/test/MonkeyAndCrocodile/assets/shared-assets/lake.jpg differ diff --git a/test/MonkeyAndCrocodile/assets/shared-assets/monkey.png b/test/MonkeyAndCrocodile/assets/shared-assets/monkey.png new file mode 100644 index 000000000..f5269f36a Binary files /dev/null and b/test/MonkeyAndCrocodile/assets/shared-assets/monkey.png differ diff --git a/test/MonkeyAndCrocodile/assets/shared-assets/monkey_clap.mp3 b/test/MonkeyAndCrocodile/assets/shared-assets/monkey_clap.mp3 new file mode 100644 index 000000000..0b6a9c4d4 Binary files /dev/null and b/test/MonkeyAndCrocodile/assets/shared-assets/monkey_clap.mp3 differ diff --git a/test/MonkeyAndCrocodile/assets/sounds/chomp/index.json b/test/MonkeyAndCrocodile/assets/sounds/chomp/index.json index f340a502c..c4526a22c 100644 --- a/test/MonkeyAndCrocodile/assets/sounds/chomp/index.json +++ b/test/MonkeyAndCrocodile/assets/sounds/chomp/index.json @@ -1,5 +1,5 @@ { - "path": "../../../../shared-assets/chomp.wav", + "path": "../../shared-assets/chomp.wav", "rate": 11025, "sampleCount": 2912 } \ No newline at end of file diff --git a/test/MonkeyAndCrocodile/assets/sounds/clap/index.json b/test/MonkeyAndCrocodile/assets/sounds/clap/index.json index 29b896f68..b38e7eb8b 100644 --- a/test/MonkeyAndCrocodile/assets/sounds/clap/index.json +++ b/test/MonkeyAndCrocodile/assets/sounds/clap/index.json @@ -1,3 +1,3 @@ { - "path": "../../../../shared-assets/monkey_clap.mp3" + "path": "../../shared-assets/monkey_clap.mp3" } \ No newline at end of file diff --git a/test/MonkeyAndCrocodile/assets/sprites/Crocodile/index.json b/test/MonkeyAndCrocodile/assets/sprites/Crocodile/index.json index 35f8f2726..cfc429762 100644 --- a/test/MonkeyAndCrocodile/assets/sprites/Crocodile/index.json +++ b/test/MonkeyAndCrocodile/assets/sprites/Crocodile/index.json @@ -1,6 +1,6 @@ { "costumeSet": { - "path": "../../../../shared-assets/crocodile.png", + "path": "../../shared-assets/crocodile.png", "nx": 5, "items": [ { diff --git a/test/MonkeyAndCrocodile/assets/sprites/Monkey/index.json b/test/MonkeyAndCrocodile/assets/sprites/Monkey/index.json index 224d44500..8af175e45 100644 --- a/test/MonkeyAndCrocodile/assets/sprites/Monkey/index.json +++ b/test/MonkeyAndCrocodile/assets/sprites/Monkey/index.json @@ -1,7 +1,7 @@ { "costumeSet": { "faceRight": 180, - "path": "../../../../shared-assets/monkey.png", + "path": "../../shared-assets/monkey.png", "nx": 96 }, "costumeIndex": 0, diff --git a/test/Quote/assets/index.json b/test/Quote/assets/index.json index e63d04522..3328d06e5 100644 --- a/test/Quote/assets/index.json +++ b/test/Quote/assets/index.json @@ -2,7 +2,7 @@ "backdrops": [ { "name": "backdrop1", - "path": "../../shared-assets/lake.jpg" + "path": "shared-assets/lake.jpg" } ], "zorder": [ diff --git a/test/Quote/assets/shared-assets/chomp.wav b/test/Quote/assets/shared-assets/chomp.wav new file mode 100644 index 000000000..0af89632d Binary files /dev/null and b/test/Quote/assets/shared-assets/chomp.wav differ diff --git a/test/Quote/assets/shared-assets/crocodile.png b/test/Quote/assets/shared-assets/crocodile.png new file mode 100644 index 000000000..e0780f141 Binary files /dev/null and b/test/Quote/assets/shared-assets/crocodile.png differ diff --git a/test/Quote/assets/shared-assets/lake.jpg b/test/Quote/assets/shared-assets/lake.jpg new file mode 100644 index 000000000..b17679709 Binary files /dev/null and b/test/Quote/assets/shared-assets/lake.jpg differ diff --git a/test/Quote/assets/shared-assets/monkey.png b/test/Quote/assets/shared-assets/monkey.png new file mode 100644 index 000000000..f5269f36a Binary files /dev/null and b/test/Quote/assets/shared-assets/monkey.png differ diff --git a/test/Quote/assets/shared-assets/monkey_clap.mp3 b/test/Quote/assets/shared-assets/monkey_clap.mp3 new file mode 100644 index 000000000..0b6a9c4d4 Binary files /dev/null and b/test/Quote/assets/shared-assets/monkey_clap.mp3 differ diff --git a/test/Quote/assets/sounds/chomp/index.json b/test/Quote/assets/sounds/chomp/index.json index f340a502c..c4526a22c 100644 --- a/test/Quote/assets/sounds/chomp/index.json +++ b/test/Quote/assets/sounds/chomp/index.json @@ -1,5 +1,5 @@ { - "path": "../../../../shared-assets/chomp.wav", + "path": "../../shared-assets/chomp.wav", "rate": 11025, "sampleCount": 2912 } \ No newline at end of file diff --git a/test/Quote/assets/sounds/clap/index.json b/test/Quote/assets/sounds/clap/index.json index ed41630be..993be58bd 100644 --- a/test/Quote/assets/sounds/clap/index.json +++ b/test/Quote/assets/sounds/clap/index.json @@ -1,5 +1,5 @@ { - "path": "../../../../shared-assets/monkey_clap.mp3", + "path": "../../shared-assets/monkey_clap.mp3", "rate": 11025, "sampleCount": 2912 } \ No newline at end of file diff --git a/test/Quote/assets/sprites/Crocodile/index.json b/test/Quote/assets/sprites/Crocodile/index.json index e4dac06da..31c02707d 100644 --- a/test/Quote/assets/sprites/Crocodile/index.json +++ b/test/Quote/assets/sprites/Crocodile/index.json @@ -1,7 +1,7 @@ { "costumeSet": { "faceRight": 180, - "path": "../../../../shared-assets/crocodile.png", + "path": "../../shared-assets/crocodile.png", "nx": 5, "items": [ { diff --git a/test/Quote/assets/sprites/Monkey/index.json b/test/Quote/assets/sprites/Monkey/index.json index c11ba498c..a665b852d 100644 --- a/test/Quote/assets/sprites/Monkey/index.json +++ b/test/Quote/assets/sprites/Monkey/index.json @@ -1,7 +1,7 @@ { "costumeSet": { "faceRight": 180, - "path": "../../../../shared-assets/monkey.png", + "path": "../../shared-assets/monkey.png", "nx": 96 }, "costumeIndex": 0, diff --git a/test/Reloadable/assets/6.png b/test/Reloadable/assets/6.png new file mode 100644 index 000000000..b395ac94c Binary files /dev/null and b/test/Reloadable/assets/6.png differ diff --git a/test/Reloadable/assets/ch-2.json b/test/Reloadable/assets/ch-2.json index aff85ce28..807eb7410 100644 --- a/test/Reloadable/assets/ch-2.json +++ b/test/Reloadable/assets/ch-2.json @@ -2,7 +2,7 @@ "backdrops": [ { "name": "backdrop1", - "path": "../../Dinosaur/assets/6.png" + "path": "6.png" } ], "backdropIndex": 0, diff --git a/test/Reloadable/assets/index.json b/test/Reloadable/assets/index.json index d1437c37f..340278632 100644 --- a/test/Reloadable/assets/index.json +++ b/test/Reloadable/assets/index.json @@ -2,7 +2,7 @@ "backdrops": [ { "name": "backdrop1", - "path": "../../shared-assets/lake.jpg" + "path": "shared-assets/lake.jpg" } ], "zorder": [ diff --git a/test/Reloadable/assets/shared-assets/lake.jpg b/test/Reloadable/assets/shared-assets/lake.jpg new file mode 100644 index 000000000..b17679709 Binary files /dev/null and b/test/Reloadable/assets/shared-assets/lake.jpg differ diff --git a/test/Reloadable/assets/shared-assets/monkey.png b/test/Reloadable/assets/shared-assets/monkey.png new file mode 100644 index 000000000..f5269f36a Binary files /dev/null and b/test/Reloadable/assets/shared-assets/monkey.png differ diff --git a/test/Reloadable/assets/sprites/Monkey/index.json b/test/Reloadable/assets/sprites/Monkey/index.json index 44bd40975..99808849d 100644 --- a/test/Reloadable/assets/sprites/Monkey/index.json +++ b/test/Reloadable/assets/sprites/Monkey/index.json @@ -1,7 +1,7 @@ { "costumeSet": { "faceRight": 180, - "path": "../../../../shared-assets/monkey.png", + "path": "../../shared-assets/monkey.png", "nx": 96 }, "costumeIndex": 0, diff --git a/test/Reloadable/assets/sprites/Monkey2/index.json b/test/Reloadable/assets/sprites/Monkey2/index.json index 44bd40975..99808849d 100644 --- a/test/Reloadable/assets/sprites/Monkey2/index.json +++ b/test/Reloadable/assets/sprites/Monkey2/index.json @@ -1,7 +1,7 @@ { "costumeSet": { "faceRight": 180, - "path": "../../../../shared-assets/monkey.png", + "path": "../../shared-assets/monkey.png", "nx": 96 }, "costumeIndex": 0, diff --git a/test/StageMonitor/assets/index.json b/test/StageMonitor/assets/index.json index 016df0f7c..fbf0f577f 100644 --- a/test/StageMonitor/assets/index.json +++ b/test/StageMonitor/assets/index.json @@ -2,7 +2,7 @@ "backdrops": [ { "name": "backdrop1", - "path": "../../shared-assets/lake.jpg" + "path": "shared-assets/lake.jpg" } ], "zorder": [ diff --git a/test/StageMonitor/assets/shared-assets/lake.jpg b/test/StageMonitor/assets/shared-assets/lake.jpg new file mode 100644 index 000000000..b17679709 Binary files /dev/null and b/test/StageMonitor/assets/shared-assets/lake.jpg differ diff --git a/test/StageMonitor/assets/shared-assets/monkey.png b/test/StageMonitor/assets/shared-assets/monkey.png new file mode 100644 index 000000000..f5269f36a Binary files /dev/null and b/test/StageMonitor/assets/shared-assets/monkey.png differ diff --git a/test/StageMonitor/assets/sprites/Monkey/index.json b/test/StageMonitor/assets/sprites/Monkey/index.json index 04026d41f..b98fab2e5 100644 --- a/test/StageMonitor/assets/sprites/Monkey/index.json +++ b/test/StageMonitor/assets/sprites/Monkey/index.json @@ -1,7 +1,7 @@ { "costumeSet": { "faceRight": 180, - "path": "../../../../shared-assets/monkey.png", + "path": "../../shared-assets/monkey.png", "nx": 96 }, "costumeIndex": 0, diff --git a/test/TurnHeading/assets/index.json b/test/TurnHeading/assets/index.json index d1437c37f..340278632 100644 --- a/test/TurnHeading/assets/index.json +++ b/test/TurnHeading/assets/index.json @@ -2,7 +2,7 @@ "backdrops": [ { "name": "backdrop1", - "path": "../../shared-assets/lake.jpg" + "path": "shared-assets/lake.jpg" } ], "zorder": [ diff --git a/test/TurnHeading/assets/shared-assets/lake.jpg b/test/TurnHeading/assets/shared-assets/lake.jpg new file mode 100644 index 000000000..b17679709 Binary files /dev/null and b/test/TurnHeading/assets/shared-assets/lake.jpg differ diff --git a/test/TurnHeading/assets/shared-assets/monkey.png b/test/TurnHeading/assets/shared-assets/monkey.png new file mode 100644 index 000000000..f5269f36a Binary files /dev/null and b/test/TurnHeading/assets/shared-assets/monkey.png differ diff --git a/test/TurnHeading/assets/sprites/Monkey/index.json b/test/TurnHeading/assets/sprites/Monkey/index.json index 44bd40975..99808849d 100644 --- a/test/TurnHeading/assets/sprites/Monkey/index.json +++ b/test/TurnHeading/assets/sprites/Monkey/index.json @@ -1,7 +1,7 @@ { "costumeSet": { "faceRight": 180, - "path": "../../../../shared-assets/monkey.png", + "path": "../../shared-assets/monkey.png", "nx": 96 }, "costumeIndex": 0, diff --git a/test/TurnToMouse/assets/index.json b/test/TurnToMouse/assets/index.json index d1437c37f..340278632 100644 --- a/test/TurnToMouse/assets/index.json +++ b/test/TurnToMouse/assets/index.json @@ -2,7 +2,7 @@ "backdrops": [ { "name": "backdrop1", - "path": "../../shared-assets/lake.jpg" + "path": "shared-assets/lake.jpg" } ], "zorder": [ diff --git a/test/TurnToMouse/assets/shared-assets/lake.jpg b/test/TurnToMouse/assets/shared-assets/lake.jpg new file mode 100644 index 000000000..b17679709 Binary files /dev/null and b/test/TurnToMouse/assets/shared-assets/lake.jpg differ diff --git a/test/TurnToMouse/assets/shared-assets/monkey.png b/test/TurnToMouse/assets/shared-assets/monkey.png new file mode 100644 index 000000000..f5269f36a Binary files /dev/null and b/test/TurnToMouse/assets/shared-assets/monkey.png differ diff --git a/test/TurnToMouse/assets/sprites/Monkey/index.json b/test/TurnToMouse/assets/sprites/Monkey/index.json index 44bd40975..99808849d 100644 --- a/test/TurnToMouse/assets/sprites/Monkey/index.json +++ b/test/TurnToMouse/assets/sprites/Monkey/index.json @@ -1,7 +1,7 @@ { "costumeSet": { "faceRight": 180, - "path": "../../../../shared-assets/monkey.png", + "path": "../../shared-assets/monkey.png", "nx": 96 }, "costumeIndex": 0, diff --git a/test/TurnTogether/assets/index.json b/test/TurnTogether/assets/index.json index 72f877b96..4430385be 100644 --- a/test/TurnTogether/assets/index.json +++ b/test/TurnTogether/assets/index.json @@ -2,7 +2,7 @@ "costumes": [ { "name": "backdrop1", - "path": "../../shared-assets/lake.jpg" + "path": "shared-assets/lake.jpg" } ], "costumeIndex": 0, diff --git a/test/TurnTogether/assets/shared-assets/banana.png b/test/TurnTogether/assets/shared-assets/banana.png new file mode 100644 index 000000000..e3c2fea15 Binary files /dev/null and b/test/TurnTogether/assets/shared-assets/banana.png differ diff --git a/test/TurnTogether/assets/shared-assets/lake.jpg b/test/TurnTogether/assets/shared-assets/lake.jpg new file mode 100644 index 000000000..b17679709 Binary files /dev/null and b/test/TurnTogether/assets/shared-assets/lake.jpg differ diff --git a/test/TurnTogether/assets/shared-assets/monkey.png b/test/TurnTogether/assets/shared-assets/monkey.png new file mode 100644 index 000000000..f5269f36a Binary files /dev/null and b/test/TurnTogether/assets/shared-assets/monkey.png differ diff --git a/test/TurnTogether/assets/sprites/Banana/index.json b/test/TurnTogether/assets/sprites/Banana/index.json index 463827f0c..f32388cdd 100644 --- a/test/TurnTogether/assets/sprites/Banana/index.json +++ b/test/TurnTogether/assets/sprites/Banana/index.json @@ -1,7 +1,7 @@ { "costumeSet": { "faceRight": 180, - "path": "../../../../shared-assets/banana.png", + "path": "../../shared-assets/banana.png", "nx": 58 }, "costumeIndex": 0, diff --git a/test/TurnTogether/assets/sprites/Banana2/index.json b/test/TurnTogether/assets/sprites/Banana2/index.json index 463827f0c..f32388cdd 100644 --- a/test/TurnTogether/assets/sprites/Banana2/index.json +++ b/test/TurnTogether/assets/sprites/Banana2/index.json @@ -1,7 +1,7 @@ { "costumeSet": { "faceRight": 180, - "path": "../../../../shared-assets/banana.png", + "path": "../../shared-assets/banana.png", "nx": 58 }, "costumeIndex": 0, diff --git a/test/TurnTogether/assets/sprites/Monkey/index.json b/test/TurnTogether/assets/sprites/Monkey/index.json index 44bd40975..99808849d 100644 --- a/test/TurnTogether/assets/sprites/Monkey/index.json +++ b/test/TurnTogether/assets/sprites/Monkey/index.json @@ -1,7 +1,7 @@ { "costumeSet": { "faceRight": 180, - "path": "../../../../shared-assets/monkey.png", + "path": "../../shared-assets/monkey.png", "nx": 96 }, "costumeIndex": 0,