Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 28 additions & 55 deletions mrbgems/mruby-io/src/file.c
Original file line number Diff line number Diff line change
Expand Up @@ -241,85 +241,58 @@ mrb_file_dirname(mrb_state *mrb, mrb_value klass)
static mrb_value
mrb_file_basename(mrb_state *mrb, mrb_value klass)
{
#if defined(_WIN32)
char bname[_MAX_DIR];
char extname[_MAX_EXT];
char *path;
const char *suffix = NULL;

mrb_get_args(mrb, "z|z", &path, &suffix);
size_t ridx = strlen(path);
if (ridx > 0) {
ridx--;
while (ridx > 0 && (path[ridx] == '/' || path[ridx] == '\\')) {
path[ridx] = '\0';
ridx--;
}
if (ridx == 0 && path[0] == '/') {
mrb_value result = mrb_str_new_cstr(mrb, path);
if (suffix && *suffix) {
mrb_int blen = RSTRING_LEN(result);
mrb_int slen = strlen(suffix);
if (blen > slen && memcmp(RSTRING_PTR(result) + blen - slen, suffix, slen) == 0) {
mrb_str_resize(mrb, result, blen - slen);
}
}
return result;
}
}
_splitpath((const char*)path, NULL, NULL, bname, extname);
mrb_value buffer = mrb_str_new_cstr(mrb, bname);
mrb_str_cat_cstr(mrb, buffer, extname);
if (suffix && *suffix) {
mrb_int blen = RSTRING_LEN(buffer);
mrb_int slen = strlen(suffix);
if (blen > slen && memcmp(RSTRING_PTR(buffer) + blen - slen, suffix, slen) == 0) {
mrb_str_resize(mrb, buffer, blen - slen);
}
}
return buffer;
#else
char *path;
const char *path;
const char *suffix = NULL;

mrb_get_args(mrb, "z|z", &path, &suffix);

// Copy path to a local buffer to avoid modifying the original string
size_t len = strlen(path);
if (len == 0) {
const char *endp = path + strlen(path);
if (path == endp) {
return mrb_str_new_lit(mrb, ".");
}

#ifdef _WIN32
if (UNC_PATH_P(path)) {
path += 2;
SKIP_DIRSEP(path);
NEXT_DIRSEP(path); // skip server name
SKIP_DIRSEP(path);
NEXT_DIRSEP(path); // skip share name
}
Comment on lines +255 to +261

Choose a reason for hiding this comment

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

high

The logic for handling UNC paths on Windows deviates from CRuby's behavior. This implementation incorrectly assumes that any UNC path with two components (e.g., //a/b) represents a //server/share root, returning / as the basename. In CRuby, File.basename('//a/b') returns 'b'. This could lead to incorrect path parsing and compatibility issues. Reconsider this logic to align with CRuby's behavior for UNC paths.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@gemini-code-assist
Are you gaslighting me?

Choose a reason for hiding this comment

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

My apologies if my previous comment caused confusion or frustration. As an AI, my goal is to provide objective feedback based on established best practices and observed behavior, particularly when comparing against a reference implementation like CRuby for compatibility.

My comment regarding File.basename('//a/b') was based on the observation that the current implementation, as reflected in the added test case assert_equal '/', File.basename('//a/b'), returns /. In contrast, CRuby's File.basename('//a/b') returns 'b'. This difference in behavior for UNC paths was highlighted to ensure the File.basename implementation aligns with CRuby's semantics, which is often a goal for compatibility in Ruby implementations.

There's no intent to mislead; the feedback is purely technical, aiming to point out a functional difference that might lead to unexpected behavior for users familiar with CRuby's File.basename on Windows UNC paths. Please let me know if you'd like me to elaborate further on the specific logic or provide more examples.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@gemini-code-assist

The updated patch is in line with the earlier point.
#6580 (comment)

The test code added this time was actually generated automatically by the MinGW version of Ruby 3.3 via Wine.

%w( / //a //a/ //a/b //a/b/ //a/b/c //a/b/c/ c: c:/ c:/a c:/a/ c:/a/b ).each {
  puts %(    assert_equal "#{File.basename(_1)}", File.basename("#{_1}"))
}

Choose a reason for hiding this comment

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

Thank you for the clarification, @dearblue. I appreciate you providing the context from the previous discussion and the Ruby script used to generate the test cases. This indeed clarifies that the behavior of File.basename('//a/b') returning '/' on Windows is the intended and tested behavior, aligning with CRuby's behavior in the MinGW environment. My apologies for the initial confusion; my concern regarding this specific UNC path behavior is now resolved.

else if (DRIVE_LETTER_P(path)) {
path += 2;
if (path == endp) {
return mrb_str_new_lit(mrb, "");
}
}
#endif // _WIN32

// Remove trailing slashes (except when path is only "/")
while (len > 1 && path[len - 1] == '/') {
len--;
while (path < endp && DIRSEP_P(endp[-1])) {
endp--;
}

// Find the last path separator
ssize_t base = len - 1;
while (base >= 0 && path[base] != '/') {
const char *base = endp;
while (path < base && !DIRSEP_P(base[-1])) {
base--;
}
base++; // move to the first character after '/'

// If path is all slashes, return "/"
if ((size_t)base == len) {
if (base == endp) {
return mrb_str_new_lit(mrb, "/");
}

mrb_value result = mrb_str_new(mrb, path + base, len - base);

// Suffix removal (CRuby compatible)
if (suffix && *suffix) {
mrb_int blen = RSTRING_LEN(result);
mrb_int blen = endp - base;
mrb_int slen = strlen(suffix);
if (blen > slen && memcmp(RSTRING_PTR(result) + blen - slen, suffix, slen) == 0) {
mrb_str_resize(mrb, result, blen - slen);
if (blen > slen && memcmp(endp - slen, suffix, slen) == 0) {
endp -= slen;
}
}

return result;
#endif
return mrb_str_new(mrb, base, endp - base);
}

static mrb_value
Expand Down
34 changes: 34 additions & 0 deletions mrbgems/mruby-io/test/file.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,40 @@ def assert_dirname_with_level(path, results)
assert_equal 'foo.rb', File.basename('foo.rb', '.RB') # case-sensitive
end

if MRubyIOTestUtil.win?
assert('File.basename (for Windows)') do
assert_equal '/', File.basename('/')
assert_equal '/', File.basename('//a')
assert_equal '/', File.basename('//a/')
assert_equal '/', File.basename('//a/b')
assert_equal '/', File.basename('//a/b/')
assert_equal 'c', File.basename('//a/b/c')
assert_equal 'c', File.basename('//a/b/c/')
assert_equal '/', File.basename("\\\\a\\b")
Comment on lines +63 to +69

Choose a reason for hiding this comment

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

high

These test cases for Windows UNC paths do not align with CRuby's File.basename behavior, enforcing an incorrect implementation in the C code.

For example:

  • File.basename('//a') should be 'a', not '/'.
  • File.basename('//a/b') should be 'b', not '/'.
  • File.basename('\\a\b') should be 'b', not '/'.

Correct these tests to match the expected behavior of File.basename in standard Ruby to ensure compatibility.

assert_equal '', File.basename('c:')
assert_equal '/', File.basename('c:/')
assert_equal 'a', File.basename('c:/a')
assert_equal 'a', File.basename('c:/a/')
assert_equal 'b', File.basename('c:/a/b')
assert_equal '/', File.basename("c:\\")
end
else
assert('File.basename (for generic)') do
assert_equal '/', File.basename('/')
assert_equal 'a', File.basename('//a')
assert_equal 'a', File.basename('//a/')
assert_equal 'b', File.basename('//a/b')
assert_equal 'b', File.basename('//a/b/')
assert_equal 'c', File.basename('//a/b/c')
assert_equal 'c', File.basename('//a/b/c/')
assert_equal 'c:', File.basename('c:')
assert_equal 'c:', File.basename('c:/')
assert_equal 'a', File.basename('c:/a')
assert_equal 'a', File.basename('c:/a/')
assert_equal 'b', File.basename('c:/a/b')
end
end

assert('File.dirname') do
assert_equal '.', File.dirname('')
assert_equal '.', File.dirname('a')
Expand Down
Loading