1
+package cli
2
+
3
+import (
4
+ "os"
5
+ "path/filepath"
6
+ "strings"
7
+ "testing"
8
+ "time"
9
+
10
+ "github.com/ipfs/kubo/test/cli/harness"
11
+ "github.com/stretchr/testify/assert"
12
+ "github.com/stretchr/testify/require"
13
+)
14
+
15
+func TestLsLongFormat(t *testing.T) {
16
+ t.Parallel()
17
+
18
+ t.Run("long format shows mode and mtime when preserved", func(t *testing.T) {
19
+ t.Parallel()
20
+ node := harness.NewT(t).NewNode().Init().StartDaemon()
21
+ defer node.StopDaemon()
22
+
23
+ // Create a test directory structure with known permissions
24
+ testDir := filepath.Join(node.Dir, "testdata")
25
+ require.NoError(t, os.MkdirAll(testDir, 0755))
26
+
27
+ // Create files with specific permissions
28
+ file1 := filepath.Join(testDir, "readable.txt")
29
+ require.NoError(t, os.WriteFile(file1, []byte("hello"), 0644))
30
+
31
+ file2 := filepath.Join(testDir, "executable.sh")
32
+ require.NoError(t, os.WriteFile(file2, []byte("#!/bin/sh\necho hi"), 0755))
33
+
34
+ // Set a known mtime in the past (to get year format, avoiding flaky time-based tests)
35
+ oldTime := time.Date(2020, time.June, 15, 10, 30, 0, 0, time.UTC)
36
+ require.NoError(t, os.Chtimes(file1, oldTime, oldTime))
37
+ require.NoError(t, os.Chtimes(file2, oldTime, oldTime))
38
+
39
+ // Add with preserved mode and mtime
40
+ addRes := node.IPFS("add", "-r", "--preserve-mode", "--preserve-mtime", "-Q", testDir)
41
+ dirCid := addRes.Stdout.Trimmed()
42
+
43
+ // Run ls with --long flag
44
+ lsRes := node.IPFS("ls", "--long", dirCid)
45
+ output := lsRes.Stdout.String()
46
+
47
+ // Verify format: Mode Hash Size ModTime Name
48
+ lines := strings.Split(strings.TrimSpace(output), "\n")
49
+ require.Len(t, lines, 2, "expected 2 files in output")
50
+
51
+ // Check executable.sh line (should be first alphabetically)
52
+ assert.Contains(t, lines[0], "-rwxr-xr-x", "executable should have 755 permissions")
53
+ assert.Contains(t, lines[0], "Jun 15 2020", "should show mtime with year format")
54
+ assert.Contains(t, lines[0], "executable.sh", "should show filename")
55
+
56
+ // Check readable.txt line
57
+ assert.Contains(t, lines[1], "-rw-r--r--", "readable file should have 644 permissions")
58
+ assert.Contains(t, lines[1], "Jun 15 2020", "should show mtime with year format")
59
+ assert.Contains(t, lines[1], "readable.txt", "should show filename")
60
+ })
61
+
62
+ t.Run("long format shows dash for files without preserved mode or mtime", func(t *testing.T) {
63
+ t.Parallel()
64
+ node := harness.NewT(t).NewNode().Init().StartDaemon()
65
+ defer node.StopDaemon()
66
+
67
+ // Create and add a file without --preserve-mode or --preserve-mtime
68
+ testFile := filepath.Join(node.Dir, "nopreserve.txt")
69
+ require.NoError(t, os.WriteFile(testFile, []byte("test content"), 0644))
70
+
71
+ addRes := node.IPFS("add", "-Q", testFile)
72
+ fileCid := addRes.Stdout.Trimmed()
73
+
74
+ // Create a wrapper directory to list
75
+ node.IPFS("files", "mkdir", "/testdir")
76
+ node.IPFS("files", "cp", "/ipfs/"+fileCid, "/testdir/file.txt")
77
+ statRes := node.IPFS("files", "stat", "--hash", "/testdir")
78
+ dirCid := statRes.Stdout.Trimmed()
79
+
80
+ // Run ls with --long flag
81
+ lsRes := node.IPFS("ls", "--long", dirCid)
82
+ output := lsRes.Stdout.String()
83
+
84
+ // Files without preserved mode or mtime should show "-" for both columns
85
+ // Format: "-" (mode) <CID> <size> "-" (mtime) <name>
86
+ assert.Regexp(t, `^-\s+\S+\s+\d+\s+-\s+`, output, "missing mode and mtime should both show dash")
87
+ })
88
+
89
+ t.Run("long format with headers shows correct column order", func(t *testing.T) {
90
+ t.Parallel()
91
+ node := harness.NewT(t).NewNode().Init().StartDaemon()
92
+ defer node.StopDaemon()
93
+
94
+ // Create a simple test file
95
+ testDir := filepath.Join(node.Dir, "headertest")
96
+ require.NoError(t, os.MkdirAll(testDir, 0755))
97
+ testFile := filepath.Join(testDir, "file.txt")
98
+ require.NoError(t, os.WriteFile(testFile, []byte("hello"), 0644))
99
+
100
+ oldTime := time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC)
101
+ require.NoError(t, os.Chtimes(testFile, oldTime, oldTime))
102
+
103
+ addRes := node.IPFS("add", "-r", "--preserve-mode", "--preserve-mtime", "-Q", testDir)
104
+ dirCid := addRes.Stdout.Trimmed()
105
+
106
+ // Run ls with --long and --headers (--size defaults to true)
107
+ lsRes := node.IPFS("ls", "--long", "--headers", dirCid)
108
+ output := lsRes.Stdout.String()
109
+ lines := strings.Split(strings.TrimSpace(output), "\n")
110
+
111
+ // First line should be headers in correct order: Mode Hash Size ModTime Name
112
+ require.GreaterOrEqual(t, len(lines), 2)
113
+ headerFields := strings.Fields(lines[0])
114
+ require.Len(t, headerFields, 5, "header should have 5 columns")
115
+ assert.Equal(t, "Mode", headerFields[0])
116
+ assert.Equal(t, "Hash", headerFields[1])
117
+ assert.Equal(t, "Size", headerFields[2])
118
+ assert.Equal(t, "ModTime", headerFields[3])
119
+ assert.Equal(t, "Name", headerFields[4])
120
+
121
+ // Data line should have matching columns
122
+ dataFields := strings.Fields(lines[1])
123
+ require.GreaterOrEqual(t, len(dataFields), 5)
124
+ assert.Regexp(t, `^-[rwx-]{9}$`, dataFields[0], "first field should be mode")
125
+ assert.Regexp(t, `^Qm`, dataFields[1], "second field should be CID")
126
+ assert.Regexp(t, `^\d+$`, dataFields[2], "third field should be size")
127
+ })
128
+
129
+ t.Run("long format with headers and size=false", func(t *testing.T) {
130
+ t.Parallel()
131
+ node := harness.NewT(t).NewNode().Init().StartDaemon()
132
+ defer node.StopDaemon()
133
+
134
+ testDir := filepath.Join(node.Dir, "headertest2")
135
+ require.NoError(t, os.MkdirAll(testDir, 0755))
136
+ testFile := filepath.Join(testDir, "file.txt")
137
+ require.NoError(t, os.WriteFile(testFile, []byte("hello"), 0644))
138
+
139
+ oldTime := time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC)
140
+ require.NoError(t, os.Chtimes(testFile, oldTime, oldTime))
141
+
142
+ addRes := node.IPFS("add", "-r", "--preserve-mode", "--preserve-mtime", "-Q", testDir)
143
+ dirCid := addRes.Stdout.Trimmed()
144
+
145
+ // Run ls with --long --headers --size=false
146
+ lsRes := node.IPFS("ls", "--long", "--headers", "--size=false", dirCid)
147
+ output := lsRes.Stdout.String()
148
+ lines := strings.Split(strings.TrimSpace(output), "\n")
149
+
150
+ // Header should be: Mode Hash ModTime Name (no Size)
151
+ require.GreaterOrEqual(t, len(lines), 2)
152
+ headerFields := strings.Fields(lines[0])
153
+ require.Len(t, headerFields, 4, "header should have 4 columns without size")
154
+ assert.Equal(t, "Mode", headerFields[0])
155
+ assert.Equal(t, "Hash", headerFields[1])
156
+ assert.Equal(t, "ModTime", headerFields[2])
157
+ assert.Equal(t, "Name", headerFields[3])
158
+ })
159
+
160
+ t.Run("long format for directories shows trailing slash", func(t *testing.T) {
161
+ t.Parallel()
162
+ node := harness.NewT(t).NewNode().Init().StartDaemon()
163
+ defer node.StopDaemon()
164
+
165
+ // Create nested directory structure
166
+ testDir := filepath.Join(node.Dir, "dirtest")
167
+ subDir := filepath.Join(testDir, "subdir")
168
+ require.NoError(t, os.MkdirAll(subDir, 0755))
169
+ require.NoError(t, os.WriteFile(filepath.Join(subDir, "file.txt"), []byte("hi"), 0644))
170
+
171
+ addRes := node.IPFS("add", "-r", "--preserve-mode", "-Q", testDir)
172
+ dirCid := addRes.Stdout.Trimmed()
173
+
174
+ // Run ls with --long flag
175
+ lsRes := node.IPFS("ls", "--long", dirCid)
176
+ output := lsRes.Stdout.String()
177
+
178
+ // Directory should end with /
179
+ assert.Contains(t, output, "subdir/", "directory should have trailing slash")
180
+ // Directory should show 'd' in mode
181
+ assert.Contains(t, output, "drwxr-xr-x", "directory should show directory mode")
182
+ })
183
+
184
+ t.Run("long format without size flag", func(t *testing.T) {
185
+ t.Parallel()
186
+ node := harness.NewT(t).NewNode().Init().StartDaemon()
187
+ defer node.StopDaemon()
188
+
189
+ testDir := filepath.Join(node.Dir, "nosizetest")
190
+ require.NoError(t, os.MkdirAll(testDir, 0755))
191
+ testFile := filepath.Join(testDir, "file.txt")
192
+ require.NoError(t, os.WriteFile(testFile, []byte("hello world"), 0644))
193
+
194
+ oldTime := time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC)
195
+ require.NoError(t, os.Chtimes(testFile, oldTime, oldTime))
196
+
197
+ addRes := node.IPFS("add", "-r", "--preserve-mode", "--preserve-mtime", "-Q", testDir)
198
+ dirCid := addRes.Stdout.Trimmed()
199
+
200
+ // Run ls with --long but --size=false
201
+ lsRes := node.IPFS("ls", "--long", "--size=false", dirCid)
202
+ output := lsRes.Stdout.String()
203
+
204
+ // Should still have mode and mtime, but format differs (no size column)
205
+ assert.Contains(t, output, "-rw-r--r--")
206
+ assert.Contains(t, output, "Jan 01 2020")
207
+ assert.Contains(t, output, "file.txt")
208
+ })
209
+
210
+ t.Run("long format output is stable", func(t *testing.T) {
211
+ // This test ensures the output format doesn't change due to refactors
212
+ t.Parallel()
213
+ node := harness.NewT(t).NewNode().Init().StartDaemon()
214
+ defer node.StopDaemon()
215
+
216
+ testDir := filepath.Join(node.Dir, "stabletest")
217
+ require.NoError(t, os.MkdirAll(testDir, 0755))
218
+ testFile := filepath.Join(testDir, "test.txt")
219
+ require.NoError(t, os.WriteFile(testFile, []byte("stable"), 0644))
220
+
221
+ // Use a fixed time for reproducibility
222
+ fixedTime := time.Date(2020, time.December, 25, 12, 0, 0, 0, time.UTC)
223
+ require.NoError(t, os.Chtimes(testFile, fixedTime, fixedTime))
224
+
225
+ addRes := node.IPFS("add", "-r", "--preserve-mode", "--preserve-mtime", "-Q", testDir)
226
+ dirCid := addRes.Stdout.Trimmed()
227
+
228
+ // The CID should be deterministic given same content, mode, and mtime
229
+ // This is the expected CID for this specific test data
230
+ lsRes := node.IPFS("ls", "--long", dirCid)
231
+ output := strings.TrimSpace(lsRes.Stdout.String())
232
+
233
+ // Verify the format: Mode<tab>Hash<tab>Size<tab>ModTime<tab>Name
234
+ fields := strings.Fields(output)
235
+ require.GreaterOrEqual(t, len(fields), 5, "output should have at least 5 fields")
236
+
237
+ // Field 0: mode (10 chars, starts with - for regular file)
238
+ assert.Regexp(t, `^-[rwx-]{9}$`, fields[0], "mode should be Unix permission format")
239
+
240
+ // Field 1: CID (starts with Qm or bafy)
241
+ assert.Regexp(t, `^(Qm|bafy)`, fields[1], "second field should be CID")
242
+
243
+ // Field 2: size (numeric)
244
+ assert.Regexp(t, `^\d+$`, fields[2], "third field should be numeric size")
245
+
246
+ // Fields 3-4: date (e.g., "Dec 25 2020" or "Dec 25 12:00")
247
+ // The date format is "Mon DD YYYY" for old files
248
+ assert.Equal(t, "Dec", fields[3])
249
+ assert.Equal(t, "25", fields[4])
250
+
251
+ // Last field: filename
252
+ assert.Equal(t, "test.txt", fields[len(fields)-1])
253
+ })
254
+}