|
@@ -0,0 +1,321 @@
|
|
|
|
|
+package agi
|
|
|
|
|
+
|
|
|
|
|
+import (
|
|
|
|
|
+ "archive/tar"
|
|
|
|
|
+ "archive/zip"
|
|
|
|
|
+ "compress/gzip"
|
|
|
|
|
+ "io"
|
|
|
|
|
+ "os"
|
|
|
|
|
+ "path"
|
|
|
|
|
+ "path/filepath"
|
|
|
|
|
+ "reflect"
|
|
|
|
|
+ "sort"
|
|
|
|
|
+ "strings"
|
|
|
|
|
+ "testing"
|
|
|
|
|
+
|
|
|
|
|
+ "github.com/robertkrimen/otto"
|
|
|
|
|
+ "imuslab.com/arozos/mod/agi/static"
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+// exportJS evaluates a JS expression and returns its exported Go value, which
|
|
|
|
|
+// is exactly what the ziplib functions receive from call.Argument(n).Export().
|
|
|
|
|
+func exportJS(t *testing.T, expr string) interface{} {
|
|
|
|
|
+ t.Helper()
|
|
|
|
|
+ val, err := otto.New().Run("(" + expr + ")")
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("evaluating %s: %v", expr, err)
|
|
|
|
|
+ }
|
|
|
|
|
+ exported, err := val.Export()
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("exporting %s: %v", expr, err)
|
|
|
|
|
+ }
|
|
|
|
|
+ return exported
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestZiplibSourceList(t *testing.T) {
|
|
|
|
|
+ cases := []struct {
|
|
|
|
|
+ name string
|
|
|
|
|
+ expr string
|
|
|
|
|
+ want []string
|
|
|
|
|
+ wantOK bool
|
|
|
|
|
+ }{
|
|
|
|
|
+ {"array of strings", `["user:/a.txt", "user:/b.txt"]`, []string{"user:/a.txt", "user:/b.txt"}, true},
|
|
|
|
|
+ {"array of one string", `["user:/a.txt"]`, []string{"user:/a.txt"}, true},
|
|
|
|
|
+ {"empty array", `[]`, []string{}, true},
|
|
|
|
|
+ {"mixed array keeps the strings", `["user:/a.txt", 1, null]`, []string{"user:/a.txt"}, true},
|
|
|
|
|
+ {"single string", `"user:/a.txt"`, []string{"user:/a.txt"}, true},
|
|
|
|
|
+ {"undefined", `undefined`, nil, false},
|
|
|
|
|
+ {"number", `42`, nil, false},
|
|
|
|
|
+ {"array of numbers", `[1, 2]`, nil, false},
|
|
|
|
|
+ {"object", `({a: "user:/a.txt"})`, nil, false},
|
|
|
|
|
+ }
|
|
|
|
|
+ for _, c := range cases {
|
|
|
|
|
+ t.Run(c.name, func(t *testing.T) {
|
|
|
|
|
+ got, ok := ziplibSourceList(exportJS(t, c.expr))
|
|
|
|
|
+ if ok != c.wantOK {
|
|
|
|
|
+ t.Fatalf("ok = %v, want %v", ok, c.wantOK)
|
|
|
|
|
+ }
|
|
|
|
|
+ if !reflect.DeepEqual(got, c.want) {
|
|
|
|
|
+ t.Errorf("sources = %#v, want %#v", got, c.want)
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// The returned list is rewritten in place by the callers, so it must not share
|
|
|
|
|
+// its backing array with the exported argument.
|
|
|
|
|
+func TestZiplibSourceListCopiesInput(t *testing.T) {
|
|
|
|
|
+ in := []string{"user:/a.txt"}
|
|
|
|
|
+ got, _ := ziplibSourceList(in)
|
|
|
|
|
+ got[0] = "changed"
|
|
|
|
|
+ if in[0] != "user:/a.txt" {
|
|
|
|
|
+ t.Errorf("ziplibSourceList shares its result with the input slice")
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestSzParsePathsArg(t *testing.T) {
|
|
|
|
|
+ cases := []struct {
|
|
|
|
|
+ name string
|
|
|
|
|
+ expr string
|
|
|
|
|
+ want []string
|
|
|
|
|
+ }{
|
|
|
|
|
+ {"array of strings", `["docs/", "/a.txt"]`, []string{"docs/", "a.txt"}},
|
|
|
|
|
+ {"mixed array keeps the strings", `["a.txt", 1]`, []string{"a.txt"}},
|
|
|
|
|
+ {"JSON array string", `'["docs/", "/a.txt"]'`, []string{"docs/", "a.txt"}},
|
|
|
|
|
+ {"single path string", `"/a.txt"`, []string{"a.txt"}},
|
|
|
|
|
+ {"number", `42`, nil},
|
|
|
|
|
+ {"undefined", `undefined`, nil},
|
|
|
|
|
+ }
|
|
|
|
|
+ for _, c := range cases {
|
|
|
|
|
+ t.Run(c.name, func(t *testing.T) {
|
|
|
|
|
+ got := szParsePathsArg(exportJS(t, c.expr))
|
|
|
|
|
+ if !reflect.DeepEqual(got, c.want) {
|
|
|
|
|
+ t.Errorf("szParsePathsArg(%s) = %#v, want %#v", c.expr, got, c.want)
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// newZipLibTestVM returns a VM with ziplib loaded for a user from newFsTestUser,
|
|
|
|
|
+// with a.txt, b.txt and docs/c.txt in the user's storage.
|
|
|
|
|
+func newZipLibTestVM(t *testing.T) (*otto.Otto, string) {
|
|
|
|
|
+ t.Helper()
|
|
|
|
|
+ u, dir := newFsTestUser(t)
|
|
|
|
|
+ files := map[string]string{"a.txt": "alpha", "b.txt": "bravo", "docs/c.txt": "charlie"}
|
|
|
|
|
+ for name, content := range files {
|
|
|
|
|
+ p := filepath.Join(dir, filepath.FromSlash(name))
|
|
|
|
|
+ if err := os.MkdirAll(filepath.Dir(p), 0755); err != nil {
|
|
|
|
|
+ t.Fatalf("mkdir: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if err := os.WriteFile(p, []byte(content), 0644); err != nil {
|
|
|
|
|
+ t.Fatalf("write fixture: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ vm := otto.New()
|
|
|
|
|
+ minimalGateway().injectZipFileLibFunctions(&static.AgiLibInjectionPayload{VM: vm, User: u})
|
|
|
|
|
+ return vm, dir
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// archiveEntries lists the base names of the files in a zip, tar or tar.gz.
|
|
|
|
|
+func archiveEntries(t *testing.T, file string) []string {
|
|
|
|
|
+ t.Helper()
|
|
|
|
|
+ var names []string
|
|
|
|
|
+ if strings.HasSuffix(file, ".zip") {
|
|
|
|
|
+ r, err := zip.OpenReader(file)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("open zip: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ defer r.Close()
|
|
|
|
|
+ for _, f := range r.File {
|
|
|
|
|
+ if !f.FileInfo().IsDir() {
|
|
|
|
|
+ names = append(names, path.Base(f.Name))
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ f, err := os.Open(file)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("open archive: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ defer f.Close()
|
|
|
|
|
+ var r io.Reader = f
|
|
|
|
|
+ if strings.HasSuffix(file, ".tar.gz") {
|
|
|
|
|
+ gz, err := gzip.NewReader(f)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("open gzip: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ defer gz.Close()
|
|
|
|
|
+ r = gz
|
|
|
|
|
+ }
|
|
|
|
|
+ tr := tar.NewReader(r)
|
|
|
|
|
+ for {
|
|
|
|
|
+ h, err := tr.Next()
|
|
|
|
|
+ if err == io.EOF {
|
|
|
|
|
+ break
|
|
|
|
|
+ }
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("read tar: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if h.Typeflag != tar.TypeDir {
|
|
|
|
|
+ names = append(names, path.Base(h.Name))
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ sort.Strings(names)
|
|
|
|
|
+ return names
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// A JS array of strings exports as []string, which the create functions used to
|
|
|
|
|
+// reject; every source shape must now produce the archive.
|
|
|
|
|
+func TestZiplibCreateFromSources(t *testing.T) {
|
|
|
|
|
+ cases := []struct {
|
|
|
|
|
+ name string
|
|
|
|
|
+ call string // %s is replaced with the output path
|
|
|
|
|
+ output string
|
|
|
|
|
+ want []string // nil means the call fails and writes nothing
|
|
|
|
|
+ }{
|
|
|
|
|
+ {"zip from array", `ziplib.createZipFile(["test:/a.txt", "test:/b.txt"], "%s")`, "out.zip", []string{"a.txt", "b.txt"}},
|
|
|
|
|
+ {"zip from array of one", `ziplib.createZipFile(["test:/a.txt"], "%s")`, "out.zip", []string{"a.txt"}},
|
|
|
|
|
+ {"zip from string", `ziplib.createZipFile("test:/a.txt", "%s")`, "out.zip", []string{"a.txt"}},
|
|
|
|
|
+ {"zip from folder in array", `ziplib.createZipFile(["test:/docs"], "%s")`, "out.zip", []string{"c.txt"}},
|
|
|
|
|
+ {"zip from number", `ziplib.createZipFile(42, "%s")`, "out.zip", nil},
|
|
|
|
|
+ {"tar from array", `ziplib.createTarFile(["test:/a.txt", "test:/b.txt"], "%s")`, "out.tar", []string{"a.txt", "b.txt"}},
|
|
|
|
|
+ {"tar from string", `ziplib.createTarFile("test:/a.txt", "%s")`, "out.tar", []string{"a.txt"}},
|
|
|
|
|
+ {"tar from number", `ziplib.createTarFile(42, "%s")`, "out.tar", nil},
|
|
|
|
|
+ {"tar.gz from array", `ziplib.createTarGzFile(["test:/a.txt", "test:/b.txt"], "%s")`, "out.tar.gz", []string{"a.txt", "b.txt"}},
|
|
|
|
|
+ {"tar.gz from number", `ziplib.createTarGzFile(42, "%s")`, "out.tar.gz", nil},
|
|
|
|
|
+ {"any zip from array", `ziplib.createAnyZipFile(["test:/a.txt", "test:/b.txt"], "%s", "zip")`, "out.zip", []string{"a.txt", "b.txt"}},
|
|
|
|
|
+ {"any tar.gz from array", `ziplib.createAnyZipFile(["test:/a.txt", "test:/b.txt"], "%s", "tar.gz")`, "out.tar.gz", []string{"a.txt", "b.txt"}},
|
|
|
|
|
+ {"any zip from number", `ziplib.createAnyZipFile(42, "%s", "zip")`, "out.zip", nil},
|
|
|
|
|
+ }
|
|
|
|
|
+ for _, c := range cases {
|
|
|
|
|
+ t.Run(c.name, func(t *testing.T) {
|
|
|
|
|
+ vm, dir := newZipLibTestVM(t)
|
|
|
|
|
+ val, err := vm.Run(strings.Replace(c.call, "%s", "test:/"+c.output, 1))
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("%s: %v", c.call, err)
|
|
|
|
|
+ }
|
|
|
|
|
+ ok, _ := val.ToBoolean()
|
|
|
|
|
+ outFile := filepath.Join(dir, c.output)
|
|
|
|
|
+ if c.want == nil {
|
|
|
|
|
+ if ok {
|
|
|
|
|
+ t.Errorf("call returned true, want false")
|
|
|
|
|
+ }
|
|
|
|
|
+ if _, err := os.Stat(outFile); err == nil {
|
|
|
|
|
+ t.Errorf("a failed call left %s behind", c.output)
|
|
|
|
|
+ }
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ if !ok {
|
|
|
|
|
+ t.Fatalf("call returned false, want true")
|
|
|
|
|
+ }
|
|
|
|
|
+ if got := archiveEntries(t, outFile); !reflect.DeepEqual(got, c.want) {
|
|
|
|
|
+ t.Errorf("archive holds %v, want %v", got, c.want)
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// writeTestZip writes a zip holding the given slash-separated entries, each
|
|
|
|
|
+// containing its own name.
|
|
|
|
|
+func writeTestZip(t *testing.T, file string, entries []string) {
|
|
|
|
|
+ t.Helper()
|
|
|
|
|
+ f, err := os.Create(file)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("create zip: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ defer f.Close()
|
|
|
|
|
+ zw := zip.NewWriter(f)
|
|
|
|
|
+ for _, name := range entries {
|
|
|
|
|
+ w, err := zw.Create(name)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("add %s: %v", name, err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if _, err := w.Write([]byte(name)); err != nil {
|
|
|
|
|
+ t.Fatalf("write %s: %v", name, err)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if err := zw.Close(); err != nil {
|
|
|
|
|
+ t.Fatalf("close zip: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestZiplibExtractPartialZip(t *testing.T) {
|
|
|
|
|
+ cases := []struct {
|
|
|
|
|
+ name string
|
|
|
|
|
+ paths string
|
|
|
|
|
+ want []string // files under the destination folder, slash separated
|
|
|
|
|
+ }{
|
|
|
|
|
+ {"array of one file", `["a.txt"]`, []string{"a.txt"}},
|
|
|
|
|
+ {"array of files", `["a.txt", "b.txt"]`, []string{"a.txt", "b.txt"}},
|
|
|
|
|
+ {"array with a folder", `["docs/"]`, []string{"docs/c.txt"}},
|
|
|
|
|
+ {"single path string", `"b.txt"`, []string{"b.txt"}},
|
|
|
|
|
+ }
|
|
|
|
|
+ for _, c := range cases {
|
|
|
|
|
+ t.Run(c.name, func(t *testing.T) {
|
|
|
|
|
+ vm, dir := newZipLibTestVM(t)
|
|
|
|
|
+ writeTestZip(t, filepath.Join(dir, "in.zip"), []string{"a.txt", "b.txt", "docs/c.txt"})
|
|
|
|
|
+
|
|
|
|
|
+ val, err := vm.Run(`ziplib.extractPartialZip("test:/in.zip", ` + c.paths + `, "test:/out")`)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("extractPartialZip: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if ok, _ := val.ToBoolean(); !ok {
|
|
|
|
|
+ t.Fatalf("extractPartialZip returned false")
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var got []string
|
|
|
|
|
+ outDir := filepath.Join(dir, "out")
|
|
|
|
|
+ filepath.Walk(outDir, func(p string, info os.FileInfo, err error) error {
|
|
|
|
|
+ if err == nil && !info.IsDir() {
|
|
|
|
|
+ rel, _ := filepath.Rel(outDir, p)
|
|
|
|
|
+ got = append(got, filepath.ToSlash(rel))
|
|
|
|
|
+ }
|
|
|
|
|
+ return nil
|
|
|
|
|
+ })
|
|
|
|
|
+ sort.Strings(got)
|
|
|
|
|
+ if !reflect.DeepEqual(got, c.want) {
|
|
|
|
|
+ t.Errorf("extracted %v, want %v", got, c.want)
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Archives the create functions write can be read back by the extract
|
|
|
|
|
+// functions. tar.gz used to panic both ways on an archiver built without its
|
|
|
|
|
+// embedded Tar.
|
|
|
|
|
+func TestZiplibCreateThenExtract(t *testing.T) {
|
|
|
|
|
+ cases := []struct {
|
|
|
|
|
+ name string
|
|
|
|
|
+ create string
|
|
|
|
|
+ extract string
|
|
|
|
|
+ }{
|
|
|
|
|
+ {"tar", `ziplib.createTarFile(["test:/a.txt", "test:/b.txt"], "test:/x.tar")`, `ziplib.extractTarFile("test:/x.tar", "test:/out")`},
|
|
|
|
|
+ {"tar.gz", `ziplib.createTarGzFile(["test:/a.txt", "test:/b.txt"], "test:/x.tar.gz")`, `ziplib.extractTarGzFile("test:/x.tar.gz", "test:/out")`},
|
|
|
|
|
+ {"any tar.gz", `ziplib.createAnyZipFile(["test:/a.txt", "test:/b.txt"], "test:/x.tgz", "tgz")`, `ziplib.extractAnyFile("test:/x.tgz", "test:/out")`},
|
|
|
|
|
+ {"zip", `ziplib.createZipFile(["test:/a.txt", "test:/b.txt"], "test:/x.zip")`, `ziplib.extractZipFile("test:/x.zip", "test:/out")`},
|
|
|
|
|
+ }
|
|
|
|
|
+ for _, c := range cases {
|
|
|
|
|
+ t.Run(c.name, func(t *testing.T) {
|
|
|
|
|
+ vm, dir := newZipLibTestVM(t)
|
|
|
|
|
+ for _, script := range []string{c.create, c.extract} {
|
|
|
|
|
+ val, err := vm.Run(script)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("%s: %v", script, err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if ok, _ := val.ToBoolean(); !ok {
|
|
|
|
|
+ t.Fatalf("%s returned false", script)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ for name, want := range map[string]string{"a.txt": "alpha", "b.txt": "bravo"} {
|
|
|
|
|
+ got, err := os.ReadFile(filepath.Join(dir, "out", name))
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Errorf("%s was not extracted: %v", name, err)
|
|
|
|
|
+ continue
|
|
|
|
|
+ }
|
|
|
|
|
+ if string(got) != want {
|
|
|
|
|
+ t.Errorf("%s holds %q, want %q", name, got, want)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+}
|