From 52ef9967fca5946ee047847d744d00ccaecff1a8 Mon Sep 17 00:00:00 2001 From: Shulhan Date: Fri, 19 Jul 2024 01:48:28 +0700 Subject: lib/memfs: sanitize the Root directory to fix refresh In [MemFS.refresh], if the requested url is "/file1" and [Options.Root] is ".", the path during refresh become "file1" and if passed to [filepath.Dir] it will return ".". This cause the loop on refresh never end because there is no PathNodes equal with ".". --- lib/memfs/internal/testdata/get_refresh_test.data | 90 +++++++++--- lib/memfs/memfs.go | 12 ++ lib/memfs/memfs_test.go | 164 +++++++++++++++++++++- lib/memfs/options.go | 6 + 4 files changed, 249 insertions(+), 23 deletions(-) diff --git a/lib/memfs/internal/testdata/get_refresh_test.data b/lib/memfs/internal/testdata/get_refresh_test.data index 1cf9b8cc..ce3e5d9e 100644 --- a/lib/memfs/internal/testdata/get_refresh_test.data +++ b/lib/memfs/internal/testdata/get_refresh_test.data @@ -2,10 +2,46 @@ Test Get that trigger refresh. The MemFS Options TryDirect must be set to true. If the input content is not set then the file will not be written. ->>> /dir-a/dir-b/file -Content of file. +<<< / +{ + "path": "/", + "name": "/", + "content_type": "", + "mode_string": "drwxr-xr-x", + "size": 0, + "is_dir": true, + "childs": [] +} + +>>> /file1 +Content of file1. + +<<< /file1 +{ + "path": "/", + "name": "/", + "content_type": "", + "mode_string": "drwxr-xr-x", + "size": 0, + "is_dir": true, + "childs": [ + { + "path": "/file1", + "name": "file1", + "content_type": "text/plain; charset=utf-8", + "mode_string": "-rw-------", + "size": 17, + "is_dir": false, + "content": "Q29udGVudCBvZiBmaWxlMS4=", + "childs": [] + } + ] +} + +>>> /dir-a/dir-b/file2 +Content of file2. -<<< /dir-a/dir-b/file +<<< /dir-a/dir-b/file2 { "path": "/", "name": "/", @@ -14,6 +50,16 @@ Content of file. "size": 0, "is_dir": true, "childs": [ + { + "path": "/file1", + "name": "file1", + "content_type": "text/plain; charset=utf-8", + "mode_string": "-rw-------", + "size": 17, + "is_dir": false, + "content": "Q29udGVudCBvZiBmaWxlMS4=", + "childs": [] + }, { "path": "/dir-a", "name": "dir-a", @@ -31,13 +77,13 @@ Content of file. "is_dir": true, "childs": [ { - "path": "/dir-a/dir-b/file", - "name": "file", + "path": "/dir-a/dir-b/file2", + "name": "file2", "content_type": "text/plain; charset=utf-8", "mode_string": "-rw-------", - "size": 16, + "size": 17, "is_dir": false, - "content": "Q29udGVudCBvZiBmaWxlLg==", + "content": "Q29udGVudCBvZiBmaWxlMi4=", "childs": [] } ] @@ -47,10 +93,10 @@ Content of file. ] } ->>> /dir-a/dir-b/file2 -Content of file2. +>>> /dir-a/dir-b/file3 +Content of file3. -<<< /dir-a/dir-b/file2 +<<< /dir-a/dir-b/file3 { "path": "/", "name": "/", @@ -59,6 +105,16 @@ Content of file2. "size": 0, "is_dir": true, "childs": [ + { + "path": "/file1", + "name": "file1", + "content_type": "text/plain; charset=utf-8", + "mode_string": "-rw-------", + "size": 17, + "is_dir": false, + "content": "Q29udGVudCBvZiBmaWxlMS4=", + "childs": [] + }, { "path": "/dir-a", "name": "dir-a", @@ -76,23 +132,23 @@ Content of file2. "is_dir": true, "childs": [ { - "path": "/dir-a/dir-b/file", - "name": "file", + "path": "/dir-a/dir-b/file2", + "name": "file2", "content_type": "text/plain; charset=utf-8", "mode_string": "-rw-------", - "size": 16, + "size": 17, "is_dir": false, - "content": "Q29udGVudCBvZiBmaWxlLg==", + "content": "Q29udGVudCBvZiBmaWxlMi4=", "childs": [] }, { - "path": "/dir-a/dir-b/file2", - "name": "file2", + "path": "/dir-a/dir-b/file3", + "name": "file3", "content_type": "text/plain; charset=utf-8", "mode_string": "-rw-------", "size": 17, "is_dir": false, - "content": "Q29udGVudCBvZiBmaWxlMi4=", + "content": "Q29udGVudCBvZiBmaWxlMy4=", "childs": [] } ] diff --git a/lib/memfs/memfs.go b/lib/memfs/memfs.go index 6ef40c2d..4ac9eba0 100644 --- a/lib/memfs/memfs.go +++ b/lib/memfs/memfs.go @@ -637,6 +637,13 @@ out: func (mfs *MemFS) refresh(url string) (node *Node, err error) { syspath := filepath.Join(mfs.Root.SysPath, url) + if syspath[0] != '/' { + // When "." joined with url "/file", the syspath become + // "file" instead of "./file", this cause + // [strings.HasPrefix] return false. + syspath = `./` + syspath + } + if !strings.HasPrefix(syspath, mfs.Root.SysPath) { return nil, fs.ErrNotExist } @@ -655,6 +662,11 @@ func (mfs *MemFS) refresh(url string) (node *Node, err error) { for node == nil { dir = filepath.Dir(dir) + if dir == `.` { + // On "./file" it will return ".". + node = mfs.Root + break + } node = mfs.PathNodes.Get(dir) } diff --git a/lib/memfs/memfs_test.go b/lib/memfs/memfs_test.go index 12651413..8275ea3b 100644 --- a/lib/memfs/memfs_test.go +++ b/lib/memfs/memfs_test.go @@ -65,11 +65,46 @@ func TestNew(t *testing.T) { opts Options } + var dirTestdata = filepath.Join(_testWD, `testdata`) + var err error + + err = os.Chdir(dirTestdata) + if err != nil { + t.Fatal(err) + } + + t.Cleanup(func() { + err = os.Chdir(_testWD) + if err != nil { + t.Fatal(err) + } + }) + var afile = filepath.Join(_testWD, `testdata/index.html`) var listCase = []testCase{{ - desc: "With empty dir", - expErr: "open : no such file or directory", + desc: `With empty dir`, + expMapKeys: []string{ + `/`, + `/direct`, + `/direct/add`, + `/direct/add/file`, + `/direct/add/file2`, + `/exclude`, + `/exclude/dir`, + `/exclude/index-link.css`, + `/exclude/index-link.html`, + `/exclude/index-link.js`, + `/include`, + `/include/dir`, + `/include/index.css`, + `/include/index.html`, + `/include/index.js`, + `/index.css`, + `/index.html`, + `/index.js`, + `/plain`, + }, }, { desc: "With file", opts: Options{ @@ -79,7 +114,7 @@ func TestNew(t *testing.T) { }, { desc: "With directory", opts: Options{ - Root: filepath.Join(_testWD, "testdata"), + Root: dirTestdata, Excludes: []string{ "memfs_generate.go$", "direct$", @@ -156,7 +191,6 @@ func TestNew(t *testing.T) { var ( c testCase mfs *MemFS - err error ) for _, c = range listCase { t.Log(c.desc) @@ -392,7 +426,7 @@ func TestMemFS_Get_refresh(t *testing.T) { var ( tempDir = t.TempDir() opts = Options{ - Root: tempDir, + Root: tempDir + `/`, TryDirect: true, } @@ -405,9 +439,127 @@ func TestMemFS_Get_refresh(t *testing.T) { } var listCase = []testCase{{ - filePath: `/dir-a/dir-b/file`, + filePath: `/file1`, }, { filePath: `/dir-a/dir-b/file2`, + }, { + filePath: `/dir-a/dir-b/file3`, + }} + + var ( + c testCase + gotJSON bytes.Buffer + ) + for _, c = range listCase { + var fullpath = filepath.Join(tempDir, c.filePath) + + err = os.MkdirAll(filepath.Dir(fullpath), 0700) + if err != nil { + t.Fatal(err) + } + + var expContent = tdata.Input[c.filePath] + if len(expContent) != 0 { + // Only create the file if content is set. + err = os.WriteFile(fullpath, expContent, 0600) + if err != nil { + t.Fatal(err) + } + } + + // Try Get the file. + + var ( + tag = c.filePath + `:error` + expError = string(tdata.Output[tag]) + node *Node + ) + + node, err = mfs.Get(c.filePath) + if err != nil { + test.Assert(t, tag, expError, err.Error()) + continue + } + + // Check the tree of MemFS. + + var rawJSON []byte + + rawJSON, err = mfs.Root.JSON(9999, true, false) + if err != nil { + t.Fatal(err) + } + + gotJSON.Reset() + err = json.Indent(&gotJSON, rawJSON, ``, ` `) + if err != nil { + t.Fatal(err) + } + + test.Assert(t, c.filePath+` content`, string(expContent), string(node.Content)) + + var expJSON = string(tdata.Output[c.filePath]) + test.Assert(t, c.filePath+` JSON of memfs.Root`, expJSON, gotJSON.String()) + } +} + +// TestMemFS_Get_refresh_withDot test [MemFS.refresh] using "." as Root +// directory. +func TestMemFS_Get_refresh_withDot(t *testing.T) { + type testCase struct { + filePath string + } + + var ( + tdata *test.Data + err error + ) + + tdata, err = test.LoadData(`internal/testdata/get_refresh_test.data`) + if err != nil { + t.Fatal(err) + } + + var ( + tempDir = t.TempDir() + opts = Options{ + Root: `.`, + TryDirect: true, + } + workDir string + mfs *MemFS + ) + + workDir, err = os.Getwd() + if err != nil { + t.Fatal(err) + } + + err = os.Chdir(tempDir) + if err != nil { + t.Fatal(err) + } + + t.Cleanup(func() { + err = os.Chdir(workDir) + if err != nil { + t.Logf(err.Error()) + } + }) + + mfs, err = New(&opts) + if err != nil { + t.Fatal(err) + } + + var listCase = []testCase{{ + filePath: `/`, + }, { + filePath: `/file1`, + }, { + filePath: `/dir-a/dir-b/file2`, + }, { + filePath: `/dir-a/dir-b/file3`, }} var ( diff --git a/lib/memfs/options.go b/lib/memfs/options.go index cae701b2..0b415f5c 100644 --- a/lib/memfs/options.go +++ b/lib/memfs/options.go @@ -4,6 +4,8 @@ package memfs +import "strings" + const ( defaultMaxFileSize = 1024 * 1024 * 5 ) @@ -48,4 +50,8 @@ func (opts *Options) init() { if opts.MaxFileSize == 0 { opts.MaxFileSize = defaultMaxFileSize } + opts.Root = strings.TrimSuffix(opts.Root, `/`) + if len(opts.Root) == 0 { + opts.Root = `.` + } } -- cgit v1.3