package handler import ( "errors" "path/filepath" "strings" ) // URL ↔ filesystem path math used by several handler files. Pure // string manipulation — no I/O, no policy decisions — so it lives // in its own file rather than being attached to any one feature. // resolvePath translates a URL `path=` query (relative to fsRoot, with // '/' separator and leading '/') into an absolute filesystem path. It // rejects path traversal and any segment beginning with '.' or '_' so // reserved namespaces (e.g. .devshell) cannot be addressed through // admin APIs. Returns the cleaned absolute path or an error suitable // for a 404. func resolvePath(fsRoot, urlPath string) (string, error) { urlPath = strings.TrimSpace(urlPath) if urlPath == "" { urlPath = "/" } if !strings.HasPrefix(urlPath, "/") { return "", errors.New("path must be absolute (start with /)") } cleanURL := filepath.ToSlash(filepath.Clean(urlPath)) // Reject reserved-prefix segments so callers cannot create // .foo/.zddc or _bar/.zddc through admin APIs. for _, seg := range strings.Split(strings.Trim(cleanURL, "/"), "/") { if seg == "" { continue } if strings.HasPrefix(seg, ".") || strings.HasPrefix(seg, "_") { return "", errors.New("reserved-prefix path segment") } } rel := strings.TrimPrefix(cleanURL, "/") abs := filepath.Join(fsRoot, filepath.FromSlash(rel)) abs = filepath.Clean(abs) // Path containment. if abs != fsRoot && !strings.HasPrefix(abs, fsRoot+string(filepath.Separator)) { return "", errors.New("path escapes root") } return abs, nil } // urlPathOf produces the URL form of an absolute filesystem path under // fsRoot. Returns "/" for fsRoot itself, otherwise "/". func urlPathOf(fsRoot, abs string) string { if abs == fsRoot { return "/" } rel, err := filepath.Rel(fsRoot, abs) if err != nil { return "/" } return "/" + filepath.ToSlash(rel) } // chainDirs reproduces EffectivePolicy's directory walk so callers can // label each policy-chain level with the directory it came from. Used // by the virtual-.zddc body to annotate which ancestor contributed // which rule. func chainDirs(fsRoot, dirPath string) []string { fsRoot = filepath.Clean(fsRoot) dirPath = filepath.Clean(dirPath) dirs := []string{fsRoot} if dirPath == fsRoot { return dirs } rel, err := filepath.Rel(fsRoot, dirPath) if err != nil || rel == "." { return dirs } current := fsRoot for _, part := range strings.Split(rel, string(filepath.Separator)) { current = filepath.Join(current, part) dirs = append(dirs, current) } return dirs }