package platform import ( "archive/tar" "archive/zip" "compress/gzip" "context" "encoding/json" "fmt" "io" "os" "path/filepath" "strconv" "strings" "github.com/Eriyc/rules_wails/pkg/wails3kit/updates" ) func extractBundle(_ context.Context, archivePath string, format updates.ArtifactFormat) (string, updates.BundleManifest, error) { stageDir, err := os.MkdirTemp("", "wails3kit-stage-*") if err != nil { return "", updates.BundleManifest{}, err } switch format { case updates.ArtifactFormatZip: err = extractZip(archivePath, stageDir) case updates.ArtifactFormatTarGz: err = extractTarGz(archivePath, stageDir) default: err = fmt.Errorf("%w: unsupported format %s", updates.ErrInvalidArtifact, format) } if err != nil { _ = os.RemoveAll(stageDir) return "", updates.BundleManifest{}, err } manifestPath := filepath.Join(stageDir, "bundle.json") bytes, err := os.ReadFile(manifestPath) if err != nil { _ = os.RemoveAll(stageDir) return "", updates.BundleManifest{}, err } var manifest updates.BundleManifest if err := json.Unmarshal(bytes, &manifest); err != nil { _ = os.RemoveAll(stageDir) return "", updates.BundleManifest{}, err } if err := validateBundle(stageDir, manifest); err != nil { _ = os.RemoveAll(stageDir) return "", updates.BundleManifest{}, err } return stageDir, manifest, nil } func extractZip(archivePath string, targetDir string) error { reader, err := zip.OpenReader(archivePath) if err != nil { return err } defer reader.Close() for _, file := range reader.File { path, err := safeArchivePath(targetDir, file.Name) if err != nil { return err } if file.FileInfo().IsDir() { if err := os.MkdirAll(path, 0o755); err != nil { return err } continue } if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { return err } source, err := file.Open() if err != nil { return err } if err := writeFile(path, source, file.Mode()); err != nil { _ = source.Close() return err } _ = source.Close() } return nil } func extractTarGz(archivePath string, targetDir string) error { file, err := os.Open(archivePath) if err != nil { return err } defer file.Close() gzipReader, err := gzip.NewReader(file) if err != nil { return err } defer gzipReader.Close() reader := tar.NewReader(gzipReader) for { header, err := reader.Next() if err == io.EOF { return nil } if err != nil { return err } path, err := safeArchivePath(targetDir, header.Name) if err != nil { return err } switch header.Typeflag { case tar.TypeDir: if err := os.MkdirAll(path, 0o755); err != nil { return err } case tar.TypeReg: if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { return err } if err := writeFile(path, reader, os.FileMode(header.Mode)); err != nil { return err } default: return fmt.Errorf("%w: unsupported tar entry %s", updates.ErrInvalidArtifact, header.Name) } } } func validateBundle(stageDir string, manifest updates.BundleManifest) error { if manifest.SchemaVersion != 1 { return fmt.Errorf("%w: unsupported bundle schema %d", updates.ErrInvalidArtifact, manifest.SchemaVersion) } if !isSafeRelative(manifest.EntryPoint) { return fmt.Errorf("%w: invalid entrypoint", updates.ErrInvalidArtifact) } if len(manifest.Files) == 0 { return fmt.Errorf("%w: bundle contains no files", updates.ErrInvalidArtifact) } for _, file := range manifest.Files { if !isSafeRelative(file.Path) { return fmt.Errorf("%w: invalid bundle path %s", updates.ErrInvalidArtifact, file.Path) } if _, err := strconv.ParseUint(file.Mode, 8, 32); err != nil { return fmt.Errorf("%w: invalid mode %s", updates.ErrInvalidArtifact, file.Mode) } info, err := os.Stat(filepath.Join(stageDir, filepath.FromSlash(file.Path))) if err != nil { return err } if info.IsDir() { return fmt.Errorf("%w: file entry %s is a directory", updates.ErrInvalidArtifact, file.Path) } } return nil } func safeArchivePath(root string, name string) (string, error) { if !isSafeRelative(name) && strings.TrimSpace(name) != "bundle.json" { return "", fmt.Errorf("%w: unsafe archive path %s", updates.ErrInvalidArtifact, name) } return filepath.Join(root, filepath.FromSlash(name)), nil } func isSafeRelative(path string) bool { cleaned := filepath.Clean(filepath.FromSlash(path)) if cleaned == "." || cleaned == "" { return false } if filepath.IsAbs(cleaned) { return false } return !strings.HasPrefix(cleaned, "..") }