114 lines
2.7 KiB
Go
114 lines
2.7 KiB
Go
|
|
package skills
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"bytes"
|
|||
|
|
"crypto/sha256"
|
|||
|
|
"fmt"
|
|||
|
|
"io/fs"
|
|||
|
|
"os"
|
|||
|
|
"path"
|
|||
|
|
"path/filepath"
|
|||
|
|
"strings"
|
|||
|
|
"sync"
|
|||
|
|
|
|||
|
|
"cursor/internal/appdata"
|
|||
|
|
"cursor/internal/securefile"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
var operationMu sync.Mutex
|
|||
|
|
|
|||
|
|
func checkManagedPaths(project, name string) error {
|
|||
|
|
paths := []string{filepath.Join(project, ".cursor"), skillsDir(project), skillTargetDir(project, name), projectCommandsDir(project)}
|
|||
|
|
for _, file := range commandFileNames(name) {
|
|||
|
|
paths = append(paths, filepath.Join(projectCommandsDir(project), file))
|
|||
|
|
}
|
|||
|
|
for _, entry := range paths {
|
|||
|
|
info, err := os.Lstat(entry)
|
|||
|
|
if os.IsNotExist(err) {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
if err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
if info.Mode()&os.ModeSymlink != 0 {
|
|||
|
|
return fmt.Errorf("安全保护:拒绝通过符号链接修改 %s", entry)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
target := skillTargetDir(project, name)
|
|||
|
|
if _, err := os.Stat(target); os.IsNotExist(err) {
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
return filepath.WalkDir(target, func(p string, d fs.DirEntry, err error) error {
|
|||
|
|
if err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
if d.Type()&os.ModeSymlink != 0 {
|
|||
|
|
return fmt.Errorf("安全保护:skill 内含符号链接 %s", p)
|
|||
|
|
}
|
|||
|
|
return nil
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func backupBeforeReplace(target string, next []byte) error {
|
|||
|
|
previous, err := os.ReadFile(target)
|
|||
|
|
if os.IsNotExist(err) {
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
if err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
if bytes.Equal(previous, next) {
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
id := fmt.Sprintf("%x", sha256.Sum256([]byte(target)))
|
|||
|
|
dir := filepath.Join(appdata.DataRootPath(), "skill-backups", id[:16])
|
|||
|
|
if err = securefile.PrivateDir(dir); err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
file, err := os.CreateTemp(dir, filepath.Base(target)+"-*.bak")
|
|||
|
|
if err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
name := file.Name()
|
|||
|
|
if err = file.Close(); err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
return securefile.Write(name, previous)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func ensureNoCustomSkillFiles(project, name string) error {
|
|||
|
|
allowed := map[string]bool{}
|
|||
|
|
err := fs.WalkDir(builtinFS, path.Join(builtinRoot, name), func(p string, d fs.DirEntry, err error) error {
|
|||
|
|
if err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
if !d.IsDir() {
|
|||
|
|
rel := strings.TrimPrefix(p, path.Join(builtinRoot, name)+"/")
|
|||
|
|
allowed[rel] = true
|
|||
|
|
}
|
|||
|
|
return nil
|
|||
|
|
})
|
|||
|
|
if err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
for _, file := range retiredSkillFiles[name] {
|
|||
|
|
allowed[file] = true
|
|||
|
|
}
|
|||
|
|
target := skillTargetDir(project, name)
|
|||
|
|
if _, err = os.Stat(target); os.IsNotExist(err) {
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
return filepath.WalkDir(target, func(p string, d fs.DirEntry, err error) error {
|
|||
|
|
if err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
if !d.IsDir() {
|
|||
|
|
rel, _ := filepath.Rel(target, p)
|
|||
|
|
if !allowed[filepath.ToSlash(rel)] {
|
|||
|
|
return fmt.Errorf("skill 含自建文件 %s,已保留;请先备份后手动移除", strings.TrimPrefix(p, target))
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return nil
|
|||
|
|
})
|
|||
|
|
}
|