Skip to content

Commit 043a050

Browse files
committed
is_symlink in MEX if not native
1 parent 88a44d8 commit 043a050

File tree

7 files changed

+254
-15
lines changed

7 files changed

+254
-15
lines changed

+stdlib/is_char_device.m

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
%% IS_CHAR_DEVICE is path a character device
2+
% e.g. NUL, /dev/null
3+
% false if file does not exist
4+
5+
function ok = is_char_device(p)
6+
arguments
7+
p (1,1) string
8+
end
9+
10+
if stdlib.isoctave()
11+
ok = S_ISCHR(stat(p).mode);
12+
else
13+
ok = is_char_device_mex(p);
14+
end
15+
16+
end
17+
18+
%!assert (!is_exe(''))
19+
%!test
20+
%! if ispc
21+
%! n = "NUL";
22+
%! else
23+
%! n = "/dev/null";
24+
%! end
25+
%! assert (is_char_device(n))

+stdlib/is_symlink.m

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,21 @@
99
try
1010
ok = isSymbolicLink(p);
1111
catch e
12-
% must be absolute path
13-
% NOT .canonical or symlink is gobbled!
14-
p = stdlib.absolute(p, "", false);
15-
op = javaPathObject(p);
16-
17-
% https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/nio/file/Files.html#isSymbolicLink(java.nio.file.Path)
18-
% https://dev.java/learn/java-io/file-system/links/
19-
2012
if strcmp(e.identifier, "MATLAB:UndefinedFunction")
21-
ok = java.nio.file.Files.isSymbolicLink(op);
13+
try
14+
ok = is_symlink_mex(p);
15+
catch e
16+
if strcmp(e.identifier, "MATLAB:UndefinedFunction")
17+
ok = java.nio.file.Files.isSymbolicLink(javaPathObject(stdlib.absolute(p, "", false)));
18+
else
19+
rethrow(e)
20+
end
21+
end
2222
elseif strcmp(e.identifier, "Octave:undefined-function")
23-
ok = javaMethod("isSymbolicLink", "java.nio.file.Files", op);
23+
ok = S_ISLNK(stat(p).mode);
2424
else
2525
rethrow(e)
2626
end
27-
2827
end
2928

3029
end

buildfile.m

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@
1616

1717
msvc = startsWith(cxx.ShortName, "MSVCPP");
1818

19-
% disp("Mex using: " + cxx.Name + " " + cxx.Version)
20-
2119
std = "-std=c++17";
2220
compiler_id = "";
2321
% FIXME: Windows oneAPI
@@ -34,7 +32,7 @@
3432
if s == 0
3533
compiler_id = "CXX=g++-10";
3634
else
37-
warning("GCC 10 not found, using default, may fail on runtime")
35+
warning("GCC 10 not found, using default GCC " + cxx.Version + " may fail on runtime")
3836
end
3937
end
4038
end
@@ -51,7 +49,15 @@
5149

5250
plan("clean") = matlab.buildtool.tasks.CleanTask;
5351

54-
plan("mex:is_char_device") = matlab.buildtool.tasks.MexTask("is_char_device.cpp", bindir, ...
52+
plan("mex:is_char_device_mex") = matlab.buildtool.tasks.MexTask("src/is_char_device.cpp", bindir, ...
53+
Options=[compiler_id, compiler_opt]);
54+
55+
is_symlink_src = ["src/is_symlink.cpp"];
56+
if ispc
57+
is_symlink_src(end+1) = "src/windows.cpp";
58+
end
59+
60+
plan("mex:is_symlink_mex") = matlab.buildtool.tasks.MexTask(is_symlink_src, bindir, ...
5561
Options=[compiler_id, compiler_opt]);
5662

5763
plan("test").Dependencies = "mex";

example/java/is_symlink.m

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
function ok = is_symlink(p)
2+
% must be absolute path
3+
% NOT .canonical or symlink is gobbled!
4+
p = stdlib.absolute(p, "", false);
5+
op = javaPathObject(p);
6+
7+
% https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/nio/file/Files.html#isSymbolicLink(java.nio.file.Path)
8+
% https://dev.java/learn/java-io/file-system/links/
9+
10+
ok = java.nio.file.Files.isSymbolicLink(op);
11+
12+
end

src/is_symlink.cpp

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// C++ Matlab MEX function using C++17 <filesystem>
2+
// https://www.mathworks.com/help/matlab/matlab_external/data-types-for-passing-mex-function-data-1.html
3+
4+
#include "mex.hpp"
5+
#include "mexAdapter.hpp"
6+
7+
#include <string>
8+
// note: <string_view> causes compile failures with MSVC at least
9+
10+
#include <vector>
11+
#include <memory>
12+
13+
#if defined(_WIN32)
14+
# include "win32_fs.h"
15+
#endif
16+
17+
#if __has_include(<filesystem>)
18+
# include <filesystem>
19+
# include <system_error>
20+
#endif
21+
22+
#include <sys/types.h>
23+
#include <sys/stat.h>
24+
25+
26+
static bool fs_is_symlink(std::string path)
27+
{
28+
29+
#if defined(__MINGW32__) || (defined(_WIN32) && !defined(__cpp_lib_filesystem))
30+
return fs_win32_is_symlink(path);
31+
#elif defined(__cpp_lib_filesystem)
32+
// std::filesystem::symlink_status doesn't detect symlinks on MinGW
33+
std::error_code ec;
34+
const auto s = std::filesystem::symlink_status(path, ec);
35+
return !ec && std::filesystem::is_symlink(s);
36+
#else
37+
struct stat s;
38+
return lstat(path.data(), &s) == 0 && S_ISLNK(s.st_mode);
39+
#endif
40+
}
41+
42+
43+
class MexFunction : public matlab::mex::Function {
44+
public:
45+
void operator()(matlab::mex::ArgumentList outputs, matlab::mex::ArgumentList inputs) {
46+
// boilerplate engine & ArrayFactory setup
47+
std::shared_ptr<matlab::engine::MATLABEngine> matlabPtr = getEngine();
48+
49+
matlab::data::ArrayFactory factory;
50+
// boilerplate input checks
51+
if (inputs.size() != 1) {
52+
matlabPtr->feval(u"error", 0,
53+
std::vector<matlab::data::Array>({ factory.createScalar("One input required") }));
54+
}
55+
if (inputs[0].getType() != matlab::data::ArrayType::MATLAB_STRING) {
56+
matlabPtr->feval(u"error", 0,
57+
std::vector<matlab::data::Array>({ factory.createScalar("Input must be a string") }));
58+
}
59+
if (inputs[0].getNumberOfElements() != 1) {
60+
matlabPtr->feval(u"error", 0,
61+
std::vector<matlab::data::Array>({ factory.createScalar("Input must be a scalar string") }));
62+
}
63+
64+
// Matlab strings are an array, so we use [0][0] to get the first element
65+
std::string inputStr = inputs[0][0];
66+
67+
// actual function algorithm / computation
68+
bool y = fs_is_symlink(inputStr);
69+
70+
// convert to Matlab output -- even scalars are arrays in Matlab
71+
// https://www.mathworks.com/help/matlab/matlab_external/create-matlab-array-with-matlab-data-cpp-api.html
72+
// https://www.mathworks.com/help/matlab/apiref/matlab.data.arrayfactory.html
73+
74+
outputs[0] = factory.createArray<bool>({1,1}, {y});
75+
}
76+
};

src/win32_fs.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
bool fs_win32_is_symlink(std::string path);

src/windows.cpp

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
#include <string>
2+
#include <cstddef>
3+
4+
#define WIN32_LEAN_AND_MEAN
5+
#include <windows.h>
6+
#include <winioctl.h>
7+
8+
#include "win32_fs.h"
9+
10+
11+
// create type PREPARSE_DATA_BUFFER
12+
// from ntifs.h, which can only be used by drivers
13+
// typedef is copied from https://gitlab.kitware.com/utils/kwsys/-/blob/master/SystemTools.cxx
14+
// that has a BSD 3-clause license
15+
typedef struct _REPARSE_DATA_BUFFER
16+
{
17+
ULONG ReparseTag;
18+
USHORT ReparseDataLength;
19+
USHORT Reserved;
20+
union
21+
{
22+
struct
23+
{
24+
USHORT SubstituteNameOffset;
25+
USHORT SubstituteNameLength;
26+
USHORT PrintNameOffset;
27+
USHORT PrintNameLength;
28+
ULONG Flags;
29+
WCHAR PathBuffer[1];
30+
} SymbolicLinkReparseBuffer;
31+
struct
32+
{
33+
USHORT SubstituteNameOffset;
34+
USHORT SubstituteNameLength;
35+
USHORT PrintNameOffset;
36+
USHORT PrintNameLength;
37+
WCHAR PathBuffer[1];
38+
} MountPointReparseBuffer;
39+
struct
40+
{
41+
UCHAR DataBuffer[1];
42+
} GenericReparseBuffer;
43+
struct
44+
{
45+
ULONG Version;
46+
WCHAR StringList[1];
47+
// In version 3, there are 4 NUL-terminated strings:
48+
// * Package ID
49+
// * Entry Point
50+
// * Executable Path
51+
// * Application Type
52+
} AppExecLinkReparseBuffer;
53+
} DUMMYUNIONNAME;
54+
} REPARSE_DATA_BUFFER, *PREPARSE_DATA_BUFFER;
55+
56+
57+
static bool fs_win32_get_reparse_buffer(std::string path, std::byte* buffer)
58+
{
59+
// this function is adapted from
60+
// https://gitlab.kitware.com/utils/kwsys/-/blob/master/SystemTools.cxx
61+
// that has a BSD 3-clause license
62+
63+
const DWORD attr = GetFileAttributesA(path.data());
64+
// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfileattributesa
65+
66+
// https://learn.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants
67+
if (attr == INVALID_FILE_ATTRIBUTES || !(attr & FILE_ATTRIBUTE_REPARSE_POINT))
68+
return false;
69+
70+
// Using 0 instead of GENERIC_READ as it allows reading of file attributes
71+
// even if we do not have permission to read the file itself
72+
73+
// A reparse point may be an execution alias (Windows Store app), which
74+
// is similar to a symlink but it cannot be opened as a regular file.
75+
// We must look at the reparse point data explicitly.
76+
77+
// FILE_ATTRIBUTE_REPARSE_POINT means:
78+
// * a file or directory that has an associated reparse point, or
79+
// * a file that is a symbolic link.
80+
HANDLE h = CreateFileA(
81+
path.data(), 0, 0, nullptr, OPEN_EXISTING,
82+
FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr);
83+
84+
if (h == INVALID_HANDLE_VALUE)
85+
return false;
86+
87+
DWORD bytesReturned = 0;
88+
89+
BOOL ok = DeviceIoControl(h, FSCTL_GET_REPARSE_POINT, nullptr, 0, buffer,
90+
MAXIMUM_REPARSE_DATA_BUFFER_SIZE, &bytesReturned,
91+
nullptr);
92+
93+
CloseHandle(h);
94+
95+
return ok;
96+
}
97+
98+
99+
bool fs_win32_is_symlink(std::string path)
100+
{
101+
// distinguish between Windows symbolic links and reparse points as
102+
// reparse points can be unlike symlinks.
103+
//
104+
// this function is adapted from
105+
// https://gitlab.kitware.com/utils/kwsys/-/blob/master/SystemTools.cxx
106+
// that has a BSD 3-clause license
107+
108+
std::byte buffer[MAXIMUM_REPARSE_DATA_BUFFER_SIZE];
109+
// Since FILE_ATTRIBUTE_REPARSE_POINT is set this file must be
110+
// a symbolic link if it is not a reparse point.
111+
if (!fs_win32_get_reparse_buffer(path, buffer))
112+
return GetLastError() == ERROR_NOT_A_REPARSE_POINT;
113+
114+
ULONG reparseTag =
115+
reinterpret_cast<PREPARSE_DATA_BUFFER>(&buffer[0])->ReparseTag;
116+
117+
return (reparseTag == IO_REPARSE_TAG_SYMLINK) ||
118+
(reparseTag == IO_REPARSE_TAG_MOUNT_POINT);
119+
120+
}

0 commit comments

Comments
 (0)