189 lines
4.5 KiB
Go
189 lines
4.5 KiB
Go
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, "..")
|
|
}
|