lessphp
lessphp copied to clipboard
Imports don't work if relative path appears in a symlink folder with a non-alphabetical character
This is easier to demonstrate than to explain.
Imagine the following file structure. Assume that subfolder-b is a symlinked folder.
/path/to/subfolder-a/subdir/filea.less
/path/to/subfolder-b/subdir/fileb.less
In fileb.less:
import "../../subfolder-a/subdir/filea.less";
When the value being passed gets sent along to fileExists(), it looks something like:
/path/to/subfolder-b/subdir/../../subfolder-a/subdir/filea.less
Even though this is a valid file path that should resolve, it will fail in our scenario in two ways:
- "subfolder-b" will end up as "subfolder". The reason is because the regex used in
preg_replace
matches folder names using \w. In other words, it will only match if the folder name has the characters [a-zA-Z0-9_]. - The regular expression replace will only munge a single instance of "filename/..". Since we have two relative paths in a row ("../..") these will get missed by the regex.
Both of these examples will work in the case of a regular folder, since realpath can handle those just fine. However, there are inconsistencies in the platform implementation of realpath() which can cause it to choke if there's a symlink involved in the file path (i.e. it traverses up the wrong tree).
I apologize for the obtuse explanation of the problem -- please let me know if there are any questions. It is a very real problem (which I spent this afternoon trying to work around). I will observe that the lessc bundled with less.js (1.2.2) handles paths like this correctly.
The good news is I have a solution and will submit a pull request shortly.
Sorry for taking forever to respond to this.
I'm trying to work through this problem but I am a bit confused. I am not sure where the original "sym link workaround" came from. I don't know why it's even there. It was from some pull request a few years ago and it's stuck around since then.
I'm removing it. Anyway, I am testing your arrangement of folders and symlinks, and everything is working fine on my computer without anything more than file_exists
.
Can you enlighten me on why any of this needs to be done? I've been searching around but I haven't found any information.
Hi, sorry for my own delay in following up on this.
This is unfortunately an incredibly obtuse and specific issue. The project I'm working with has a somewhat complex build structure (a few symlinked folders here and there) and it makes use of the import statement in less to some of those symlinks. The current version of lessphp fails, and while troubleshooting I determined that the cause of this was that the current implementation of fileExists()
was not correctly munging together the file path into an actual correct absolute path.
In a nutshell, the current implementation is doing a very simplistic munging of the path before it's passed along to real path. It's in essence saying, if you have a path "/folder1/folder2/../folder3/" that's the same exact thing as "/folder1/folder3" ... because if you travel down into folder2 and then traverse back up via "..", you're back where you started (in folder1).
I have no idea why it's even necessary to do this (shouldn't realpath do this work for you?) but that's how realpath() works apparently.
So that's fine, but the current implementation is naive. It assumes that a folder name will match one or more regex word characters (/w+). So it would correctly munge a path like the example I gave above. However, it would miss "/folder-1/folder-2/../folder-3" because the "-" character is not included in \w. So realpath() would get passed the relative file path and it would choke on it.
A second problem is it only makes a single pass on the path. So if you have "/foldera/folderb/folderc/../../folderd" that should munge to "/foldera/folderd". But using the current implementation it would only get to "/foldera/folderb/../folderd" because only a single pass is made.
My implementation corrects these two oversimplifications. To start, it uses a regex that better matches a folder name. Any folder name will match except "." and "..". It also munges the path in a loop until there are no more "/folder/.." patterns left.
The challenge is finding a way to test all of this in order to verify the implementation (and point out the existing flaws). I tried a few months ago to put together a test but unfortunately I believe both the imported file AND the file with the import statement have to themselves be on a symlink path. I haven't had the time to sit down and work out exactly which one or how.
If I have some time I'll see if it's at all possible to construct a test that would demonstrate all of this. Not sure if my explanation was at all enlightening but hopefully it made things a bit more clear?
The path munging has been deleted. Right now I only use PHP's built in file_exists
. Does anything more need to be done to support your directory structure?