Skip to content

buffer: Store potential resolved symlinks as AbsPath#3997

Open
JoeKar wants to merge 3 commits intomicro-editor:masterfrom
JoeKar:fix/symlinks
Open

buffer: Store potential resolved symlinks as AbsPath#3997
JoeKar wants to merge 3 commits intomicro-editor:masterfrom
JoeKar:fix/symlinks

Conversation

@JoeKar
Copy link
Member

@JoeKar JoeKar commented Feb 5, 2026

This will help us to keep track of the same file opened via different symlinks.

Fixes #3995

@dmaluka
Copy link
Collaborator

dmaluka commented Feb 10, 2026

I've noticed an "issue" with this PR. If we have a regular file foo and a symlink bar pointing to it, and we open bar in micro, the statusline shows its name is bar (not resolved). Ok, that is not a problem in itself. Then we open foo in another pane, and the statusline shows its name is bar too, not foo. Just because bar has been already opened, so its SharedBuffer already exists (and thus is reused by foo), and b.Path in this SharedBuffer is bar, not foo. Even though it is foo that is the original file, while bar is a symlink to it.

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)...

@JoeKar
Copy link
Member Author

JoeKar commented Feb 10, 2026

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.
The order of opening the file shouldn't influence how it is named in the editor.
It should be every time the file name/path the user opened or the resolved path.

EDIT:
The more tricky thing is the (relative) path before the basename of the file (set basename false).

EDIT2:
Didn't find an elegant solution so far.

@dmaluka
Copy link
Collaborator

dmaluka commented Feb 11, 2026

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 filepath.EvalSymlinks() does resolve them all.

@JoeKar
Copy link
Member Author

JoeKar commented Feb 11, 2026

Hmmmm... what is this? https://pkg.go.dev/path/filepath#EvalSymlinks :)

Worth checking if we can just use 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.
But yes, definitely and it sounds promising, since it does exactly what we need.

Thank you for pointing me to this link!

@JoeKar JoeKar force-pushed the fix/symlinks branch 2 times, most recently from 5e77c5a to 9191df8 Compare February 11, 2026 06:39
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.
absPath := path
if btype == BTDefault && path != "" {
// Ignore the returned error, since the checks are already performed in
// NewBufferFromFileWithCommand()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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().

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Different saveundo buffers for symlinked files

2 participants