From 212b2f651fa60bf13e0c975e42a155920da50ad2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Manuel=20Gonz=C3=A1lez?= <manuel@compensate.com>
Date: Sat, 13 Nov 2021 14:37:03 +0200
Subject: [PATCH] Add file mode in symbolic notation to `ls --json`

This aligns `restic ls --json` with `restic find --json`, utilizing the same
naming.
---
 changelog/unreleased/issue-3542 |  8 ++++++
 cmd/restic/cmd_ls.go            | 46 +++++++++++++++++----------------
 cmd/restic/cmd_ls_test.go       |  9 ++++---
 3 files changed, 37 insertions(+), 26 deletions(-)
 create mode 100644 changelog/unreleased/issue-3542

diff --git a/changelog/unreleased/issue-3542 b/changelog/unreleased/issue-3542
new file mode 100644
index 000000000..87f908fc1
--- /dev/null
+++ b/changelog/unreleased/issue-3542
@@ -0,0 +1,8 @@
+Enhancement: Add file mode in symbolic notation to `ls --json`
+
+Now `restic ls --json` provides mode in symbolic notation aligned
+with `restic find --json`.
+
+https://github.com/restic/restic/issues/3542
+https://github.com/restic/restic/pull/3573
+https://forum.restic.net/t/restic-ls-understanding-file-mode-with-json/4371
diff --git a/cmd/restic/cmd_ls.go b/cmd/restic/cmd_ls.go
index 228e7696c..9f1ab7ec6 100644
--- a/cmd/restic/cmd_ls.go
+++ b/cmd/restic/cmd_ls.go
@@ -77,31 +77,33 @@ type lsSnapshot struct {
 // Print node in our custom JSON format, followed by a newline.
 func lsNodeJSON(enc *json.Encoder, path string, node *restic.Node) error {
 	n := &struct {
-		Name       string      `json:"name"`
-		Type       string      `json:"type"`
-		Path       string      `json:"path"`
-		UID        uint32      `json:"uid"`
-		GID        uint32      `json:"gid"`
-		Size       *uint64     `json:"size,omitempty"`
-		Mode       os.FileMode `json:"mode,omitempty"`
-		ModTime    time.Time   `json:"mtime,omitempty"`
-		AccessTime time.Time   `json:"atime,omitempty"`
-		ChangeTime time.Time   `json:"ctime,omitempty"`
-		StructType string      `json:"struct_type"` // "node"
+		Name        string      `json:"name"`
+		Type        string      `json:"type"`
+		Path        string      `json:"path"`
+		UID         uint32      `json:"uid"`
+		GID         uint32      `json:"gid"`
+		Size        *uint64     `json:"size,omitempty"`
+		Mode        os.FileMode `json:"mode,omitempty"`
+		Permissions string      `json:"permissions,omitempty"`
+		ModTime     time.Time   `json:"mtime,omitempty"`
+		AccessTime  time.Time   `json:"atime,omitempty"`
+		ChangeTime  time.Time   `json:"ctime,omitempty"`
+		StructType  string      `json:"struct_type"` // "node"
 
 		size uint64 // Target for Size pointer.
 	}{
-		Name:       node.Name,
-		Type:       node.Type,
-		Path:       path,
-		UID:        node.UID,
-		GID:        node.GID,
-		size:       node.Size,
-		Mode:       node.Mode,
-		ModTime:    node.ModTime,
-		AccessTime: node.AccessTime,
-		ChangeTime: node.ChangeTime,
-		StructType: "node",
+		Name:        node.Name,
+		Type:        node.Type,
+		Path:        path,
+		UID:         node.UID,
+		GID:         node.GID,
+		size:        node.Size,
+		Mode:        node.Mode,
+		Permissions: node.Mode.String(),
+		ModTime:     node.ModTime,
+		AccessTime:  node.AccessTime,
+		ChangeTime:  node.ChangeTime,
+		StructType:  "node",
 	}
 	// Always print size for regular files, even when empty,
 	// but never for other types.
diff --git a/cmd/restic/cmd_ls_test.go b/cmd/restic/cmd_ls_test.go
index bef749c76..8a4fa51ee 100644
--- a/cmd/restic/cmd_ls_test.go
+++ b/cmd/restic/cmd_ls_test.go
@@ -18,6 +18,7 @@ func TestLsNodeJSON(t *testing.T) {
 		expect string
 	}{
 		// Mode is omitted when zero.
+		// Permissions, by convention is "-" per mode bit
 		{
 			path: "/bar/baz",
 			Node: restic.Node{
@@ -31,7 +32,7 @@ func TestLsNodeJSON(t *testing.T) {
 				Group: "nobodies",
 				Links: 1,
 			},
-			expect: `{"name":"baz","type":"file","path":"/bar/baz","uid":10000000,"gid":20000000,"size":12345,"mtime":"0001-01-01T00:00:00Z","atime":"0001-01-01T00:00:00Z","ctime":"0001-01-01T00:00:00Z","struct_type":"node"}`,
+			expect: `{"name":"baz","type":"file","path":"/bar/baz","uid":10000000,"gid":20000000,"size":12345,"permissions":"----------","mtime":"0001-01-01T00:00:00Z","atime":"0001-01-01T00:00:00Z","ctime":"0001-01-01T00:00:00Z","struct_type":"node"}`,
 		},
 
 		// Even empty files get an explicit size.
@@ -48,7 +49,7 @@ func TestLsNodeJSON(t *testing.T) {
 				Group: "not printed",
 				Links: 0xF00,
 			},
-			expect: `{"name":"empty","type":"file","path":"/foo/empty","uid":1001,"gid":1001,"size":0,"mtime":"0001-01-01T00:00:00Z","atime":"0001-01-01T00:00:00Z","ctime":"0001-01-01T00:00:00Z","struct_type":"node"}`,
+			expect: `{"name":"empty","type":"file","path":"/foo/empty","uid":1001,"gid":1001,"size":0,"permissions":"----------","mtime":"0001-01-01T00:00:00Z","atime":"0001-01-01T00:00:00Z","ctime":"0001-01-01T00:00:00Z","struct_type":"node"}`,
 		},
 
 		// Non-regular files do not get a size.
@@ -61,7 +62,7 @@ func TestLsNodeJSON(t *testing.T) {
 				Mode:       os.ModeSymlink | 0777,
 				LinkTarget: "not printed",
 			},
-			expect: `{"name":"link","type":"symlink","path":"/foo/link","uid":0,"gid":0,"mode":134218239,"mtime":"0001-01-01T00:00:00Z","atime":"0001-01-01T00:00:00Z","ctime":"0001-01-01T00:00:00Z","struct_type":"node"}`,
+			expect: `{"name":"link","type":"symlink","path":"/foo/link","uid":0,"gid":0,"mode":134218239,"permissions":"Lrwxrwxrwx","mtime":"0001-01-01T00:00:00Z","atime":"0001-01-01T00:00:00Z","ctime":"0001-01-01T00:00:00Z","struct_type":"node"}`,
 		},
 
 		{
@@ -74,7 +75,7 @@ func TestLsNodeJSON(t *testing.T) {
 				AccessTime: time.Date(2021, 2, 3, 4, 5, 6, 7, time.UTC),
 				ChangeTime: time.Date(2022, 3, 4, 5, 6, 7, 8, time.UTC),
 			},
-			expect: `{"name":"directory","type":"dir","path":"/some/directory","uid":0,"gid":0,"mode":2147484141,"mtime":"2020-01-02T03:04:05Z","atime":"2021-02-03T04:05:06.000000007Z","ctime":"2022-03-04T05:06:07.000000008Z","struct_type":"node"}`,
+			expect: `{"name":"directory","type":"dir","path":"/some/directory","uid":0,"gid":0,"mode":2147484141,"permissions":"drwxr-xr-x","mtime":"2020-01-02T03:04:05Z","atime":"2021-02-03T04:05:06.000000007Z","ctime":"2022-03-04T05:06:07.000000008Z","struct_type":"node"}`,
 		},
 	} {
 		buf := new(bytes.Buffer)