Subtle bug indirectly caused by require() and case-insensitive filesystems

71 views
Skip to first unread message

Federico Ferri

unread,
Dec 17, 2025, 12:03:15 PM (2 days ago) Dec 17
to lu...@googlegroups.com
Hi list,

I observed a weird bug, where an object instance x (of a class created with middleclass) fails to be identified as such using MyClass.isInstanceOf(x, MyClass) across modules.

i.e.:
-- Color.lua
local class = require 'middleclass'
local Color = class 'Color'
...
return Color

-- a_module.lua
local Color = require 'Color'
return {test = function(x) print(Color.isInstanceOf(x, Color) and 'is a Color' or 'is NOT a Color') end}

-- another_module.lua
local Color = require 'color' -- << notice the different case
local a_module = require 'a_module'
c = Color(...)
a_module.test(c) -- prints: is NOT a Color

This problem is catched very easily on Linux, since the filesystem is typically case-sensitive, and require'color' would fail.
But on Windows and macOS, which are case-insensitive, it goes unnoticed, and produces this apparently crazy behavior.

It all boils down to the fact:

> rawequal(require 'Color', require 'Color')
true
> rawequal(require 'Color', require 'color')
false
so if a module was loaded from a different file, for Lua it is a different module (fine) and this has repercussions on how middleclass checks for class appartenance (this is LESS fine, if one wants to maintain mental sanity).

Now, if the different name via which a module was loaded, differs from the canonical file system name, only in case (i.e. NOT in the actual file name, e.g. it could be a symlink, or location e.g. it could be a module with the same file name but in a different directory), then we are exactly in the crazy scenario where things might break very subtly.

One could monkey-patch require() (i.e. in pure Lua code) but it seems a lot more work, as one would have to replicate a lot of module search logic of the built-in require().
To me it would make sense for require() to warn or even fail if the require() arg doesn't match in case the actual filesystem name (especially when one works with multiple OSes, and it doesn't make sense that require() with mistyped arg (bad casing) works on one platform but fails on another).
What are your thoughts on this?

Cheers,
Federico Ferri

Tomás Guisasola

unread,
Dec 17, 2025, 12:18:42 PM (2 days ago) Dec 17
to lu...@googlegroups.com
Hi Federico

I would redefine require as:

-- untested code
do
local original_require = require
require = function (mod)
return original_require (mod:lower())
end
end

It is not too much work and I think it would achieve the behavior you
need, don't you think?

Regards,
Tomás

Em qua., 17 de dez. de 2025 às 14:03, Federico Ferri
<federico...@gmail.com> escreveu:
> --
> You received this message because you are subscribed to the Google Groups "lua-l" group.
> To unsubscribe from this group and stop receiving emails from it, send an email to lua-l+un...@googlegroups.com.
> To view this discussion visit https://groups.google.com/d/msgid/lua-l/CA%2BVucGDt7N2mGMccJy_tAL-msXfERr05s9msxkZGk4vRCEV-hw%40mail.gmail.com.

Sainan

unread,
Dec 17, 2025, 12:18:53 PM (2 days ago) Dec 17
to lu...@googlegroups.com
A possibly related issue could occur if you use symlinks and then require the same file using a different path. The question then is if require should use the canonical path before attempting to load a module.

-- Sainan

Federico Ferri

unread,
Dec 18, 2025, 4:32:47 AM (yesterday) Dec 18
to lu...@googlegroups.com
Hi Tomas, that's a clever idea, however it requires to have all modules in lowercase, or it would not work on case-sensitive file-systems e.g. on Linux.

The more general solution requires extracting the canonical filename (i.e. with original case) which I did by extending lfs (LuaFileSystem) with the function:

function lfs.realpath(path)
    local dir, file = lfs.pathsplit(path)
    local lower = file:lower()
    for entry in lfs.dir(dir) do
        if entry:lower() == lower then
            return lfs.pathjoin(dir, entry)
        end
    end
end

-- and also lfs.pathsplit, lfs.pathjoin whose implementation is omitted for brevity

then patching require to compare the resolved module filename with the canonical name:

function require(required_name)
    local ret_vals = {original_require(required_name)}
    local resolved, err = package.searchpath(required_name, package.path)
    if resolved and lfs.realpath(resolved) ~= resolved then
        error(("require('%s'): filename case mismatch (actual file exists with different case)"):format(required_name))
    end
    return table.unpack(ret_vals)
end

Tomás Guisasola

unread,
Dec 18, 2025, 6:45:16 AM (22 hours ago) Dec 18
to lu...@googlegroups.com
Hi Federico,

> Hi Tomas, that's a clever idea, however it requires to have all modules in lowercase, or it would not work on case-sensitive file-systems e.g. on Linux.

You could redefine `require` only on case-insensitive file-systems.
In fact, the problem is the opposite in case-sensitive file-systems:
if your code requires "color" instead of "Color" in a Linux
file-system it will get an error -- or worse!

> The more general solution requires extracting the canonical filename (i.e. with original case) which I did by extending lfs (LuaFileSystem) with the function:

Lua does not rely on LuaFileSystem...

> function lfs.realpath(path)
> local dir, file = lfs.pathsplit(path)
> local lower = file:lower()
> for entry in lfs.dir(dir) do
> if entry:lower() == lower then
> return lfs.pathjoin(dir, entry)
> end
> end
> end
>
> -- and also lfs.pathsplit, lfs.pathjoin whose implementation is omitted for brevity
>
> then patching require to compare the resolved module filename with the canonical name:
>
> function require(required_name)
> local ret_vals = {original_require(required_name)}
> local resolved, err = package.searchpath(required_name, package.path)
> if resolved and lfs.realpath(resolved) ~= resolved then
> error(("require('%s'): filename case mismatch (actual file exists with different case)"):format(required_name))
> end
> return table.unpack(ret_vals)
> end

Or you could redefine `require` to raise an error when the module name
has any upper case to avoid clashes...

The mismatch between case-sensitiveness in the language and any other
system it has to communicate with, such as databases, is a tricky
problem. I didn't try to solve that; I just avoided the problem by
writing everything in lower case.

Regards,
Tomás
Reply all
Reply to author
Forward
0 new messages