package appexport

import (
	"fmt"
	"path/filepath"
	"regexp"
	"strings"
)

// safeSegment matches a single safe path component: starts with an
// alphanumeric, then alphanumerics / dot / dash / underscore. It cannot be
// "." or ".." (must start alnum), cannot contain a path separator, and cannot
// be an absolute path. This covers the legitimate values these fields hold —
// app slugs (e.g. "paperless-ngx"), HDD mount basenames (e.g. "felhom-usb"),
// and docker volume names (e.g. "adventurelog_postgres_data").
var safeSegment = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9._-]*$`)

// ValidateSegment rejects any value that is not a single safe path component.
// It is the guard for [CTRL-001]: manifest fields that reach filepath.Join with
// a trusted base (AppName, HDDSubdirs, VolumeNames) are fully attacker-controlled
// JSON inside an imported .fab, so a value like "../../etc/cron.d/x" must be
// refused before it can escape the stacks / HDD destination directory.
//
// NOTE: this is deliberately NOT applied to manifest.ConfigFiles — those are
// dotfile-bearing names (e.g. ".felhom.yml") that are never used in a restore
// join (restoreConfig enumerates the extracted dir via os.ReadDir, whose names
// are already single components).
func ValidateSegment(kind, s string) error {
	if s == "" {
		return fmt.Errorf("appexport: empty %s", kind)
	}
	if s == "." || s == ".." {
		return fmt.Errorf("appexport: %s %q is a path-traversal segment", kind, s)
	}
	if strings.ContainsAny(s, `/\`) || strings.ContainsRune(s, filepath.Separator) {
		return fmt.Errorf("appexport: %s %q must not contain a path separator", kind, s)
	}
	if filepath.IsAbs(s) {
		return fmt.Errorf("appexport: %s %q must not be an absolute path", kind, s)
	}
	if !safeSegment.MatchString(s) {
		return fmt.Errorf("appexport: %s %q is not a safe single-segment name", kind, s)
	}
	return nil
}

// validateManifestPaths checks every manifest field that is later used as a
// path segment in a filepath.Join against a trusted base. Called from
// UnmarshalManifest so a hostile bundle fails the parse, before executeImport
// can MkdirAll/extract into a traversed location.
func validateManifestPaths(m *Manifest) error {
	if err := ValidateSegment("app_name", m.AppName); err != nil {
		return err
	}
	for _, s := range m.HDDSubdirs {
		if err := ValidateSegment("hdd_subdir", s); err != nil {
			return err
		}
	}
	for _, v := range m.VolumeNames {
		if err := ValidateSegment("volume_name", v); err != nil {
			return err
		}
	}
	return nil
}
