faster_path 0.3.10 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/src/pathname.rs CHANGED
@@ -1,325 +1,405 @@
1
- use helpers::*;
2
- use basename;
3
- use chop_basename;
4
- use cleanpath_aggressive;
5
- use cleanpath_conservative;
6
- use dirname;
7
- use extname;
8
- use plus;
9
- use relative_path_from;
10
- use debug;
11
- use helpers::{TryFrom, to_str};
12
- use path_parsing::{SEP, find_last_non_sep_pos};
13
-
14
- use ruru;
15
- use ruru::{
16
- RString,
17
- Boolean,
18
- Array,
19
- AnyObject,
20
- NilClass,
21
- Object,
22
- Class,
23
- VerifiedObject,
24
- Exception as Exc,
25
- AnyException as Exception,
26
- };
27
- use ruru::types::{Value, ValueType};
28
1
  use std::borrow::Cow;
29
- use std::path::{MAIN_SEPARATOR, Path};
2
+ use std::ffi::OsStr;
30
3
  use std::fs;
4
+ use std::io;
5
+
6
+ use rutie::{AnyException, AnyObject, Array, Boolean, Class, Integer, NilClass, Object, RString, Symbol};
7
+
8
+ use crate::basename;
9
+ use crate::chop_basename::{self, is_relative};
10
+ use crate::cleanpath_aggressive;
11
+ use crate::cleanpath_conservative::{self, add_trailing_separator, del_trailing_separator, has_trailing_separator};
12
+ use crate::dirname;
13
+ use crate::extname;
14
+ use crate::path_parsing::Rules;
15
+ use crate::plus;
16
+ use crate::relative_path_from::{self, RelativePathError};
17
+ use crate::ruby::{
18
+ argument_error, check_max_arguments, error, inspect, new_pathname, path_like_to_string, pathname_class,
19
+ truthy_argument, EncodingId, EncodingOf, PathArgument, PathString, RubyResult,
20
+ };
31
21
 
32
- type MaybeArray = Result<ruru::Array, ruru::result::Error>;
33
- type MaybeString = Result<ruru::RString, ruru::result::Error>;
34
- type MaybeBoolean = Result<ruru::Boolean, ruru::result::Error>;
35
-
36
- pub struct Pathname {
37
- value: Value
38
- }
39
-
40
- impl Pathname {
41
- pub fn new(path: &str) -> Pathname {
42
- let mut instance = Class::from_existing("Pathname").allocate();
43
- instance.instance_variable_set("@path", RString::new(path).to_any_object());
22
+ // Ruby's path rules on this platform
23
+ const RULES: Rules = Rules::NATIVE;
44
24
 
45
- Pathname { value: instance.value() }
25
+ pub fn pn_add_trailing_separator(arguments: &[AnyObject]) -> RubyResult {
26
+ check_max_arguments(arguments, 1)?;
27
+ let argument = PathArgument::new(arguments, 0)?;
28
+ if argument.is_missing() {
29
+ return Err(implicit_conversion_error(arguments.first()));
46
30
  }
47
-
48
- pub fn to_any_object(&self) -> AnyObject {
49
- AnyObject::from(self.value())
31
+ let path = argument.path()?;
32
+ match (add_trailing_separator(RULES, Cow::Borrowed(path.bytes())), argument.string()) {
33
+ (Cow::Borrowed(_), Some(string)) => Ok(string.to_any_object()),
34
+ (result, _) => Ok(path.to_ruby(&result).into()),
50
35
  }
51
36
  }
52
37
 
53
- impl From<Value> for Pathname {
54
- fn from(value: Value) -> Self {
55
- Pathname { value }
56
- }
38
+ pub fn pn_is_absolute(arguments: &[AnyObject]) -> RubyResult {
39
+ check_max_arguments(arguments, 1)?;
40
+ let argument = PathArgument::new(arguments, 0)?;
41
+ let path = argument.path()?;
42
+ Ok(Boolean::new(!is_relative(RULES, path.bytes())).into())
57
43
  }
58
44
 
59
- impl TryFrom<AnyObject> for Pathname {
60
- type Error = debug::RubyDebugInfo;
61
- fn try_from(obj: AnyObject) -> Result<Pathname, Self::Error> {
62
- if Class::from_existing("String").case_equals(&obj) {
63
- Ok(Pathname::new(&RString::from(obj.value()).to_str()))
64
- } else if Class::from_existing("Pathname").case_equals(&obj) {
65
- Ok(Pathname::from(obj.value()))
66
- } else if obj.respond_to("to_path") {
67
- Ok(Pathname::new(&RString::from(obj.send("to_path", None).value()).to_str()))
68
- } else {
69
- Err(Self::Error::from(obj))
45
+ pub fn pn_basename(arguments: &[AnyObject]) -> RubyResult {
46
+ check_max_arguments(arguments, 2)?;
47
+ let argument = PathArgument::new(arguments, 0)?;
48
+ let path = argument.path()?;
49
+ let ext_argument = PathArgument::new(arguments, 1)?;
50
+ let ext = ext_argument.path()?;
51
+ let (component, base_len) = basename::last_component(RULES, path.bytes());
52
+ let result = match base_len {
53
+ // Like Ruby, ignore an extension in an incompatible encoding, and only
54
+ // remove one that starts a character.
55
+ Some(base_len) if !ext.bytes().is_empty() &&
56
+ (path.encoding == ext.encoding || EncodingOf::new(&path).merge(&ext).is_ok()) => {
57
+ let end = basename::ext_end(RULES, component, base_len, ext.bytes());
58
+ if end == component.len() || path.is_char_boundary(component, end) { &component[..end] } else { component }
70
59
  }
71
- }
60
+ _ => component,
61
+ };
62
+ Ok(path.to_ruby(result).into())
72
63
  }
73
64
 
74
- impl Object for Pathname {
75
- #[inline]
76
- fn value(&self) -> Value {
77
- self.value
78
- }
65
+ pub fn pn_children(arguments: &[AnyObject]) -> RubyResult {
66
+ check_max_arguments(arguments, 2)?;
67
+ let argument = PathArgument::new(arguments, 0)?;
68
+ let path = argument.path_or(b".")?;
69
+ let with_directory = truthy_argument(arguments, 1, true) && path.bytes() != b".";
70
+ let error_path = error_path(&argument, ".");
71
+ children(&path, with_directory, &error_path, |entry| Ok(entry.into()))
79
72
  }
80
73
 
81
- impl VerifiedObject for Pathname {
82
- fn is_correct_type<T: Object>(object: &T) -> bool {
83
- object.value().ty() == ValueType::Class &&
84
- Class::from_existing("Pathname").case_equals(object)
85
- }
74
+ pub fn pn_children_compat(arguments: &[AnyObject]) -> RubyResult {
75
+ check_max_arguments(arguments, 2)?;
76
+ let argument = PathArgument::new(arguments, 0)?;
77
+ let path = argument.path()?;
78
+ let with_directory = truthy_argument(arguments, 1, true) && path.bytes() != b".";
79
+ let error_path = error_path(&argument, "");
80
+ let pathname = pathname_class()?;
81
+ children(&path, with_directory, &error_path, |entry| new_pathname(&pathname, entry))
82
+ }
86
83
 
87
- fn error_message() -> &'static str {
88
- "Error converting to Pathname"
84
+ fn children<F>(path: &PathString, with_directory: bool, error_path: &AnyObject, mut wrap: F) -> RubyResult
85
+ where F: FnMut(RString) -> RubyResult {
86
+ let entries = read_dir(path, error_path)?;
87
+ let filesystem = EncodingId::filesystem();
88
+ let mut array = Array::with_capacity(entries.size_hint().0);
89
+ for entry in entries {
90
+ let entry = entry.map_err(|e| system_call_error(&e, error_path, "dir_read"))?;
91
+ let os_name = entry.file_name();
92
+ let (name, name_encoding) = file_name(&os_name, filesystem);
93
+ let string = if with_directory {
94
+ // `File.join(path, name)`, encoded like that, or as the file name if they don't mix.
95
+ let encoding = EncodingOf::new(path).merge_with(EncodingOf::of_bytes(&name, name_encoding)).
96
+ map_or(name_encoding, |encoding| encoding.encoding);
97
+ encoding.new_string(&RULES.join(path.bytes(), &name))
98
+ } else {
99
+ name_encoding.new_string(&name)
100
+ };
101
+ array.push(wrap(string)?);
89
102
  }
103
+ Ok(array.into())
90
104
  }
91
105
 
92
- pub fn pn_add_trailing_separator(pth: MaybeString) -> RString {
93
- let p = pth.unwrap();
94
- let x = format!("{}{}", p.to_str(), "a");
95
- match x.rsplit_terminator(MAIN_SEPARATOR).next() {
96
- Some("a") => p,
97
- _ => RString::new(format!("{}{}", p.to_str(), MAIN_SEPARATOR).as_str())
106
+ pub fn pn_chop_basename(arguments: &[AnyObject]) -> RubyResult {
107
+ check_max_arguments(arguments, 1)?;
108
+ let argument = PathArgument::new(arguments, 0)?;
109
+ let path = argument.path()?;
110
+ match chop_basename::chop_basename(RULES, path.bytes()) {
111
+ Some((dirname, basename)) => {
112
+ let mut array = Array::with_capacity(2);
113
+ array.push(path.to_ruby(dirname));
114
+ array.push(path.to_ruby(basename));
115
+ Ok(array.into())
116
+ },
117
+ None => Ok(NilClass::new().into()),
98
118
  }
99
119
  }
100
120
 
101
- pub fn pn_is_absolute(pth: MaybeString) -> Boolean {
102
- Boolean::new(to_str(&pth).as_bytes().get(0) == Some(&SEP))
121
+ pub fn pn_cleanpath_aggressive(arguments: &[AnyObject]) -> RubyResult {
122
+ check_max_arguments(arguments, 1)?;
123
+ let argument = PathArgument::new(arguments, 0)?;
124
+ let path = argument.path()?;
125
+ Ok(path.to_ruby(&cleanpath_aggressive::cleanpath_aggressive(RULES, path.bytes())).into())
103
126
  }
104
127
 
105
- // pub fn pn_ascend(){}
106
-
107
- pub fn pn_basename(pth: MaybeString, ext: MaybeString) -> RString {
108
- RString::new(basename::basename(to_str(&pth), to_str(&ext)))
128
+ pub fn pn_cleanpath_conservative(arguments: &[AnyObject]) -> RubyResult {
129
+ check_max_arguments(arguments, 1)?;
130
+ let argument = PathArgument::new(arguments, 0)?;
131
+ let path = argument.path()?;
132
+ Ok(path.to_ruby(&cleanpath_conservative::cleanpath_conservative(RULES, path.bytes())).into())
109
133
  }
110
134
 
111
- pub fn pn_children(pth: MaybeString, with_dir: MaybeBoolean) -> Result<AnyObject, Exception> {
112
- let path = pth.unwrap_or(RString::new("."));
113
- let path = path.to_str();
114
-
115
- if let Ok(entries) = fs::read_dir(path) {
116
- let mut with_directory = with_dir.unwrap_or(Boolean::new(true)).to_bool();
117
- if path == "." {
118
- with_directory = false;
119
- }
120
-
121
- let mut arr = Array::with_capacity(entries.size_hint().1.unwrap_or(0));
122
- for entry in entries {
123
- if with_directory {
124
- match entry {
125
- Ok(v) => { arr.push(RString::new(v.path().to_str().unwrap())); },
126
- _ => {}
127
- };
128
- } else {
129
- match entry {
130
- Ok(v) => { arr.push(RString::new(v.file_name().to_str().unwrap())); },
131
- _ => {}
132
- };
133
- }
134
- }
135
-
136
- Ok(arr.to_any_object())
137
- } else {
138
- let msg = format!("No such file or directory @ dir_initialize - {}", path);
139
- Err(Exception::new("Errno::NOENT", Some(&msg)))
135
+ pub fn pn_del_trailing_separator(arguments: &[AnyObject]) -> RubyResult {
136
+ check_max_arguments(arguments, 1)?;
137
+ let argument = PathArgument::new(arguments, 0)?;
138
+ let path = argument.path()?;
139
+ match (del_trailing_separator(RULES, path.bytes()), argument.string()) {
140
+ (Cow::Borrowed(result), Some(string)) if result.len() == path.bytes().len() => Ok(string.to_any_object()),
141
+ (result, _) => Ok(path.to_ruby(&result).into()),
140
142
  }
141
143
  }
142
144
 
143
- pub fn pn_children_compat(pth: MaybeString, with_dir: MaybeBoolean) -> Result<AnyObject, Exception> {
144
- let path = to_str(&pth);
145
-
146
- if let Ok(entries) = fs::read_dir(path) {
147
- let mut with_directory = with_dir.unwrap_or(Boolean::new(true)).to_bool();
148
- if path == "." {
149
- with_directory = false;
150
- }
145
+ pub fn pn_is_directory(arguments: &[AnyObject]) -> RubyResult {
146
+ check_max_arguments(arguments, 1)?;
147
+ let argument = PathArgument::new(arguments, 0)?;
148
+ let path = argument.path()?;
149
+ // Windows takes "dir\\..." for "dir"; Ruby doesn't.
150
+ let is_directory = !RULES.has_dots_name(path.bytes()) &&
151
+ path.os_path().map_or(false, |os_path| os_path.is_dir());
152
+ Ok(Boolean::new(is_directory).into())
153
+ }
151
154
 
152
- let mut arr = Array::with_capacity(entries.size_hint().1.unwrap_or(0));
153
- for entry in entries {
154
- if with_directory {
155
- if let Ok(v) = entry {
156
- arr.push(Pathname::new(v.path().to_str().unwrap()));
157
- };
158
- } else {
159
- if let Ok(v) = entry {
160
- arr.push(Pathname::new(v.file_name().to_str().unwrap()));
161
- };
162
- }
155
+ // `File.dirname(path, level = 1)`: `level` is the number of trailing
156
+ // components to remove (Ruby 3.1+).
157
+ pub fn pn_dirname(arguments: &[AnyObject]) -> RubyResult {
158
+ check_max_arguments(arguments, 2)?;
159
+ let argument = PathArgument::new(arguments, 0)?;
160
+ let path = argument.path()?;
161
+ let level = match arguments.get(1) {
162
+ Some(level) => level_argument(level)?,
163
+ None => 1,
164
+ };
165
+ let mut result: Cow<[u8]> = Cow::Borrowed(path.bytes());
166
+ for _ in 0..level {
167
+ let parent = dirname::dirname(RULES, &result);
168
+ if *parent == *result {
169
+ // The root ("/", "." or "C:/"), which is its own dirname
170
+ break;
163
171
  }
164
-
165
- Ok(arr.to_any_object())
166
- } else {
167
- let msg = format!("No such file or directory @ dir_initialize - {}", path);
168
- Err(Exception::new("Errno::NOENT", Some(&msg)))
172
+ result = Cow::Owned(parent.into_owned());
173
+ }
174
+ if result.is_empty() {
175
+ // `File.dirname("", 0)` is "." like `File.dirname("")`
176
+ result = Cow::Borrowed(b".");
169
177
  }
178
+ Ok(path.to_ruby(&result).into())
170
179
  }
171
180
 
172
- pub fn pn_chop_basename(pth: MaybeString) -> AnyObject {
173
- match chop_basename::chop_basename(to_str(&pth)) {
174
- Some((dirname, basename)) => {
175
- let mut arr = Array::with_capacity(2);
176
- arr.push(RString::new(&dirname));
177
- arr.push(RString::new(&basename));
178
- arr.to_any_object()
179
- },
180
- None => NilClass::new().to_any_object()
181
+ // The `level` argument of `File.dirname`, converted like Ruby's `NUM2INT`.
182
+ fn level_argument(object: &AnyObject) -> Result<usize, AnyException> {
183
+ let integer = match object.try_convert_to::<Integer>() {
184
+ Ok(integer) => integer,
185
+ Err(_) => {
186
+ let responds = object.protect_send("respond_to?", &[Symbol::new("to_int").into()])?;
187
+ if !responds.value().is_true() {
188
+ let class_name = object.protect_send("class", &[]).ok().and_then(|class| inspect(&class).ok()).unwrap_or_default();
189
+ return Err(error(&Class::type_error(), &format!("no implicit conversion of {} into Integer", class_name)));
190
+ }
191
+ let converted = object.protect_send("to_int", &[])?;
192
+ converted.try_convert_to::<Integer>()?
193
+ }
194
+ };
195
+ let level = integer.to_i64();
196
+ if level < 0 {
197
+ return Err(argument_error(&format!("negative level: {}", level)));
181
198
  }
199
+ Ok(level as usize)
182
200
  }
183
201
 
184
- // pub fn pn_cleanpath(pth: MaybeString){}
185
-
186
- pub fn pn_cleanpath_aggressive(pth: MaybeString) -> RString {
187
- RString::new(&cleanpath_aggressive::cleanpath_aggressive(to_str(&pth)))
202
+ // Returns an array of `String`s
203
+ pub fn pn_entries(arguments: &[AnyObject]) -> RubyResult {
204
+ check_max_arguments(arguments, 1)?;
205
+ let argument = PathArgument::new(arguments, 0)?;
206
+ let path = argument.path()?;
207
+ let error_path = error_path(&argument, "");
208
+ entries(&path, &error_path, |entry| Ok(entry.into()))
188
209
  }
189
210
 
190
- pub fn pn_cleanpath_conservative(pth: MaybeString) -> RString {
191
- RString::new(&cleanpath_conservative::cleanpath_conservative(to_str(&pth)))
211
+ // Returns an array of `Pathname`s
212
+ pub fn pn_entries_compat(arguments: &[AnyObject]) -> RubyResult {
213
+ check_max_arguments(arguments, 1)?;
214
+ let argument = PathArgument::new(arguments, 0)?;
215
+ let path = argument.path()?;
216
+ let error_path = error_path(&argument, "");
217
+ let pathname = pathname_class()?;
218
+ entries(&path, &error_path, |entry| new_pathname(&pathname, entry))
192
219
  }
193
220
 
194
- pub fn pn_del_trailing_separator(pth: MaybeString) -> RString {
195
- {
196
- let path = to_str(&pth);
197
- if path.is_empty() {
198
- return RString::new("/");
199
- }
200
- let pos = match find_last_non_sep_pos(path.as_bytes()) {
201
- Some(pos) => pos,
202
- None => return RString::new("/"),
203
- };
204
- if pos != path.len() - 1 {
205
- return RString::new(&path[..pos + 1]);
206
- }
221
+ fn entries<F>(path: &PathString, error_path: &AnyObject, mut wrap: F) -> RubyResult
222
+ where F: FnMut(RString) -> RubyResult {
223
+ let files = read_dir(path, error_path)?;
224
+ let filesystem = EncodingId::filesystem();
225
+ let mut array = Array::with_capacity(files.size_hint().0 + 2);
226
+ array.push(wrap(filesystem.new_string(b"."))?);
227
+ array.push(wrap(filesystem.new_string(b".."))?);
228
+ for file in files {
229
+ let file = file.map_err(|e| system_call_error(&e, error_path, "dir_read"))?;
230
+ let os_name = file.file_name();
231
+ let (name, encoding) = file_name(&os_name, filesystem);
232
+ array.push(wrap(encoding.new_string(&name))?);
207
233
  }
208
- pth.unwrap()
234
+ Ok(array.into())
209
235
  }
210
236
 
211
- // pub fn pn_descend(){}
212
-
213
- pub fn pn_is_directory(pth: MaybeString) -> Boolean {
214
- Boolean::new(Path::new(to_str(&pth)).is_dir())
237
+ pub fn pn_extname(arguments: &[AnyObject]) -> RubyResult {
238
+ check_max_arguments(arguments, 1)?;
239
+ let argument = PathArgument::new(arguments, 0)?;
240
+ let path = argument.path()?;
241
+ Ok(path.to_ruby(extname::extname(RULES, path.bytes())).into())
215
242
  }
216
243
 
217
- pub fn pn_dirname(pth: MaybeString) -> RString {
218
- RString::new(dirname::dirname(to_str(&pth)))
244
+ pub fn pn_has_trailing_separator(arguments: &[AnyObject]) -> RubyResult {
245
+ check_max_arguments(arguments, 1)?;
246
+ let argument = PathArgument::new(arguments, 0)?;
247
+ let path = argument.path()?;
248
+ Ok(Boolean::new(has_trailing_separator(RULES, path.bytes())).into())
219
249
  }
220
250
 
221
- // pub fn pn_each_child(){}
222
-
223
- // pub fn pn_each_filename(pth: MaybeString) -> NilClass {
224
- // NilClass::new()
225
- // }
226
-
227
- pub fn pn_entries(pth: MaybeString) -> Result<AnyObject, Exception> {
228
- let path = to_str(&pth);
229
- if let Ok(files) = fs::read_dir(path) {
230
- let mut arr = Array::with_capacity(files.size_hint().1.unwrap_or(0) + 2);
231
-
232
- arr.push(RString::new("."));
233
- arr.push(RString::new(".."));
251
+ pub fn pn_join(arguments: &[AnyObject]) -> RubyResult {
252
+ if arguments.is_empty() {
253
+ return Err(argument_error("wrong number of arguments (given 0, expected 1+)"));
254
+ }
255
+ let pathname = pathname_class()?;
256
+
257
+ // Copy every path out of Ruby before calling more Ruby code: the strings
258
+ // returned by `to_path` or `to_s` aren't referenced from anywhere the
259
+ // garbage collector looks once we only hold their bytes.
260
+ let mut paths: Vec<Vec<u8>> = Vec::with_capacity(arguments.len());
261
+ let mut encoding: Option<EncodingOf> = None;
262
+ for argument in arguments {
263
+ let string = path_like_to_string(argument, &pathname)?;
264
+ let path = PathString::new(&string)?;
265
+ encoding = Some(match encoding {
266
+ None => EncodingOf::new(&path),
267
+ Some(previous) => previous.merge(&path)?,
268
+ });
269
+ paths.push(path.bytes().to_vec());
270
+ }
234
271
 
235
- for file in files {
236
- arr.push(RString::new(file.unwrap().file_name().to_str().unwrap()));
272
+ let mut parts = paths.iter().rev();
273
+ let mut result: Cow<[u8]> = match parts.next() {
274
+ Some(last) => Cow::Borrowed(last),
275
+ None => Cow::Borrowed(b""),
276
+ };
277
+ for part in parts {
278
+ if !is_relative(RULES, &result) {
279
+ break;
237
280
  }
238
-
239
- Ok(arr.to_any_object())
240
- } else {
241
- let msg = format!("No such file or directory @ dir_initialize - {}", path);
242
- Err(Exception::new("Errno::NOENT", Some(&msg)))
281
+ result = Cow::Owned(plus::plus_paths(RULES, part, &result).into_owned());
243
282
  }
283
+ let encoding = match encoding {
284
+ Some(encoding) => encoding.encoding,
285
+ None => EncodingId::us_ascii(),
286
+ };
287
+ new_pathname(&pathname, encoding.new_string(&result))
244
288
  }
245
289
 
246
- pub fn pn_entries_compat(pth: MaybeString) -> Result<AnyObject, Exception> {
247
- let path = to_str(&pth);
248
- if let Ok(files) = fs::read_dir(path) {
249
- let mut arr = Array::with_capacity(files.size_hint().1.unwrap_or(0) + 2);
290
+ pub fn pn_plus(arguments: &[AnyObject]) -> RubyResult {
291
+ check_max_arguments(arguments, 2)?;
292
+ let argument1 = PathArgument::new(arguments, 0)?;
293
+ let path1 = argument1.path()?;
294
+ let argument2 = PathArgument::new(arguments, 1)?;
295
+ let path2 = argument2.path()?;
296
+ let encoding = EncodingOf::new(&path1).merge(&path2)?.encoding;
297
+ Ok(encoding.new_string(&plus::plus_paths(RULES, path1.bytes(), path2.bytes())).into())
298
+ }
250
299
 
251
- arr.push(Pathname::new("."));
252
- arr.push(Pathname::new(".."));
300
+ pub fn pn_is_relative(arguments: &[AnyObject]) -> RubyResult {
301
+ check_max_arguments(arguments, 1)?;
302
+ let argument = PathArgument::new(arguments, 0)?;
303
+ if argument.is_missing() {
304
+ return Ok(Boolean::new(false).into());
305
+ }
306
+ let path = argument.path()?;
307
+ Ok(Boolean::new(is_relative(RULES, path.bytes())).into())
308
+ }
253
309
 
254
- for file in files {
255
- arr.push(Pathname::new(file.unwrap().file_name().to_str().unwrap()));
310
+ pub fn pn_relative_path_from(arguments: &[AnyObject]) -> RubyResult {
311
+ check_max_arguments(arguments, 2)?;
312
+ let pathname = pathname_class()?;
313
+ let argument = PathArgument::new(arguments, 0)?;
314
+ let dest = argument.path()?;
315
+ let base = match arguments.get(1) {
316
+ Some(base_directory) => {
317
+ let base_string = path_like_to_string(base_directory, &pathname)?;
318
+ // Copied right away, see `pn_join`.
319
+ PathString::new(&base_string)?.into_owned()
256
320
  }
257
-
258
- Ok(arr.to_any_object())
259
- } else {
260
- let msg = format!("No such file or directory @ dir_initialize - {}", path);
261
- Err(Exception::new("Errno::NOENT", Some(&msg)))
321
+ None => PathString::empty(),
322
+ };
323
+ let encoding = EncodingOf::new(&dest).merge(&base)?.encoding;
324
+ let inspect = |bytes: &[u8]| crate::ruby::inspect(&encoding.new_string(bytes));
325
+
326
+ match relative_path_from::relative_path_from(RULES, dest.bytes(), base.bytes()) {
327
+ Ok(path) => new_pathname(&pathname, encoding.new_string(&path)),
328
+ Err(RelativePathError::DifferentPrefix(dest_prefix, base_directory)) => {
329
+ let message = format!("different prefix: {} and {}", inspect(&dest_prefix)?, inspect(&base_directory)?);
330
+ Err(argument_error(&message))
331
+ },
332
+ Err(RelativePathError::BaseDirectoryHasDotDot(base_directory)) => {
333
+ Err(argument_error(&format!("base_directory has ..: {}", inspect(&base_directory)?)))
334
+ },
262
335
  }
263
336
  }
264
337
 
265
- pub fn pn_extname(pth: MaybeString) -> RString {
266
- RString::new(extname::extname(to_str(&pth)))
338
+ // The path to name in a `SystemCallError`
339
+ fn error_path(argument: &PathArgument, default: &str) -> AnyObject {
340
+ match argument.string() {
341
+ Some(string) => string.to_any_object(),
342
+ None => RString::new_utf8(default).into(),
343
+ }
267
344
  }
268
345
 
269
- // pub fn pn_find(pth: MaybeString, ignore_error: Boolean){}
270
-
271
- pub fn pn_has_trailing_separator(pth: MaybeString) -> Boolean {
272
- let v = to_str(&pth);
273
- match chop_basename::chop_basename(v) {
274
- Some((a,b)) => Boolean::new(a.len() + b.len() < v.len()),
275
- _ => Boolean::new(false)
276
- }
346
+ fn read_dir(path: &PathString, error_path: &AnyObject) -> Result<fs::ReadDir, AnyException> {
347
+ let os_path = path.os_path().map_err(|e| system_call_error(&e, error_path, "dir_initialize"))?;
348
+ fs::read_dir(&*os_path).map_err(|e| system_call_error(&e, error_path, "dir_initialize"))
277
349
  }
278
350
 
279
- pub fn pn_join(args: MaybeArray) -> AnyObject {
280
- let paths = args.unwrap().into_iter().map(|arg| anyobject_to_string(arg).unwrap()).collect::<Vec<_>>();
281
- let mut paths_iter = paths.iter().rev();
282
- let mut result = Cow::Borrowed(paths_iter.next().unwrap().as_str());
283
- for part in paths_iter {
284
- result = plus::plus_paths(&part, result.as_ref());
285
- if result.as_bytes().first() == Some(&SEP) {
286
- break;
351
+ // `SystemCallError.new(path, errno, func)` gives the same `Errno::*` error
352
+ // and message as Ruby's `Dir` methods, e.g.
353
+ // "No such file or directory @ dir_initialize - path".
354
+ fn system_call_error(err: &io::Error, path: &AnyObject, func: &str) -> AnyException {
355
+ #[cfg(unix)]
356
+ {
357
+ if let Some(errno) = err.raw_os_error() {
358
+ let arguments = [path.clone(), Integer::new(errno.into()).into(), RString::new_utf8(func).into()];
359
+ if let Ok(exception) = Class::system_call_error().protect_send("new", &arguments) {
360
+ if let Ok(exception) = exception.try_convert_to::<AnyException>() {
361
+ return exception;
362
+ }
363
+ }
287
364
  }
288
365
  }
289
- Pathname::new(&result).to_any_object()
366
+ #[cfg(not(unix))]
367
+ let _ = func;
368
+ let path = crate::ruby::inspect(path).unwrap_or_default();
369
+ AnyException::from_io_error(err, &path)
290
370
  }
291
371
 
292
- // pub fn pn_mkpath(pth: MaybeString) -> NilClass {
293
- // NilClass::new()
294
- // }
295
-
296
- // pub fn pn_is_mountpoint(pth: MaybeString){}
297
-
298
- // pub fn pn_parent(pth: MaybeString){}
299
-
300
- pub fn pn_plus(pth1: MaybeString, pth2: MaybeString) -> RString {
301
- RString::new(&plus::plus_paths(to_str(&pth1), to_str(&pth2)))
372
+ // A file name from the OS as Ruby's `Dir` methods return it, in the
373
+ // filesystem encoding: its bytes, and their encoding.
374
+ #[cfg(unix)]
375
+ fn file_name(name: &OsStr, filesystem: EncodingId) -> (Cow<'_, [u8]>, EncodingId) {
376
+ use std::os::unix::ffi::OsStrExt;
377
+ (Cow::Borrowed(name.as_bytes()), filesystem)
302
378
  }
303
379
 
304
- // pub fn pn_prepend_prefix(prefix: MaybeString, relpath: MaybeString){}
305
-
306
- pub fn pn_is_relative(pth: MaybeString) -> Boolean {
307
- let path = match &pth {
308
- &Ok(ref ruru_string) => ruru_string.to_str(),
309
- &Err(_) => return Boolean::new(false),
380
+ #[cfg(not(unix))]
381
+ fn file_name(name: &OsStr, filesystem: EncodingId) -> (Cow<'_, [u8]>, EncodingId) {
382
+ let utf8 = match name.to_string_lossy() {
383
+ Cow::Borrowed(name) => Cow::Borrowed(name.as_bytes()),
384
+ Cow::Owned(name) => Cow::Owned(name.into_bytes()),
310
385
  };
311
- Boolean::new(path.as_bytes().get(0) != Some(&SEP))
386
+ if utf8.is_ascii() || filesystem == EncodingId::utf8() {
387
+ return (utf8, filesystem);
388
+ }
389
+ match EncodingId::utf8().transcode(&utf8, filesystem) {
390
+ Some(encoded) => (Cow::Owned(encoded), filesystem),
391
+ None => (utf8, EncodingId::utf8()),
392
+ }
312
393
  }
313
394
 
314
- // pub fn pn_root(pth: MaybeString){}
315
-
316
- // pub fn pn_split_names(pth: MaybeString){}
317
-
318
- pub fn pn_relative_path_from(itself: MaybeString, base_directory: MaybeString) -> Result<Pathname, Exception> {
319
- relative_path_from::relative_path_from(itself, base_directory)
395
+ fn implicit_conversion_error(argument: Option<&AnyObject>) -> AnyException {
396
+ let name = match argument {
397
+ None => "nil".to_string(),
398
+ Some(argument) if argument.is_nil() => "nil".to_string(),
399
+ Some(argument) if argument.value().is_true() => "true".to_string(),
400
+ Some(argument) if argument.value().is_false() => "false".to_string(),
401
+ Some(argument) => argument.protect_send("class", &[]).ok().
402
+ and_then(|class| crate::ruby::inspect(&class).ok()).unwrap_or_default(),
403
+ };
404
+ error(&Class::type_error(), &format!("no implicit conversion of {} into String", name))
320
405
  }
321
-
322
- // pub fn pn_rmtree(pth: MaybeString) -> NilClass {
323
- // NilClass::new()
324
- // }
325
- //