mirror of
https://github.com/ruby/ruby.git
synced 2026-07-31 09:25:15 +08:00
458 lines
13 KiB
C
458 lines
13 KiB
C
#include "ruby.h"
|
||
#include "internal.h"
|
||
#include "internal/file.h"
|
||
#include "internal/string.h"
|
||
#include "internal/vm.h"
|
||
|
||
#if defined __CYGWIN__ || defined DOSISH
|
||
# define drive_letter 1
|
||
# define alt_separator 1
|
||
# define isdirsep(x) ((x) == '/' || (x) == '\\')
|
||
#else
|
||
# define drive_letter 0
|
||
# define alt_separator 0
|
||
# define isdirsep(x) ((x) == '/')
|
||
#endif
|
||
|
||
static VALUE rb_cPathname;
|
||
static ID id_at_path;
|
||
static ID id_sub;
|
||
|
||
static VALUE
|
||
check_strpath(VALUE path)
|
||
{
|
||
Check_Type(path, T_STRING);
|
||
rb_get_path_check_no_convert(path);
|
||
return path;
|
||
}
|
||
|
||
static VALUE
|
||
get_strpath(VALUE obj)
|
||
{
|
||
VALUE strpath;
|
||
strpath = rb_ivar_get(obj, id_at_path);
|
||
if (!RB_TYPE_P(strpath, T_STRING))
|
||
rb_raise(rb_eTypeError, "unexpected @path");
|
||
rb_get_path_check_no_convert(strpath);
|
||
return strpath;
|
||
}
|
||
|
||
/*
|
||
* call-seq:
|
||
* self <=> other -> -1, 0, 1, or nil
|
||
*
|
||
* Compares the contents of +self+ and +other+ as strings;
|
||
* see String#<=>.
|
||
*
|
||
* Returns:
|
||
*
|
||
* - <tt>-1</tt> if +self+'s string is smaller than +other+'s string.
|
||
* - <tt>0</tt> if the two are equal.
|
||
* - <tt>1</tt> if +self+'s string is larger than +other+'s string.
|
||
* - <tt>nil</tt> if +other+ is not a \Pathname.
|
||
*
|
||
* Examples:
|
||
*
|
||
* Pathname('a') <=> Pathname('b') # => -1
|
||
* Pathname('a') <=> Pathname('ab') # => -1
|
||
* Pathname('a') <=> Pathname('a') # => 0
|
||
* Pathname('b') <=> Pathname('a') # => 1
|
||
* Pathname('ab') <=> Pathname('a') # => 1
|
||
* Pathname('ab') <=> 'a' # => nil
|
||
*
|
||
* Two pathnames that are different may refer to the same entry in the filesystem:
|
||
*
|
||
* Pathname('lib') <=> Pathname('./lib') # => 1
|
||
*
|
||
*/
|
||
static VALUE
|
||
path_cmp(VALUE self, VALUE other)
|
||
{
|
||
VALUE s1, s2;
|
||
char *p1, *p2;
|
||
char *e1, *e2;
|
||
if (!rb_obj_is_kind_of(other, rb_cPathname))
|
||
return Qnil;
|
||
s1 = get_strpath(self);
|
||
s2 = get_strpath(other);
|
||
p1 = RSTRING_PTR(s1);
|
||
p2 = RSTRING_PTR(s2);
|
||
e1 = p1 + RSTRING_LEN(s1);
|
||
e2 = p2 + RSTRING_LEN(s2);
|
||
while (p1 < e1 && p2 < e2) {
|
||
int c1, c2;
|
||
c1 = (unsigned char)*p1++;
|
||
c2 = (unsigned char)*p2++;
|
||
if (c1 == '/') c1 = '\0';
|
||
if (c2 == '/') c2 = '\0';
|
||
if (c1 != c2) {
|
||
if (c1 < c2)
|
||
return INT2FIX(-1);
|
||
else
|
||
return INT2FIX(1);
|
||
}
|
||
}
|
||
if (p1 < e1)
|
||
return INT2FIX(1);
|
||
if (p2 < e2)
|
||
return INT2FIX(-1);
|
||
return INT2FIX(0);
|
||
}
|
||
|
||
/*
|
||
* :markup: markdown
|
||
*
|
||
* call-seq:
|
||
* sub(pattern, replacement) -> new_pathname
|
||
* sub(pattern) {|match| ... } -> new_pathname
|
||
*
|
||
* Returns a new pathname whose path is the path in `self`,
|
||
* after the specified substitutions.
|
||
*
|
||
* Argument `pattern` may be a string or a Regexp;
|
||
* argument `replacement` may be a string or a hash.
|
||
*
|
||
* Varying types for the argument values makes this method very versatile.
|
||
*
|
||
* Below are some simple examples;
|
||
* for many more related examples (using strings, not pathnames),
|
||
* see [Substitution Methods](rdoc-ref:String@Substitution+Methods).
|
||
*
|
||
* With arguments `pattern` and string `replacement` given,
|
||
* replaces the first matching substring with the given replacement string:
|
||
*
|
||
* ```ruby
|
||
* pn = Pathname('abracadabra.txt') # => #<Pathname:abracadabra.txt>
|
||
* pn.sub('bra', 'xyzzy') # => #<Pathname:axyzzycadabra.txt>
|
||
* pn.sub(/bra/, 'xyzzy') # => #<Pathname:axyzzycadabra.txt>
|
||
* pn.sub('nope', 'xyzzy') # => #<Pathname:abracadabra.txt>
|
||
* ```
|
||
*
|
||
* With arguments `pattern` and hash `replacement` given,
|
||
* replaces the first matching substring with a value from the given replacement hash,
|
||
* or removes it:
|
||
*
|
||
* ```ruby
|
||
* h = {'a' => 'A', 'b' => 'B', 'c' => 'C'}
|
||
* pn.sub('b', h) # => #<Pathname:aBracadabra.txt>
|
||
* pn.sub(/b/, h) # => #<Pathname:aBracadabra.txt>
|
||
* pn.sub(/d/, h) # => #<Pathname:abracaabra.txt> # 'd' removed.
|
||
* ```
|
||
*
|
||
* With argument `pattern` and a block given,
|
||
* calls the block with the first matching substring;
|
||
* replaces that substring with the block’s return value:
|
||
*
|
||
* ```ruby
|
||
* pn.sub('b') {|match| match.upcase } # => #<Pathname:aBracadabra.txt>
|
||
* pn.sub(/X/) {|match| match.upcase } # => #<Pathname:abracadabra.txt>
|
||
* ```
|
||
*
|
||
*/
|
||
static VALUE
|
||
path_sub(int argc, VALUE *argv, VALUE self)
|
||
{
|
||
VALUE str = get_strpath(self);
|
||
|
||
if (rb_block_given_p()) {
|
||
str = rb_block_call(str, id_sub, argc, argv, 0, 0);
|
||
}
|
||
else {
|
||
str = rb_funcallv(str, id_sub, argc, argv);
|
||
}
|
||
return rb_class_new_instance(1, &str, rb_obj_class(self));
|
||
}
|
||
|
||
/* :nodoc: */
|
||
static VALUE
|
||
same_paths(VALUE self, VALUE a, VALUE b)
|
||
{
|
||
check_strpath(a);
|
||
check_strpath(b);
|
||
if (CASEFOLD_FILESYSTEM)
|
||
return RBOOL(rb_str_casecmp(a, b) == INT2FIX(0));
|
||
else
|
||
return rb_str_equal(a, b);
|
||
}
|
||
|
||
/*
|
||
* :markup: markdown
|
||
*
|
||
* call-seq:
|
||
* root? -> true or false
|
||
*
|
||
* Returns whether the path in `self` points to a root directory.
|
||
*
|
||
* On a non-Windows system, a root directory path is one whose name begins
|
||
* with one or more slash characters (`'/'):
|
||
*
|
||
* ```ruby
|
||
* Pathname('/').root? # => true
|
||
* Pathname('////').root? # => true
|
||
* Pathname('/usr').root? # => false
|
||
* Pathname('foo').root? # => false
|
||
* ```
|
||
*
|
||
* Does not resolve dot directories:
|
||
*
|
||
* ```ruby
|
||
* Pathname('/usr/.').root? # => false
|
||
* Pathname('/usr/..').root? # => false
|
||
* ```
|
||
*
|
||
* On a Windows system, a root directory path is one whose name begins as above,
|
||
* or with a device letter followed by a colon character (`':'`)
|
||
* and one or more slash characters (`'/'):
|
||
*
|
||
* ```ruby
|
||
* Pathname('/').root? # => true
|
||
* Pathname('////').root? # => true
|
||
* Pathname('C:/').root? # => true
|
||
* Pathname('C:////').root? # => true
|
||
* Pathname('c:/').root? # => true
|
||
* Pathname('H:/').root? # => true
|
||
* Pathname('C:/m').root? # => false
|
||
* Pathname('C:').root? # => false
|
||
* ```
|
||
*
|
||
*/
|
||
static VALUE
|
||
path_root_p(VALUE self)
|
||
{
|
||
VALUE path = get_strpath(self);
|
||
if (RSTRING_LEN(path) == 0) return Qfalse;
|
||
const char *ptr = RSTRING_PTR(path), *end = RSTRING_END(path);
|
||
rb_encoding *enc = rb_enc_get(path);
|
||
const char *base = rb_enc_path_skip_prefix_root(ptr, end, enc);
|
||
return RBOOL(base == end);
|
||
}
|
||
|
||
/*
|
||
* call-seq:
|
||
* absolute? -> true or false
|
||
*
|
||
* Returns whether +self+ contains an absolute path:
|
||
*
|
||
* Pathname('/home').absolute? # => true
|
||
* Pathname('lib').absolute? # => false
|
||
*
|
||
* The result is OS-dependent for some paths:
|
||
*
|
||
* Pathname('C:/').absolute? # => true # On Windows.
|
||
* Pathname('C:/').absolute? # => false # Elsewhere.
|
||
*
|
||
*/
|
||
static VALUE
|
||
path_absolute_p(VALUE self)
|
||
{
|
||
VALUE path = get_strpath(self);
|
||
const char *ptr = RSTRING_PTR(path);
|
||
long len = RSTRING_LEN(path);
|
||
if (len < 1) return Qfalse;
|
||
if (drive_letter) {
|
||
if (len >= 2 && ISALPHA(ptr[0]) && (ptr[1] == ':')) return Qtrue;
|
||
}
|
||
return RBOOL(isdirsep(ptr[0]));
|
||
}
|
||
|
||
/* :nodoc: */
|
||
static VALUE
|
||
has_separator_p(VALUE self, VALUE path)
|
||
{
|
||
const char *ptr = RSTRING_PTR(check_strpath(path));
|
||
const char *end = RSTRING_END(path);
|
||
if (alt_separator) {
|
||
rb_encoding *enc = rb_enc_get(path);
|
||
bool mb = !rb_str_enc_fastpath(path);
|
||
while (ptr < end) {
|
||
if (isdirsep(*ptr)) return Qtrue;
|
||
ptr += (mb ? rb_enc_mbclen(ptr, end, enc) : 1);
|
||
}
|
||
}
|
||
else {
|
||
/* assume '/' will never be trailing bytes */
|
||
if (memchr(ptr, '/', end - ptr)) return Qtrue;
|
||
}
|
||
return Qfalse;
|
||
}
|
||
|
||
/*
|
||
* :markup: markdown
|
||
*
|
||
* call-seq:
|
||
* sub_ext(replacement) -> new_pathname
|
||
*
|
||
* Returns a new pathname whose path is the path in `self`,
|
||
* after specified changes:
|
||
*
|
||
* ```ruby
|
||
* Pathname('t.tmp').sub_ext('.txt') # => #<Pathname:t.txt> # Extension replaced.
|
||
* Pathname('temp').sub_ext('.txt') # => #<Pathname:temp.txt> # Extension added.
|
||
* Pathname('t.tmp').sub_ext('') # => #<Pathname:t> # Extension removed.
|
||
* ```
|
||
*
|
||
*/
|
||
static VALUE
|
||
path_sub_ext(VALUE self, VALUE repl)
|
||
{
|
||
VALUE path = get_strpath(self);
|
||
long len = RSTRING_LEN(path);
|
||
const char *ptr = RSTRING_PTR(path);
|
||
const char *ext = ruby_enc_find_extname(ptr, &len, rb_enc_get(path));
|
||
if (len > 0) {
|
||
RUBY_ASSERT(ext, "should point the last dot");
|
||
path = rb_str_subseq(path, 0, ext - ptr);
|
||
}
|
||
else {
|
||
/* no dot or dotted file */
|
||
path = rb_str_dup(path);
|
||
}
|
||
path = rb_str_append(path, repl);
|
||
return rb_class_new_instance(1, &path, rb_obj_class(self));
|
||
}
|
||
|
||
/* :nodoc: */
|
||
/* chop_basename(path) -> [pre-basename, basename] or nil */
|
||
static VALUE
|
||
chop_basename(VALUE self, VALUE path)
|
||
{
|
||
long baselen, alllen = RSTRING_LEN(check_strpath(path));
|
||
if (alllen <= 0) return Qnil;
|
||
rb_encoding *enc = rb_enc_get(path);
|
||
const char *name = RSTRING_PTR(path);
|
||
const char *base = ruby_enc_find_basename(name, &baselen, &alllen, enc);
|
||
if (baselen < 1) return Qnil;
|
||
if (baselen == 1 && isdirsep(*base)) return Qnil;
|
||
RUBY_ASSERT(base >= name);
|
||
RUBY_ASSERT(base <= RSTRING_END(path));
|
||
VALUE dir = rb_str_subseq(path, 0, base - name);
|
||
VALUE basename = rb_enc_str_new(base, alllen, enc);
|
||
RB_GC_GUARD(path);
|
||
return rb_assoc_new(dir, basename);
|
||
}
|
||
|
||
/* :nodoc: */
|
||
/* split_names(path) -> prefix, [name, ...] */
|
||
static VALUE
|
||
split_names(VALUE self, VALUE path)
|
||
{
|
||
rb_encoding *enc = rb_enc_get(check_strpath(path));
|
||
const char *beg = RSTRING_PTR(path), *ptr = beg;
|
||
const char *end = RSTRING_END(path);
|
||
const char *root = rb_enc_path_skip_prefix_root(ptr, end, enc);
|
||
VALUE pre = rb_str_subseq(path, 0, root - ptr);
|
||
VALUE names = rb_ary_new();
|
||
while (ptr < end) {
|
||
const char *next = rb_enc_path_next(ptr, end, enc);
|
||
if (next > ptr) rb_ary_push(names, rb_str_subseq(path, ptr - beg, next - ptr));
|
||
ptr = next;
|
||
while (ptr < end && isdirsep(*ptr)) ++ptr;
|
||
}
|
||
return rb_assoc_new(pre, names);
|
||
}
|
||
|
||
/* :nodoc: */
|
||
/* has_trailing_separator?(path) -> bool */
|
||
static VALUE
|
||
has_trailing_separator(VALUE self, VALUE path)
|
||
{
|
||
long baselen, alllen = RSTRING_LEN(check_strpath(path));
|
||
if (alllen <= 0) return Qfalse;
|
||
rb_encoding *enc = rb_enc_get(path);
|
||
const char *name = RSTRING_PTR(path);
|
||
const char *base = ruby_enc_find_basename(name, &baselen, &alllen, enc);
|
||
if (baselen < 1) return Qfalse;
|
||
if (baselen == 1 && isdirsep(*base)) return Qfalse;
|
||
return RBOOL(base + alllen < RSTRING_END(path));
|
||
}
|
||
|
||
/* :nodoc: */
|
||
/* add_trailing_separator(path) -> path */
|
||
static VALUE
|
||
add_trailing_separator(VALUE self, VALUE path)
|
||
{
|
||
if (RSTRING_LEN(check_strpath(path)) <= 0) return path;
|
||
rb_encoding *enc = rb_enc_get(path);
|
||
const char *name = RSTRING_PTR(path);
|
||
const char *end = RSTRING_END(path);
|
||
const char *top = rb_enc_path_skip_prefix(name, end, enc);
|
||
if (top < end && isdirsep(end[-1])) {
|
||
if (end[-1] == '/' || rb_enc_prev_char(top, end, end, enc) == end - 1)
|
||
return path;
|
||
}
|
||
return rb_str_cat_cstr(rb_str_dup(path), "/");
|
||
}
|
||
|
||
/* :nodoc: */
|
||
static VALUE
|
||
del_trailing_separator(VALUE self, VALUE path)
|
||
{
|
||
long len = RSTRING_LEN(check_strpath(path));
|
||
if (len <= 0) return path;
|
||
rb_encoding *enc = rb_enc_get(path);
|
||
const char *name = RSTRING_PTR(path);
|
||
const char *end = name + len, *tail = end;
|
||
const char *top = rb_enc_path_skip_prefix(name, end, enc);
|
||
if (tail > top && isdirsep(tail[-1])) {
|
||
while (--tail > top && isdirsep(tail[-1]));
|
||
if (tail > top &&
|
||
tail[0] != '/' &&
|
||
!rb_str_enc_fastpath(path) &&
|
||
rb_enc_left_char_head(top, tail, end, enc) != tail) {
|
||
/* trailing byte, not a directory separator */
|
||
++tail;
|
||
}
|
||
if (tail < end) {
|
||
if (tail == name || (drive_letter && tail == top && top[-1] == ':')) {
|
||
++tail;
|
||
}
|
||
}
|
||
}
|
||
if (tail == end) return path;
|
||
return rb_str_subseq(path, 0, tail - name);
|
||
}
|
||
|
||
#include "pathname_builtin.rbinc"
|
||
|
||
static void init_ids(void);
|
||
|
||
void
|
||
Init_pathname(void)
|
||
{
|
||
#ifdef HAVE_RB_EXT_RACTOR_SAFE
|
||
rb_ext_ractor_safe(true);
|
||
#endif
|
||
|
||
init_ids();
|
||
InitVM(pathname);
|
||
}
|
||
|
||
void
|
||
InitVM_pathname(void)
|
||
{
|
||
rb_cPathname = rb_define_class("Pathname", rb_cObject);
|
||
rb_define_method(rb_cPathname, "<=>", path_cmp, 1);
|
||
rb_define_method(rb_cPathname, "sub", path_sub, -1);
|
||
rb_define_method(rb_cPathname, "sub_ext", path_sub_ext, 1);
|
||
rb_define_method(rb_cPathname, "root?", path_root_p, 0);
|
||
rb_define_method(rb_cPathname, "absolute?", path_absolute_p, 0);
|
||
|
||
rb_define_private_method(rb_cPathname, "same_paths?", same_paths, 2);
|
||
rb_define_private_method(rb_cPathname, "has_separator?", has_separator_p, 1);
|
||
rb_define_private_method(rb_cPathname, "chop_basename", chop_basename, 1);
|
||
rb_define_private_method(rb_cPathname, "split_names", split_names, 1);
|
||
rb_define_private_method(rb_cPathname, "has_trailing_separator?", has_trailing_separator, 1);
|
||
rb_define_private_method(rb_cPathname, "add_trailing_separator", add_trailing_separator, 1);
|
||
rb_define_private_method(rb_cPathname, "del_trailing_separator", del_trailing_separator, 1);
|
||
|
||
rb_provide("pathname.so");
|
||
}
|
||
|
||
void
|
||
init_ids(void)
|
||
{
|
||
#undef rb_intern
|
||
id_at_path = rb_intern("@path");
|
||
id_sub = rb_intern("sub");
|
||
}
|