package agent import ( "archive/tar" "bytes" "io" "os" "path/filepath" "strings" "testing" "time" ) func TestParseStatLine(t *testing.T) { // statCommand prints: type|size|mtime|mode|name e, ok := parseStatLine("directory|4096|1760000000|drwxr-xr-x|my dir") if !ok || !e.IsDir || e.Name != "my dir" || e.MTime != 1760000000 || e.Mode != "drwxr-xr-x" { t.Fatalf("dir: %+v %v", e, ok) } e, ok = parseStatLine("regular file|12|1760000001|-rw-r--r--|a|b.txt") if !ok || e.IsDir || e.Size != 12 || e.Name != "a|b.txt" { t.Fatalf("file with | in name: %+v %v", e, ok) } if _, ok := parseStatLine("garbage"); ok { t.Fatal("garbage parsed") } } func TestStatCommandQuotes(t *testing.T) { for _, fl := range []string{"gnu", "bsd"} { c := statCommand(fl, "/srv/it's here") if !strings.Contains(c, `'/srv/it'\''s here/'`) { t.Errorf("%s: dir not quoted: %s", fl, c) } } } func TestParseLsLine(t *testing.T) { e, ok := parseLsLine("-rw-r--r-- 1 root root 1234 Oct 15 10:00 my file.txt") if !ok || e.Name != "my file.txt" || e.Size != 1234 || e.IsDir { t.Fatalf("file: %+v %v", e, ok) } e, ok = parseLsLine("lrwxrwxrwx 1 root root 7 Oct 15 10:00 link -> target") if !ok || e.Name != "link" { t.Fatalf("link: %+v %v", e, ok) } if _, ok := parseLsLine("drwxr-xr-x 2 root root 4096 Oct 15 10:00 .."); ok { t.Fatal(".. listed") } if _, ok := parseLsLine("total 8"); ok { t.Fatal("total parsed") } } func TestRenameTarEntries(t *testing.T) { var src bytes.Buffer tw := tar.NewWriter(&src) for _, n := range []string{"old/", "old/a.txt", "old/sub/b"} { typ := byte(tar.TypeReg) if strings.HasSuffix(n, "/") { typ = tar.TypeDir } _ = tw.WriteHeader(&tar.Header{Name: n, Mode: 0o644, Typeflag: typ}) } _ = tw.Close() var out bytes.Buffer if err := renameTar(&out, &src, map[string]string{"old": "new"}); err != nil { t.Fatal(err) } tr := tar.NewReader(&out) var names []string for { h, err := tr.Next() if err != nil { break } names = append(names, h.Name) } if strings.Join(names, ",") != "new/,new/a.txt,new/sub/b" { t.Fatalf("names: %v", names) } } // A macOS host's tar packs "._name" companions with the file's attributes; // they are not the file and must not land under its name. func TestRenameTarDropsOtherEntries(t *testing.T) { var src bytes.Buffer tw := tar.NewWriter(&src) for _, e := range []struct{ n, body string }{{"./._r.txt", "attrs"}, {"./r.txt", "data"}} { _ = tw.WriteHeader(&tar.Header{Name: e.n, Mode: 0o644, Typeflag: tar.TypeReg, Size: int64(len(e.body))}) _, _ = tw.Write([]byte(e.body)) } _ = tw.Close() var out bytes.Buffer if err := renameTar(&out, &src, map[string]string{"r.txt": "new.txt"}); err != nil { t.Fatal(err) } tr := tar.NewReader(&out) var got []string for { h, err := tr.Next() if err != nil { break } b, _ := io.ReadAll(tr) got = append(got, h.Name+"="+string(b)) } if strings.Join(got, ",") != "new.txt=data" { t.Fatalf("entries: %v", got) } } // zip on an SSH host: from the items' folder, by their own names, quoted. func TestZipCommand(t *testing.T) { cmd, err := zipCommand([]string{"/srv/a b", "/srv/it's"}, "/tmp/x.zip") if err != nil { t.Fatal(err) } want := `cd -- '/srv' && if command -v zip >/dev/null 2>&1; then zip -r -q -y '/tmp/x.zip' './a b' './it'\''s'; ` + `else python3 -m zipfile -c '/tmp/x.zip' './a b' './it'\''s'; fi` if cmd != want { t.Fatalf("got %s\nwant %s", cmd, want) } if _, err := zipCommand([]string{"/a/x", "/b/y"}, "/t.zip"); err == nil { t.Fatal("items of two folders: want an error") } if _, err := zipCommand(nil, "/t.zip"); err == nil { t.Fatal("no items: want an error") } if got := unzipCommand("/srv/a.zip", ""); got != `if command -v unzip >/dev/null 2>&1; then unzip -o -q '/srv/a.zip' -d '/srv'; else python3 -m zipfile -e '/srv/a.zip' '/srv'; fi` { t.Fatalf("unzip: %s", got) } } // A connection busy with a long transfer is not idle, however long ago it // was opened; once the transfer ends the idle clock starts again. func TestSweepSparesBusyConn(t *testing.T) { c := &sshfsConn{last: time.Now().Add(-2 * sshfsIdle)} release := c.hold() if c.idle(time.Now()) { t.Fatal("a busy connection was taken for idle") } release() if c.idle(time.Now()) { t.Fatal("just released: not idle yet") } if !c.idle(time.Now().Add(2 * sshfsIdle)) { t.Fatal("unused for long: idle") } } // A file's size is its target's when it is a link: what cat will read. func TestSizeCommandFollowsLinks(t *testing.T) { for _, f := range []string{"gnu", "bsd"} { if cmd := sizeCommand(f, "/x"); !strings.Contains(cmd, "stat -L ") { t.Fatalf("%s: %s", f, cmd) } } } // Nothing of a tar stream lands outside name: other entries and ".." paths // are dropped. func TestRenameTarKeepsInsideName(t *testing.T) { var src bytes.Buffer tw := tar.NewWriter(&src) for _, n := range []string{"d/a", "d/../../etc/x", ".ssh/authorized_keys", "d/b"} { _ = tw.WriteHeader(&tar.Header{Name: n, Mode: 0o644, Typeflag: tar.TypeReg}) } _ = tw.Close() var out bytes.Buffer if err := renameTar(&out, &src, map[string]string{"d": "d"}); err != nil { t.Fatal(err) } tr := tar.NewReader(&out) var got []string for { h, err := tr.Next() if err != nil { break } got = append(got, h.Name) } if strings.Join(got, ",") != "d/a,d/b" { t.Fatalf("entries: %v", got) } } // Into an SSH host, "replace" replaces the thing whole, as on a machine. func TestUnpackCommandReplaces(t *testing.T) { cmd := unpackCommand("/srv", []string{"d"}, "replace") if !strings.HasPrefix(cmd, "rm -rf -- '/srv/d' && ") || strings.Contains(cmd, "--overwrite") { t.Fatalf("replace: %s", cmd) } if cmd := unpackCommand("/srv", []string{"d"}, "error"); cmd != "mkdir -p -- '/srv' && cd -- '/srv' && tar -xf -" { t.Fatalf("error: %s", cmd) } } // A link to a folder lists as a folder (marked as a link), so it opens. func TestParseStatLineFolderLink(t *testing.T) { e, ok := parseStatLine("LD|symbolic link|7|1700000000|lrwxrwxrwx|www") if !ok || !e.IsDir || !e.Link || e.Name != "www" { t.Fatalf("%+v %v", e, ok) } e, ok = parseStatLine("|regular file|5|1700000000|-rw-r--r--|a|b") if !ok || e.IsDir || e.Name != "a|b" { t.Fatalf("plain: %+v %v", e, ok) } if !strings.Contains(statCommand("gnu", "/x"), `[ -L "$f" ] && [ -d "$f" ]`) { t.Fatal("the command does not mark folder links") } } func TestListFolderLink(t *testing.T) { d := t.TempDir() _ = os.Mkdir(filepath.Join(d, "real"), 0o755) _ = os.Symlink(filepath.Join(d, "real"), filepath.Join(d, "ln")) r, err := fsList(d) if err != nil { t.Fatal(err) } for _, e := range r.Entries { if e.Name == "ln" && (!e.IsDir || !e.Link) { t.Fatalf("link: %+v", e) } } } // Commands go to the host as one argument of sh -c, so the user's own login // shell (fish, csh) cannot read them its way. func TestPosixCommand(t *testing.T) { if got := posix(`if [ -d '/x' ]; then echo ok; fi`); got != `sh -c 'if [ -d '\''/x'\'' ]; then echo ok; fi'` { t.Fatalf("%s", got) } } // csh expands "!" even inside single quotes: it goes out as '\!'. func TestPosixCommandBang(t *testing.T) { if got := posix(`ls .[!.]*`); got != `sh -c 'ls .['\!'.]*'` { t.Fatalf("%s", got) } }