diff --git a/array.c b/array.c index e427cb3320e141..cc15660c8a95ae 100644 --- a/array.c +++ b/array.c @@ -295,6 +295,33 @@ rb_ary_frozen_p(VALUE ary) return Qfalse; } +/* This can be used to take a snapshot of an array (with + e.g. rb_ary_replace) and check later whether the array has been + modified from the snapshot. The snapshot is cheap, though if + something does modify the array it will pay the cost of copying + it. */ +VALUE +rb_ary_dup_of_p(VALUE ary1, VALUE ary2) +{ + VALUE *p1, *p2; + long len = RARRAY_LEN(ary1); + + if (len != RARRAY_LEN(ary2)) return Qfalse; + + p1 = RARRAY_PTR(ary1); + p2 = RARRAY_PTR(ary2); + + if (ARY_EMBED_P(ary1) && ARY_EMBED_P(ary2)) { + for (; len; len--, p1++, p2++) { + if (*p1 != *p2) return Qfalse; + } + return Qtrue; + } + + if (p1 == p2) return Qtrue; + return Qfalse; +} + static VALUE ary_alloc(VALUE klass) { diff --git a/file.c b/file.c index fd64116d2432f5..cd071e10be3f4f 100644 --- a/file.c +++ b/file.c @@ -149,40 +149,60 @@ file_path_convert(VALUE name) return name; } -static VALUE -rb_get_path_check(VALUE obj, int level) +static rb_encoding * +check_path_encoding(VALUE str) +{ + rb_encoding *enc = rb_enc_get(str); + if (!rb_enc_asciicompat(enc)) { + rb_raise(rb_eEncCompatError, "path name must be ASCII-compatible (%s): %s", + rb_enc_name(enc), RSTRING_PTR(rb_str_inspect(str))); + } + return enc; +} + +VALUE +rb_get_path_check_to_string(VALUE obj, int level) { VALUE tmp; ID to_path; - rb_encoding *enc; if (insecure_obj_p(obj, level)) { rb_insecure_operation(); } + if (RB_TYPE_P(obj, T_STRING)) { + return obj; + } CONST_ID(to_path, "to_path"); tmp = rb_check_funcall(obj, to_path, 0, 0); if (tmp == Qundef) { tmp = obj; } StringValue(tmp); + return tmp; +} +VALUE +rb_get_path_check_convert(VALUE obj, VALUE tmp, int level) +{ tmp = file_path_convert(tmp); if (obj != tmp && insecure_obj_p(tmp, level)) { rb_insecure_operation(); } - enc = rb_enc_get(tmp); - if (!rb_enc_asciicompat(enc)) { - tmp = rb_str_inspect(tmp); - rb_raise(rb_eEncCompatError, "path name must be ASCII-compatible (%s): %s", - rb_enc_name(enc), RSTRING_PTR(tmp)); - } + check_path_encoding(tmp); StringValueCStr(tmp); return rb_str_new4(tmp); } +static VALUE +rb_get_path_check(VALUE obj, int level) +{ + VALUE tmp = rb_get_path_check_to_string(obj, level); + return rb_get_path_check_convert(obj, tmp, level); +} + VALUE rb_get_path_no_checksafe(VALUE obj) { @@ -3250,7 +3270,6 @@ rb_file_expand_path(VALUE fname, VALUE dname) VALUE rb_file_expand_path_fast(VALUE fname, VALUE dname) { - check_expand_path_args(fname, dname); return rb_file_expand_path_internal(fname, dname, 0, 0, EXPAND_PATH_BUFFER()); } @@ -5241,7 +5260,7 @@ rb_find_file_ext_safe(VALUE *filep, const char *const *ext, int safe_level) rb_raise(rb_eSecurityError, "loading from non-absolute path %s", f); } - RB_GC_GUARD(load_path) = rb_get_load_path(); + RB_GC_GUARD(load_path) = rb_get_expanded_load_path(); if (!load_path) return 0; fname = rb_str_dup(*filep); @@ -5306,7 +5325,7 @@ rb_find_file_safe(VALUE path, int safe_level) rb_raise(rb_eSecurityError, "loading from non-absolute path %s", f); } - RB_GC_GUARD(load_path) = rb_get_load_path(); + RB_GC_GUARD(load_path) = rb_get_expanded_load_path(); if (load_path) { long i; diff --git a/include/ruby/intern.h b/include/ruby/intern.h index 9745afdfdad0e1..78bd4fb49607e6 100644 --- a/include/ruby/intern.h +++ b/include/ruby/intern.h @@ -56,6 +56,7 @@ VALUE rb_ary_tmp_new(long); void rb_ary_free(VALUE); void rb_ary_modify(VALUE); VALUE rb_ary_freeze(VALUE); +VALUE rb_ary_dup_of_p(VALUE, VALUE); VALUE rb_ary_aref(int, VALUE*, VALUE); VALUE rb_ary_subseq(VALUE, long, long); void rb_ary_store(VALUE, long, VALUE); diff --git a/internal.h b/internal.h index 59c928462edfe7..f5af903304c592 100644 --- a/internal.h +++ b/internal.h @@ -94,6 +94,8 @@ VALUE rb_home_dir(const char *user, VALUE result); VALUE rb_realpath_internal(VALUE basedir, VALUE path, int strict); VALUE rb_file_expand_path_fast(VALUE, VALUE); VALUE rb_file_expand_path_internal(VALUE, VALUE, int, int, VALUE); +VALUE rb_get_path_check_to_string(VALUE, int); +VALUE rb_get_path_check_convert(VALUE, VALUE, int); void Init_File(void); #ifdef _WIN32 @@ -119,6 +121,7 @@ VALUE rb_iseq_clone(VALUE iseqval, VALUE newcbase); /* load.c */ VALUE rb_get_load_path(void); +VALUE rb_get_expanded_load_path(void); /* math.c */ VALUE rb_math_atan2(VALUE, VALUE); diff --git a/load.c b/load.c index 163ec4c44228db..e168044023c4a5 100644 --- a/load.c +++ b/load.c @@ -34,21 +34,120 @@ rb_get_load_path(void) return load_path; } -VALUE -rb_get_expanded_load_path(void) +enum expand_type { + EXPAND_ALL, + EXPAND_RELATIVE, + EXPAND_HOME, + EXPAND_NON_CACHE +}; + +/* Construct expanded load path and store it to cache. + We rebuild load path partially if the cache is invalid. + We don't cache non string object and expand it every time. We ensure that + string objects in $LOAD_PATH are frozen. + */ +static void +rb_construct_expanded_load_path(int type, int *has_relative, int *has_non_cache) { - VALUE load_path = rb_get_load_path(); + rb_vm_t *vm = GET_VM(); + VALUE load_path = vm->load_path; + VALUE expanded_load_path = vm->expanded_load_path; VALUE ary; long i; + int level = rb_safe_level(); ary = rb_ary_new2(RARRAY_LEN(load_path)); for (i = 0; i < RARRAY_LEN(load_path); ++i) { - VALUE path = rb_file_expand_path_fast(RARRAY_PTR(load_path)[i], Qnil); - rb_str_freeze(path); - rb_ary_push(ary, path); + VALUE path, as_str, expanded_path; + int is_string, non_cache; + char *as_cstr; + as_str = path = RARRAY_PTR(load_path)[i]; + is_string = RB_TYPE_P(path, T_STRING) ? 1 : 0; + non_cache = !is_string ? 1 : 0; + as_str = rb_get_path_check_to_string(path, level); + as_cstr = RSTRING_PTR(as_str); + + if (!non_cache) { + if ((type == EXPAND_RELATIVE && + rb_is_absolute_path(as_cstr)) || + (type == EXPAND_HOME && + (!as_cstr[0] || as_cstr[0] != '~')) || + (type == EXPAND_NON_CACHE)) { + /* Use cached expanded path. */ + rb_ary_push(ary, RARRAY_PTR(expanded_load_path)[i]); + continue; + } + } + if (!*has_relative && !rb_is_absolute_path(as_cstr)) + *has_relative = 1; + if (!*has_non_cache && non_cache) + *has_non_cache = 1; + /* Freeze only string object. We expand other objects every time. */ + if (is_string) + rb_str_freeze(path); + as_str = rb_get_path_check_convert(path, as_str, level); + expanded_path = rb_file_expand_path_fast(as_str, Qnil); + rb_str_freeze(expanded_path); + rb_ary_push(ary, expanded_path); } rb_obj_freeze(ary); - return ary; + vm->expanded_load_path = ary; + rb_ary_replace(vm->load_path_snapshot, vm->load_path); +} + +static VALUE +load_path_getcwd(void) +{ + char *cwd = my_getcwd(); + VALUE cwd_str = rb_filesystem_str_new_cstr(cwd); + xfree(cwd); + return cwd_str; +} + +VALUE +rb_get_expanded_load_path(void) +{ + rb_vm_t *vm = GET_VM(); + const VALUE non_cache = Qtrue; + + if (!rb_ary_dup_of_p(vm->load_path_snapshot, vm->load_path)) { + /* The load path was modified. Rebuild the expanded load path. */ + int has_relative = 0, has_non_cache = 0; + rb_construct_expanded_load_path(EXPAND_ALL, &has_relative, &has_non_cache); + if (has_relative) { + vm->load_path_check_cache = load_path_getcwd(); + } + else if (has_non_cache) { + /* Non string object. */ + vm->load_path_check_cache = non_cache; + } + else { + vm->load_path_check_cache = 0; + } + } + else if (vm->load_path_check_cache == non_cache) { + int has_relative = 1, has_non_cache = 1; + /* Expand only non-cacheable objects. */ + rb_construct_expanded_load_path(EXPAND_NON_CACHE, + &has_relative, &has_non_cache); + } + else if (vm->load_path_check_cache) { + int has_relative = 1, has_non_cache = 1; + VALUE cwd = load_path_getcwd(); + if (!rb_str_equal(vm->load_path_check_cache, cwd)) { + /* Current working directory or filesystem encoding was changed. + Expand relative load path and non-cacheable objects again. */ + vm->load_path_check_cache = cwd; + rb_construct_expanded_load_path(EXPAND_RELATIVE, + &has_relative, &has_non_cache); + } + else { + /* Expand only tilde (User HOME) and non-cacheable objects. */ + rb_construct_expanded_load_path(EXPAND_HOME, + &has_relative, &has_non_cache); + } + } + return vm->expanded_load_path; } static VALUE @@ -88,23 +187,22 @@ loaded_feature_path(const char *name, long vlen, const char *feature, long len, return 0; plen = e - name - len - 1; } + if (type == 's' && !IS_DLEXT(&name[plen+len+1]) + || type == 'r' && !IS_RBEXT(&name[plen+len+1]) + || name[plen] != '/') { + return 0; + } + /* Now name == "#{prefix}/#{feature}#{ext}" where ext is acceptable + (possibly empty) and prefix is some string of length plen. */ + for (i = 0; i < RARRAY_LEN(load_path); ++i) { VALUE p = RARRAY_PTR(load_path)[i]; const char *s = StringValuePtr(p); long n = RSTRING_LEN(p); - if (n != plen ) continue; - if (n && (strncmp(name, s, n) || name[n] != '/')) continue; - switch (type) { - case 's': - if (IS_DLEXT(&name[n+len+1])) return p; - break; - case 'r': - if (IS_RBEXT(&name[n+len+1])) return p; - break; - default: - return p; - } + if (n != plen) continue; + if (n && strncmp(name, s, n)) continue; + return p; } return 0; } @@ -175,6 +273,7 @@ rb_feature_p(const char *feature, const char *ext, int rb, int expanded, const c return 'r'; } } + loading_tbl = get_loading_table(); if (loading_tbl) { f = 0; @@ -183,7 +282,7 @@ rb_feature_p(const char *feature, const char *ext, int rb, int expanded, const c fs.name = feature; fs.len = len; fs.type = type; - fs.load_path = load_path ? load_path : rb_get_load_path(); + fs.load_path = load_path ? load_path : rb_get_expanded_load_path(); fs.result = 0; st_foreach(loading_tbl, loaded_feature_path_i, (st_data_t)&fs); if ((f = fs.result) != 0) { @@ -233,7 +332,7 @@ rb_feature_provided(const char *feature, const char **loading) if (*feature == '.' && (feature[1] == '/' || strncmp(feature+1, "./", 2) == 0)) { - fullpath = rb_file_expand_path_fast(rb_str_new2(feature), Qnil); + fullpath = rb_file_expand_path_fast(rb_get_path(rb_str_new2(feature)), Qnil); feature = RSTRING_PTR(fullpath); } if (ext && !strchr(ext, '/')) { @@ -774,6 +873,9 @@ Init_load() rb_alias_variable(rb_intern("$-I"), id_load_path); rb_alias_variable(rb_intern("$LOAD_PATH"), id_load_path); vm->load_path = rb_ary_new(); + vm->expanded_load_path = rb_ary_new(); + vm->load_path_snapshot = rb_ary_new(); + vm->load_path_check_cache = 0; rb_define_virtual_variable("$\"", get_loaded_features, 0); rb_define_virtual_variable("$LOADED_FEATURES", get_loaded_features, 0); diff --git a/ruby.c b/ruby.c index 3ddd96c7bb6a44..7ffc78ed727da7 100644 --- a/ruby.c +++ b/ruby.c @@ -1366,7 +1366,8 @@ process_options(int argc, char **argv, struct cmdline_options *opt) long i; VALUE load_path = GET_VM()->load_path; for (i = 0; i < RARRAY_LEN(load_path); ++i) { - rb_enc_associate(RARRAY_PTR(load_path)[i], lenc); + RARRAY_PTR(load_path)[i] = + rb_enc_associate(rb_str_dup(RARRAY_PTR(load_path)[i]), lenc); } } if (!(opt->disable & DISABLE_BIT(gems))) { diff --git a/test/ruby/test_require.rb b/test/ruby/test_require.rb index 58a9ee26b61bf5..ec7509644ec6de 100644 --- a/test/ruby/test_require.rb +++ b/test/ruby/test_require.rb @@ -356,4 +356,114 @@ def test_loaded_features_encoding $:.replace(loadpath) $".replace(features) end + + def test_require_changed_current_dir + bug7158 = '[ruby-core:47970]' + Dir.mktmpdir {|tmp| + Dir.chdir(tmp) { + Dir.mkdir("a") + Dir.mkdir("b") + open(File.join("a", "foo.rb"), "w") {} + open(File.join("b", "bar.rb"), "w") {|f| + f.puts "p :ok" + } + assert_in_out_err([], <<-INPUT, %w(:ok), [], bug7158) + $: << "." + Dir.chdir("a") + require "foo" + Dir.chdir("../b") + p :ng unless require "bar" + Dir.chdir("..") + p :ng if require "b/bar" + INPUT + } + } + end + + def test_require_not_modified_load_path + bug7158 = '[ruby-core:47970]' + Dir.mktmpdir {|tmp| + Dir.chdir(tmp) { + open("foo.rb", "w") {} + assert_in_out_err([], <<-INPUT, %w(:ok), [], bug7158) + a = Object.new + def a.to_str + "#{tmp}" + end + $: << a + require "foo" + last_path = $:.pop + p :ok if last_path == a && last_path.class == Object + INPUT + } + } + end + + def test_require_changed_home + bug7158 = '[ruby-core:47970]' + Dir.mktmpdir {|tmp| + Dir.chdir(tmp) { + open("foo.rb", "w") {} + Dir.mkdir("a") + open(File.join("a", "bar.rb"), "w") {} + assert_in_out_err([], <<-INPUT, %w(:ok), [], bug7158) + $: << '~' + ENV['HOME'] = "#{tmp}" + require "foo" + ENV['HOME'] = "#{tmp}/a" + p :ok if require "bar" + INPUT + } + } + end + + def test_require_to_path_redefined_in_load_path + bug7158 = '[ruby-core:47970]' + Dir.mktmpdir {|tmp| + Dir.chdir(tmp) { + open("foo.rb", "w") {} + assert_in_out_err(["RUBYOPT"=>nil], <<-INPUT, %w(:ok), [], bug7158) + a = Object.new + def a.to_path + "bar" + end + $: << a + begin + require "foo" + p :ng + rescue LoadError + end + def a.to_path + "#{tmp}" + end + p :ok if require "foo" + INPUT + } + } + end + + def test_require_to_str_redefined_in_load_path + bug7158 = '[ruby-core:47970]' + Dir.mktmpdir {|tmp| + Dir.chdir(tmp) { + open("foo.rb", "w") {} + assert_in_out_err(["RUBYOPT"=>nil], <<-INPUT, %w(:ok), [], bug7158) + a = Object.new + def a.to_str + "foo" + end + $: << a + begin + require "foo" + p :ng + rescue LoadError + end + def a.to_str + "#{tmp}" + end + p :ok if require "foo" + INPUT + } + } + end end diff --git a/vm.c b/vm.c index 4c44aa4a412ac7..8bbdfc7d3b913a 100644 --- a/vm.c +++ b/vm.c @@ -1591,7 +1591,12 @@ rb_vm_mark(void *ptr) RUBY_MARK_UNLESS_NULL(vm->thgroup_default); RUBY_MARK_UNLESS_NULL(vm->mark_object_ary); RUBY_MARK_UNLESS_NULL(vm->load_path); + RUBY_MARK_UNLESS_NULL(vm->load_path_snapshot); + RUBY_MARK_UNLESS_NULL(vm->load_path_check_cache); + RUBY_MARK_UNLESS_NULL(vm->expanded_load_path); RUBY_MARK_UNLESS_NULL(vm->loaded_features); + RUBY_MARK_UNLESS_NULL(vm->loaded_features_snapshot); + RUBY_MARK_UNLESS_NULL(vm->loaded_features_index); RUBY_MARK_UNLESS_NULL(vm->top_self); RUBY_MARK_UNLESS_NULL(vm->coverages); rb_gc_mark_locations(vm->special_exceptions, vm->special_exceptions + ruby_special_error_count); diff --git a/vm_core.h b/vm_core.h index dfc0e3c1ef3219..70c5f5cd007b59 100644 --- a/vm_core.h +++ b/vm_core.h @@ -299,7 +299,12 @@ typedef struct rb_vm_struct { /* load */ VALUE top_self; VALUE load_path; + VALUE load_path_snapshot; + VALUE load_path_check_cache; + VALUE expanded_load_path; VALUE loaded_features; + VALUE loaded_features_snapshot; + VALUE loaded_features_index; struct st_table *loading_table; /* signal */