530 lines
18 KiB
Go
530 lines
18 KiB
Go
package upgrade
|
|
|
|
import (
|
|
"errors"
|
|
"io"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/testseam"
|
|
)
|
|
|
|
func TestCrossPlatformCoverageSkillPathMoveSystemErrors(t *testing.T) {
|
|
t.Run("destination probe", func(t *testing.T) {
|
|
src, dst := makeSkillMoveFixture(t)
|
|
original := skillPathLstat
|
|
testseam.Swap(t, &skillPathLstat, func(path string) (os.FileInfo, error) {
|
|
if path == dst {
|
|
return nil, os.ErrPermission
|
|
}
|
|
return original(path)
|
|
})
|
|
if err := moveSkillPathRecoverably(src, dst); !errors.Is(err, os.ErrPermission) {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("destination parent", func(t *testing.T) {
|
|
src, dst := makeSkillMoveFixture(t)
|
|
testseam.Swap(t, &skillPathMkdirAll, func(string, os.FileMode) error { return os.ErrPermission })
|
|
if err := moveSkillPathRecoverably(src, dst); !errors.Is(err, os.ErrPermission) {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("staging creation", func(t *testing.T) {
|
|
src, dst := makeSkillMoveFixture(t)
|
|
forceCrossDeviceRename(t, src, dst)
|
|
testseam.Swap(t, &skillPathMkdirTemp, func(string, string) (string, error) { return "", os.ErrPermission })
|
|
if err := moveSkillPathRecoverably(src, dst); !errors.Is(err, os.ErrPermission) {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("failed staging cleanup", func(t *testing.T) {
|
|
src, dst := makeSkillMoveFixture(t)
|
|
forceCrossDeviceRename(t, src, dst)
|
|
testseam.Swap(t, &skillPathCopy, func(string, string) error { return errors.New("copy failed") })
|
|
testseam.Swap(t, &skillPathRemoveAll, func(string) error { return errors.New("cleanup failed") })
|
|
err := moveSkillPathRecoverably(src, dst)
|
|
if err == nil || !strings.Contains(err.Error(), "copy failed") || !strings.Contains(err.Error(), "cleanup failed") {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("missing copied staging", func(t *testing.T) {
|
|
src, dst := makeSkillMoveFixture(t)
|
|
forceCrossDeviceRename(t, src, dst)
|
|
testseam.Swap(t, &skillPathCopy, func(string, string) error { return nil })
|
|
testseam.Swap(t, &skillPathVerify, func(string, string) error { return nil })
|
|
if err := moveSkillPathRecoverably(src, dst); err == nil || !strings.Contains(err.Error(), "检查跨设备") {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
})
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
path func(src, dst string) string
|
|
want string
|
|
}{
|
|
{name: "prepare staging mode", path: func(_, dst string) string { return filepath.Join(filepath.Dir(dst), "payload") }, want: "准备跨设备"},
|
|
{name: "restore published mode", path: func(_, dst string) string { return dst }, want: "恢复已发布"},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
src, dst := makeSkillMoveFixture(t)
|
|
forceCrossDeviceRename(t, src, dst)
|
|
original := skillPathChmod
|
|
testseam.Swap(t, &skillPathChmod, func(path string, mode os.FileMode) error {
|
|
if tc.name == "prepare staging mode" && strings.HasSuffix(path, string(filepath.Separator)+"payload") {
|
|
return errors.New("chmod failed")
|
|
}
|
|
if tc.name == "restore published mode" && path == dst {
|
|
return errors.New("chmod failed")
|
|
}
|
|
return original(path, mode)
|
|
})
|
|
if err := moveSkillPathRecoverably(src, dst); err == nil || !strings.Contains(err.Error(), tc.want) {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
t.Run("published staging cleanup", func(t *testing.T) {
|
|
src, dst := makeSkillMoveFixture(t)
|
|
forceCrossDeviceRename(t, src, dst)
|
|
original := skillPathRemoveAll
|
|
testseam.Swap(t, &skillPathRemoveAll, func(path string) error {
|
|
if strings.Contains(filepath.Base(path), ".cross-device-") {
|
|
return errors.New("cleanup failed")
|
|
}
|
|
return original(path)
|
|
})
|
|
if err := moveSkillPathRecoverably(src, dst); err == nil || !strings.Contains(err.Error(), "清理已发布") {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("source deletion cannot be confirmed", func(t *testing.T) {
|
|
src, dst := makeSkillMoveFixture(t)
|
|
forceCrossDeviceRename(t, src, dst)
|
|
removalAttempted := false
|
|
originalLstat := skillPathLstat
|
|
originalRemove := skillPathRemoveAll
|
|
testseam.Swap(t, &skillPathRemoveAll, func(path string) error {
|
|
if path == src {
|
|
removalAttempted = true
|
|
return nil
|
|
}
|
|
return originalRemove(path)
|
|
})
|
|
testseam.Swap(t, &skillPathLstat, func(path string) (os.FileInfo, error) {
|
|
if path == src && removalAttempted {
|
|
return nil, os.ErrPermission
|
|
}
|
|
return originalLstat(path)
|
|
})
|
|
if err := moveSkillPathRecoverably(src, dst); !errors.Is(err, os.ErrPermission) {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestCrossPlatformCoverageSkillPathPermissionRestorationErrors(t *testing.T) {
|
|
t.Run("source preparation error", func(t *testing.T) {
|
|
testseam.Swap(t, &skillPathWalkDir, func(string, fs.WalkDirFunc) error {
|
|
return errors.New("prepare failed")
|
|
})
|
|
if err := removePublishedSkillSource("ignored"); err == nil || !strings.Contains(err.Error(), "prepare failed") {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("walk error", func(t *testing.T) {
|
|
testseam.Swap(t, &skillPathWalkDir, func(root string, fn fs.WalkDirFunc) error {
|
|
return fn(root, nil, errors.New("walk failed"))
|
|
})
|
|
if _, err := prepareSkillPathTreeRemoval("ignored"); err == nil || !strings.Contains(err.Error(), "walk failed") {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("writable mode preparation", func(t *testing.T) {
|
|
entry := skillPathModeDirEntry{mode: os.ModeDir | 0o500}
|
|
testseam.Swap(t, &skillPathWalkDir, func(root string, fn fs.WalkDirFunc) error {
|
|
return fn(root, entry, nil)
|
|
})
|
|
var chmodMode os.FileMode
|
|
testseam.Swap(t, &skillPathChmod, func(_ string, mode os.FileMode) error {
|
|
chmodMode = mode
|
|
return nil
|
|
})
|
|
modes, err := prepareSkillPathTreeRemoval("source")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if chmodMode != 0o700 || len(modes) != 1 || modes[0].path != "source" || modes[0].mode != 0o500 {
|
|
t.Fatalf("prepared mode = %v, records = %#v", chmodMode, modes)
|
|
}
|
|
})
|
|
|
|
t.Run("writable mode preparation chmod error", func(t *testing.T) {
|
|
entry := skillPathModeDirEntry{mode: os.ModeDir | 0o500}
|
|
testseam.Swap(t, &skillPathWalkDir, func(root string, fn fs.WalkDirFunc) error {
|
|
return fn(root, entry, nil)
|
|
})
|
|
testseam.Swap(t, &skillPathChmod, func(string, os.FileMode) error {
|
|
return errors.New("chmod failed")
|
|
})
|
|
if _, err := prepareSkillPathTreeRemoval("source"); err == nil || !strings.Contains(err.Error(), "chmod failed") {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("entry info error", func(t *testing.T) {
|
|
testseam.Swap(t, &skillPathWalkDir, func(root string, fn fs.WalkDirFunc) error {
|
|
return fn(root, skillPathErrorDirEntry{}, nil)
|
|
})
|
|
if _, err := prepareSkillPathTreeRemoval("ignored"); err == nil || !strings.Contains(err.Error(), "info failed") {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("missing restored directory", func(t *testing.T) {
|
|
if err := restoreSkillPathDirModes([]skillPathDirMode{{path: filepath.Join(t.TempDir(), "missing"), mode: 0o555}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
|
|
t.Run("restore probe error", func(t *testing.T) {
|
|
testseam.Swap(t, &skillPathLstat, func(string) (os.FileInfo, error) { return nil, os.ErrPermission })
|
|
if err := restoreSkillPathDirModes([]skillPathDirMode{{path: "blocked", mode: 0o555}}); !errors.Is(err, os.ErrPermission) {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("restore chmod error", func(t *testing.T) {
|
|
path := t.TempDir()
|
|
testseam.Swap(t, &skillPathChmod, func(string, os.FileMode) error { return os.ErrPermission })
|
|
if err := restoreSkillPathDirModes([]skillPathDirMode{{path: path, mode: 0o555}}); !errors.Is(err, os.ErrPermission) {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestCrossPlatformCoverageSkillPathLexicalCopyErrors(t *testing.T) {
|
|
t.Run("source lstat", func(t *testing.T) {
|
|
if err := copySkillPathLexically(filepath.Join(t.TempDir(), "missing"), "unused"); !os.IsNotExist(err) {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("destination mkdir", func(t *testing.T) {
|
|
root := t.TempDir()
|
|
src, dst := filepath.Join(root, "src"), filepath.Join(root, "dst")
|
|
if err := os.Mkdir(src, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(dst, []byte("occupied"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := copySkillPathLexically(src, dst); err == nil {
|
|
t.Fatal("expected mkdir failure")
|
|
}
|
|
})
|
|
|
|
t.Run("source readdir", func(t *testing.T) {
|
|
root := t.TempDir()
|
|
src := filepath.Join(root, "src")
|
|
if err := os.Mkdir(src, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
testseam.Swap(t, &skillPathReadDir, func(string) ([]os.DirEntry, error) { return nil, os.ErrPermission })
|
|
if err := copySkillPathLexically(src, filepath.Join(root, "dst")); !errors.Is(err, os.ErrPermission) {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("nested copy", func(t *testing.T) {
|
|
root := t.TempDir()
|
|
src, dst := filepath.Join(root, "src"), filepath.Join(root, "dst")
|
|
if err := os.Mkdir(src, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(src, "child"), []byte("x"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
original := skillPathLstat
|
|
testseam.Swap(t, &skillPathLstat, func(path string) (os.FileInfo, error) {
|
|
if filepath.Base(path) == "child" {
|
|
return nil, os.ErrPermission
|
|
}
|
|
return original(path)
|
|
})
|
|
if err := copySkillPathLexically(src, dst); !errors.Is(err, os.ErrPermission) {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("readlink", func(t *testing.T) {
|
|
root := t.TempDir()
|
|
src := filepath.Join(root, "link")
|
|
if err := os.Symlink("target", src); err != nil {
|
|
t.Skipf("symlink unavailable: %v", err)
|
|
}
|
|
testseam.Swap(t, &skillPathReadlink, func(string) (string, error) { return "", os.ErrPermission })
|
|
if err := copySkillPathLexically(src, filepath.Join(root, "dst")); !errors.Is(err, os.ErrPermission) {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("special path", func(t *testing.T) {
|
|
testseam.Swap(t, &skillPathLstat, func(string) (os.FileInfo, error) {
|
|
return skillPathFakeInfo{mode: os.ModeNamedPipe}, nil
|
|
})
|
|
if err := copySkillPathLexically("special", "dst"); err == nil || !strings.Contains(err.Error(), "特殊") {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("regular open", func(t *testing.T) {
|
|
if err := copyRegularSkillFile(filepath.Join(t.TempDir(), "missing"), "dst", 0o600); !os.IsNotExist(err) {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("regular destination open", func(t *testing.T) {
|
|
root := t.TempDir()
|
|
src, dst := filepath.Join(root, "src"), filepath.Join(root, "dst")
|
|
if err := os.WriteFile(src, []byte("x"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(dst, []byte("occupied"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := copyRegularSkillFile(src, dst, 0o600); err == nil {
|
|
t.Fatal("expected exclusive create failure")
|
|
}
|
|
})
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
swap func(*testing.T)
|
|
}{
|
|
{name: "copy", swap: func(t *testing.T) {
|
|
testseam.Swap(t, &skillPathCopyBytes, func(io.Writer, io.Reader) (int64, error) { return 0, errors.New("copy failed") })
|
|
}},
|
|
{name: "sync", swap: func(t *testing.T) {
|
|
testseam.Swap(t, &skillPathSync, func(*os.File) error { return errors.New("sync failed") })
|
|
}},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
root := t.TempDir()
|
|
src, dst := filepath.Join(root, "src"), filepath.Join(root, "dst")
|
|
if err := os.WriteFile(src, []byte("x"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
tc.swap(t)
|
|
if err := copyRegularSkillFile(src, dst, 0o600); err == nil || !strings.Contains(err.Error(), tc.name) {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageSkillPathVerificationErrors(t *testing.T) {
|
|
root := t.TempDir()
|
|
|
|
t.Run("source and destination lstat", func(t *testing.T) {
|
|
if err := verifySkillPathCopy(filepath.Join(root, "missing-src"), "dst"); !os.IsNotExist(err) {
|
|
t.Fatalf("source error = %v", err)
|
|
}
|
|
src := filepath.Join(root, "source")
|
|
if err := os.WriteFile(src, []byte("x"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := verifySkillPathCopy(src, filepath.Join(root, "missing-dst")); !os.IsNotExist(err) {
|
|
t.Fatalf("destination error = %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("directory and file modes", func(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
src os.FileMode
|
|
dst os.FileMode
|
|
}{
|
|
{name: "directory", src: os.ModeDir | 0o700, dst: os.ModeDir | 0o755},
|
|
{name: "file", src: 0o600, dst: 0o644},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
src, dst := "src", "dst"
|
|
testseam.Swap(t, &skillPathLstat, func(path string) (os.FileInfo, error) {
|
|
if path == src {
|
|
return skillPathFakeInfo{mode: tc.src}, nil
|
|
}
|
|
return skillPathFakeInfo{mode: tc.dst}, nil
|
|
})
|
|
if err := verifySkillPathCopy(src, dst); err == nil || !strings.Contains(err.Error(), "权限") {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("directory read errors", func(t *testing.T) {
|
|
for _, failDest := range []bool{false, true} {
|
|
t.Run(map[bool]string{false: "source", true: "destination"}[failDest], func(t *testing.T) {
|
|
base := t.TempDir()
|
|
src, dst := filepath.Join(base, "src"), filepath.Join(base, "dst")
|
|
if err := os.Mkdir(src, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Mkdir(dst, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
original := skillPathReadDir
|
|
testseam.Swap(t, &skillPathReadDir, func(path string) ([]os.DirEntry, error) {
|
|
if (path == dst) == failDest {
|
|
return nil, os.ErrPermission
|
|
}
|
|
return original(path)
|
|
})
|
|
if err := verifySkillPathCopy(src, dst); !errors.Is(err, os.ErrPermission) {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("nested mismatch", func(t *testing.T) {
|
|
base := t.TempDir()
|
|
src, dst := filepath.Join(base, "src"), filepath.Join(base, "dst")
|
|
if err := os.Mkdir(src, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Mkdir(dst, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(src, "child"), []byte("x"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Mkdir(filepath.Join(dst, "child"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := verifySkillPathCopy(src, dst); err == nil || !strings.Contains(err.Error(), "类型") {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("symlink reads", func(t *testing.T) {
|
|
base := t.TempDir()
|
|
src, dst := filepath.Join(base, "src"), filepath.Join(base, "dst")
|
|
if err := os.Symlink("one", src); err != nil {
|
|
t.Skipf("symlink unavailable: %v", err)
|
|
}
|
|
if err := os.Symlink("one", dst); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, failDest := range []bool{false, true} {
|
|
t.Run(map[bool]string{false: "source", true: "destination"}[failDest], func(t *testing.T) {
|
|
original := skillPathReadlink
|
|
testseam.Swap(t, &skillPathReadlink, func(path string) (string, error) {
|
|
if (path == dst) == failDest {
|
|
return "", os.ErrPermission
|
|
}
|
|
return original(path)
|
|
})
|
|
if err := verifySkillPathCopy(src, dst); !errors.Is(err, os.ErrPermission) {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("file digest opens", func(t *testing.T) {
|
|
base := t.TempDir()
|
|
src, dst := filepath.Join(base, "src"), filepath.Join(base, "dst")
|
|
if err := os.WriteFile(src, []byte("x"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(dst, []byte("x"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, failDest := range []bool{false, true} {
|
|
t.Run(map[bool]string{false: "source", true: "destination"}[failDest], func(t *testing.T) {
|
|
original := skillPathOpen
|
|
testseam.Swap(t, &skillPathOpen, func(path string) (*os.File, error) {
|
|
if (path == dst) == failDest {
|
|
return nil, os.ErrPermission
|
|
}
|
|
return original(path)
|
|
})
|
|
if err := verifySkillPathCopy(src, dst); !errors.Is(err, os.ErrPermission) {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("special path", func(t *testing.T) {
|
|
fake := skillPathFakeInfo{mode: os.ModeNamedPipe}
|
|
testseam.Swap(t, &skillPathLstat, func(string) (os.FileInfo, error) { return fake, nil })
|
|
if err := verifySkillPathCopy("src", "dst"); err == nil || !strings.Contains(err.Error(), "特殊") {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("entry names", func(t *testing.T) {
|
|
if _, err := skillPathEntryNames(filepath.Join(root, "missing-dir")); !os.IsNotExist(err) {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("digest open and read", func(t *testing.T) {
|
|
if _, err := skillPathFileDigest(filepath.Join(root, "missing-file")); !os.IsNotExist(err) {
|
|
t.Fatalf("open error = %v", err)
|
|
}
|
|
file := filepath.Join(root, "digest-file")
|
|
if err := os.WriteFile(file, []byte("x"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
testseam.Swap(t, &skillPathCopyBytes, func(io.Writer, io.Reader) (int64, error) { return 0, errors.New("read failed") })
|
|
if _, err := skillPathFileDigest(file); err == nil || !strings.Contains(err.Error(), "read failed") {
|
|
t.Fatalf("read error = %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
type skillPathFakeInfo struct {
|
|
mode os.FileMode
|
|
}
|
|
|
|
func (skillPathFakeInfo) Name() string { return "fake" }
|
|
func (skillPathFakeInfo) Size() int64 { return 0 }
|
|
func (info skillPathFakeInfo) Mode() os.FileMode { return info.mode }
|
|
func (skillPathFakeInfo) ModTime() time.Time { return time.Time{} }
|
|
func (info skillPathFakeInfo) IsDir() bool { return info.mode.IsDir() }
|
|
func (skillPathFakeInfo) Sys() any { return nil }
|
|
|
|
type skillPathErrorDirEntry struct{}
|
|
|
|
func (skillPathErrorDirEntry) Name() string { return "broken" }
|
|
func (skillPathErrorDirEntry) IsDir() bool { return true }
|
|
func (skillPathErrorDirEntry) Type() os.FileMode { return os.ModeDir }
|
|
func (skillPathErrorDirEntry) Info() (os.FileInfo, error) { return nil, errors.New("info failed") }
|
|
|
|
type skillPathModeDirEntry struct {
|
|
mode os.FileMode
|
|
}
|
|
|
|
func (skillPathModeDirEntry) Name() string { return "directory" }
|
|
func (skillPathModeDirEntry) IsDir() bool { return true }
|
|
func (entry skillPathModeDirEntry) Type() os.FileMode { return entry.mode.Type() }
|
|
func (entry skillPathModeDirEntry) Info() (os.FileInfo, error) {
|
|
return skillPathFakeInfo{mode: entry.mode}, nil
|
|
}
|