mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-04-21 18:26:38 +02:00
201 lines
4.5 KiB
Go
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
|
|
}
|