buffer: Store potential resolved symlinks as AbsPath#3997
buffer: Store potential resolved symlinks as AbsPath#3997JoeKar wants to merge 3 commits intomicro-editor:masterfrom
AbsPath#3997Conversation
|
I've noticed an "issue" with this PR. If we have a regular file For comparison, vim shows the resolved file name in the statusline, so it behaves predictably: it always shows the original file name. Well, maybe we can treat this as a feature ("first wins" rule)... |
Hm, indeed...this behavior is somehow strange. EDIT: EDIT2: |
|
Hmmmm... what is this? https://pkg.go.dev/path/filepath#EvalSymlinks :) Worth checking if we can just use this? Also, just realized: we should resolve each component of the path (i.e. each directory), not just the last one (the file itself), since any of the directories may be a symlink too? And skimming through the code in https://github.com/golang/go/blob/master/src/path/filepath/symlink.go it looks like this |
Whaaat...feels like this came out of nowhere. Don't know, why I didn't see this myself, especially since I had [filepath](the https://pkg.go.dev/path/filepath) documentation the last days open again. Thank you for pointing me to this link! |
9191df8 to
a7840b2
Compare
internal/buffer/buffer.go
Outdated
| absPath := path | ||
| if btype == BTDefault && path != "" { | ||
| // Ignore the returned error, since the checks are already performed in | ||
| // NewBufferFromFileWithCommand() |
There was a problem hiding this comment.
Strictly speaking, it may fail here even if the checks in NewBufferFromFileWithCommand() didn't fail, since the state of the file may have changed in the meantime? https://en.wikipedia.org/wiki/Time-of-check_to_time-of-use
And, checking and returning this error here isn't particularly hard?
There was a problem hiding this comment.
And, checking and returning this error here isn't particularly hard?
Here I agree, but...
Strictly speaking, it may fail here even if the checks in
NewBufferFromFileWithCommand()didn't fail, since the state of the file may have changed in the meantime?
...and here I only partially agree.
How often can it still change afterwards over the lifetime of the open buffer? The file, related to the stored path, is opened again at save and till then TOC/TOU is still a thing.
I though about this while writing, but didn't consider it for this PR.
The problem is, that we only track the path and no concrete file handle or any other sort of file abstraction, because we close it at exit of NewBufferFromFileWithCommand().
There was a problem hiding this comment.
I don't think we should be responsible for the file throughout the lifetime of a buffer. I doubt any editor does that, and I don't think that would be a good idea.
What we are responsible for is preventing micro from crashing or behaving unpredictably.
So, here I'm only talking about a simple issue: filepath.EvalSymlinks() may still return an error here (even if that's unlikely), so, what will be the value of path then? The documentation in https://pkg.go.dev/path/filepath#EvalSymlinks doesn't say what is returned as the 1st return value in case of an error. We might optimistically assume that it will be just the original path (without symlinks resolved), or e.g. with symlinks just partially resolved, but still a correct path. And we will be wrong: from the source code in https://github.com/golang/go/blob/master/src/path/filepath/symlink.go we can see that it will be just an empty string, not a file path.
So, it will silently cause incorrect behavior: we will open an empty unnamed buffer, instead of opening the file the user asked us to open?
There was a problem hiding this comment.
BTW I've noticed another issue: we should resolve the absolute path as well. The current working directory path may contain symlinks too, so if we only resolve the relative path, and prepend the current working directory path to it after that, the resulting absolute path will not be fully resolved.
For example:
mkdir dir
ln -s dir dir1
echo test >dir/file
cd dir1
micro -multiopen vsplit file ../dir/file
So we should do something like this?
resolvedPath, err := filepath.EvalSymlinks(path)
if err == nil {
path = resolvedPath
}
absPath, err = filepath.Abs(path)
if err != nil {
absPath = path
}
resolvedPath, err = filepath.EvalSymlinks(absPath)
if err == nil {
absPath = resolvedPath
}BTW since we are doing this in 2 or even 3 places, we should add a helper?
There was a problem hiding this comment.
[...] it will be just an empty string, not a file path.
So, it will silently cause incorrect behavior: we will open an empty unnamed buffer, instead of opening the file the user asked us to open?
Well, right now...yes. Even too much links could cause this.
The current working directory path may contain symlinks too, so if we only resolve the relative path, and prepend the current working directory path to it after that, the resulting absolute path will not be fully resolved.
Good catch, thank you!
I will update the PR.
Edit:
It should be feasible to create the absolute path first before possible links are evaluated, right? Then we wouldn't need the second evaluation loop.
There was a problem hiding this comment.
Edit:
It should be feasible to create the absolute path first before possible links are evaluated, right? Then we wouldn't need the second evaluation loop.
Just in case, github doesn't send email notifications about edits in comments, so I would never know about your question if I hadn't accidentally noticed it just now. :)
| absPath = path | ||
| } | ||
|
|
||
| resolvedPath, err = filepath.EvalSymlinks(absPath) |
There was a problem hiding this comment.
I've noticed another corner-case issue: if the file doesn't exist yet (so it is opened as an empty file) and its directory path contains symlinks, those symlinks are not resolved. E.g.:
mkdir dir
ln -s dir dir1
micro -multiopen vsplit dir/file dir1/file
For comparison, again, vim correctly resolves it (thus correctly detects that dir/file and dir/file1 are the same file, even though this file doesn't exist yet).
There was a problem hiding this comment.
Very good catch! 👍
Caused by symlink.go;l=84-87 and stat_unix.go;l=47-49, but fortunately it is wrapped into a PathError containing the causing path name. Here we can apply a little "hack" where EvalSymlinks() still does the job with the path and we pick it from the error message.
There was a problem hiding this comment.
Are we relying on some undocumented behavior here?
There is another corner case: the file doesn't exist, and its directory doesn't exist either, but some of its ancestor directories exists and contains symlinks:
mkdir dir
ln -s dir dir1
micro dir1/dir/file
As you can see, this newest version handles this case utterly incorrectly.
Furthermore, even if there are no symlinks (e.g. micro dir/dir/file with the above example), it behaves in the same utterly incorrect way. I.e. it causes a regression.
There was a problem hiding this comment.
Right, EvalSymlinks() stops at the first unavailable dir or file inside the path.
Unfortunately, it's not that simple anymore. 🤔
Otherwise we can't identify if we have the same file open multiple times via different symlinks. The test must be adapted to resolve symlinks in `findBuffer()`.
Otherwise it will be removed async, which shouldn't happen in case there is still one buffer open with the same modified file.
Otherwise we unnecessarily serialize the shared buffer every time when closing a bufpane with this buffer, so every such serialize overwrites the previous one, thus only the last serialize (when closing the last instance of the buffer, i.e. when actually closing the file, i.e. when the buffer is not shared anymore) will be used anyway.
| // to get the absolute path or to resolve symlinks, it returns unresolved path | ||
| // in place of resolved one. The only exception is the case in which the target | ||
| // file doesn't exist. We leave the path handling to EvalSymlinks() and use the | ||
| // path causing the error as target path. |
There was a problem hiding this comment.
The only exception is the case in which the target file doesn't exist. We leave the path handling to EvalSymlinks() and use the path causing the error as target path.
This sounds too detailed, and at the same time completely unclear. What "path handling", etc.
Why not just e.g. "In case the file does not exist, ResolvePath still resolves it, by resolving its directory path and keeping the file name." ?
This will help us to keep track of the same file opened via different symlinks.
Fixes #3995