Files
CyberStrikeAI/internal/skillpackage/io.go
T
2026-04-19 01:25:30 +08:00

201 lines
4.5 KiB
Go

package skillpackage
import (
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"
)
const (
maxPackageFiles = 4000
maxPackageDepth = 24
maxScriptsDepth = 24
defaultMaxRead = 10 << 20
)
// SafeRelPath resolves rel inside root (no ..).
func SafeRelPath(root, rel string) (string, error) {
rel = strings.TrimSpace(rel)
rel = filepath.ToSlash(rel)
rel = strings.TrimPrefix(rel, "/")
if rel == "" || rel == "." {
return "", fmt.Errorf("empty resource path")
}
if strings.Contains(rel, "..") {
return "", fmt.Errorf("invalid path %q", rel)
}
abs := filepath.Join(root, filepath.FromSlash(rel))
cleanRoot := filepath.Clean(root)
cleanAbs := filepath.Clean(abs)
relOut, err := filepath.Rel(cleanRoot, cleanAbs)
if err != nil || relOut == ".." || strings.HasPrefix(relOut, ".."+string(filepath.Separator)) {
return "", fmt.Errorf("path escapes skill directory: %q", rel)
}
return cleanAbs, nil
}
// ListPackageFiles lists files under a skill directory.
func ListPackageFiles(skillsRoot, skillID string) ([]PackageFileInfo, error) {
root := SkillDir(skillsRoot, skillID)
if _, err := ResolveSKILLPath(root); err != nil {
return nil, fmt.Errorf("skill %q: %w", skillID, err)
}
var out []PackageFileInfo
err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
rel, e := filepath.Rel(root, path)
if e != nil {
return e
}
if rel == "." {
return nil
}
depth := strings.Count(rel, string(os.PathSeparator))
if depth > maxPackageDepth {
if d.IsDir() {
return filepath.SkipDir
}
return nil
}
if strings.HasPrefix(d.Name(), ".") {
if d.IsDir() {
return filepath.SkipDir
}
return nil
}
if len(out) >= maxPackageFiles {
return fmt.Errorf("skill package exceeds %d files", maxPackageFiles)
}
fi, err := d.Info()
if err != nil {
return err
}
out = append(out, PackageFileInfo{
Path: filepath.ToSlash(rel),
Size: fi.Size(),
IsDir: d.IsDir(),
})
return nil
})
return out, err
}
// ReadPackageFile reads a file relative to the skill package.
func ReadPackageFile(skillsRoot, skillID, relPath string, maxBytes int64) ([]byte, error) {
if maxBytes <= 0 {
maxBytes = defaultMaxRead
}
root := SkillDir(skillsRoot, skillID)
abs, err := SafeRelPath(root, relPath)
if err != nil {
return nil, err
}
fi, err := os.Stat(abs)
if err != nil {
return nil, err
}
if fi.IsDir() {
return nil, fmt.Errorf("path is a directory")
}
if fi.Size() > maxBytes {
return readFileHead(abs, maxBytes)
}
return os.ReadFile(abs)
}
// WritePackageFile writes a file inside the skill package.
func WritePackageFile(skillsRoot, skillID, relPath string, content []byte) error {
root := SkillDir(skillsRoot, skillID)
if _, err := ResolveSKILLPath(root); err != nil {
return fmt.Errorf("skill %q: %w", skillID, err)
}
abs, err := SafeRelPath(root, relPath)
if err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(abs), 0755); err != nil {
return err
}
return os.WriteFile(abs, content, 0644)
}
func readFileHead(path string, max int64) ([]byte, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
buf := make([]byte, max)
n, err := f.Read(buf)
if err != nil && n == 0 {
return nil, err
}
return buf[:n], nil
}
func listScripts(skillsRoot, skillID string) ([]SkillScriptInfo, error) {
root := filepath.Join(SkillDir(skillsRoot, skillID), "scripts")
st, err := os.Stat(root)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, err
}
if !st.IsDir() {
return nil, nil
}
var out []SkillScriptInfo
err = filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
if err != nil {
return err
}
rel, e := filepath.Rel(root, path)
if e != nil {
return e
}
if rel == "." {
return nil
}
if d.IsDir() {
if strings.HasPrefix(d.Name(), ".") {
return filepath.SkipDir
}
if strings.Count(rel, string(os.PathSeparator)) >= maxScriptsDepth {
return filepath.SkipDir
}
return nil
}
if strings.HasPrefix(d.Name(), ".") {
return nil
}
relSkill := filepath.Join("scripts", rel)
full := filepath.Join(root, rel)
fi, err := os.Stat(full)
if err != nil || fi.IsDir() {
return nil
}
out = append(out, SkillScriptInfo{
Name: filepath.Base(rel),
RelPath: filepath.ToSlash(relSkill),
Size: fi.Size(),
})
return nil
})
return out, err
}
func countNonDirFiles(files []PackageFileInfo) int {
n := 0
for _, f := range files {
if !f.IsDir && f.Path != "SKILL.md" {
n++
}
}
return n
}