wasi-common 0.9.0

WASI implementation in Rust
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
#![allow(non_camel_case_types)]
#![allow(unused)]
use super::fs_helpers::*;
use crate::ctx::WasiCtx;
use crate::fdentry::FdEntry;
use crate::host::{Dirent, FileType};
use crate::hostcalls_impl::{fd_filestat_set_times_impl, PathGet};
use crate::sys::fdentry_impl::{determine_type_rights, OsHandle};
use crate::sys::host_impl::{self, path_from_host};
use crate::sys::hostcalls_impl::fs_helpers::PathGetExt;
use crate::{wasi, Error, Result};
use log::{debug, trace};
use std::convert::TryInto;
use std::fs::{File, Metadata, OpenOptions};
use std::io::{self, Seek, SeekFrom};
use std::os::windows::fs::{FileExt, OpenOptionsExt};
use std::os::windows::prelude::{AsRawHandle, FromRawHandle};
use std::path::{Path, PathBuf};
use winx::file::{AccessMode, CreationDisposition, FileModeInformation, Flags};

fn read_at(mut file: &File, buf: &mut [u8], offset: u64) -> io::Result<usize> {
    // get current cursor position
    let cur_pos = file.seek(SeekFrom::Current(0))?;
    // perform a seek read by a specified offset
    let nread = file.seek_read(buf, offset)?;
    // rewind the cursor back to the original position
    file.seek(SeekFrom::Start(cur_pos))?;
    Ok(nread)
}

fn write_at(mut file: &File, buf: &[u8], offset: u64) -> io::Result<usize> {
    // get current cursor position
    let cur_pos = file.seek(SeekFrom::Current(0))?;
    // perform a seek write by a specified offset
    let nwritten = file.seek_write(buf, offset)?;
    // rewind the cursor back to the original position
    file.seek(SeekFrom::Start(cur_pos))?;
    Ok(nwritten)
}

// TODO refactor common code with unix
pub(crate) fn fd_pread(
    file: &File,
    buf: &mut [u8],
    offset: wasi::__wasi_filesize_t,
) -> Result<usize> {
    read_at(file, buf, offset).map_err(Into::into)
}

// TODO refactor common code with unix
pub(crate) fn fd_pwrite(file: &File, buf: &[u8], offset: wasi::__wasi_filesize_t) -> Result<usize> {
    write_at(file, buf, offset).map_err(Into::into)
}

pub(crate) fn fd_fdstat_get(fd: &File) -> Result<wasi::__wasi_fdflags_t> {
    let mut fdflags = 0;

    let handle = unsafe { fd.as_raw_handle() };

    let access_mode = winx::file::query_access_information(handle)?;
    let mode = winx::file::query_mode_information(handle)?;

    // Append without write implies append-only (__WASI_FDFLAGS_APPEND)
    if access_mode.contains(AccessMode::FILE_APPEND_DATA)
        && !access_mode.contains(AccessMode::FILE_WRITE_DATA)
    {
        fdflags |= wasi::__WASI_FDFLAGS_APPEND;
    }

    if mode.contains(FileModeInformation::FILE_WRITE_THROUGH) {
        // Only report __WASI_FDFLAGS_SYNC
        // This is technically the only one of the O_?SYNC flags Windows supports.
        fdflags |= wasi::__WASI_FDFLAGS_SYNC;
    }

    // Files do not support the `__WASI_FDFLAGS_NONBLOCK` flag

    Ok(fdflags)
}

pub(crate) fn fd_fdstat_set_flags(
    fd: &File,
    fdflags: wasi::__wasi_fdflags_t,
) -> Result<Option<OsHandle>> {
    let handle = unsafe { fd.as_raw_handle() };

    let access_mode = winx::file::query_access_information(handle)?;

    let new_access_mode = file_access_mode_from_fdflags(
        fdflags,
        access_mode.contains(AccessMode::FILE_READ_DATA),
        access_mode.contains(AccessMode::FILE_WRITE_DATA)
            | access_mode.contains(AccessMode::FILE_APPEND_DATA),
    );

    unsafe {
        Ok(Some(OsHandle::from(File::from_raw_handle(
            winx::file::reopen_file(handle, new_access_mode, file_flags_from_fdflags(fdflags))?,
        ))))
    }
}

pub(crate) fn fd_advise(
    _file: &File,
    advice: wasi::__wasi_advice_t,
    _offset: wasi::__wasi_filesize_t,
    _len: wasi::__wasi_filesize_t,
) -> Result<()> {
    match advice {
        wasi::__WASI_ADVICE_DONTNEED
        | wasi::__WASI_ADVICE_SEQUENTIAL
        | wasi::__WASI_ADVICE_WILLNEED
        | wasi::__WASI_ADVICE_NOREUSE
        | wasi::__WASI_ADVICE_RANDOM
        | wasi::__WASI_ADVICE_NORMAL => {}
        _ => return Err(Error::EINVAL),
    }

    Ok(())
}

pub(crate) fn path_create_directory(resolved: PathGet) -> Result<()> {
    let path = resolved.concatenate()?;
    std::fs::create_dir(&path).map_err(Into::into)
}

pub(crate) fn path_link(resolved_old: PathGet, resolved_new: PathGet) -> Result<()> {
    unimplemented!("path_link")
}

pub(crate) fn path_open(
    resolved: PathGet,
    read: bool,
    write: bool,
    oflags: wasi::__wasi_oflags_t,
    fdflags: wasi::__wasi_fdflags_t,
) -> Result<File> {
    use winx::file::{AccessMode, CreationDisposition, Flags};

    let is_trunc = oflags & wasi::__WASI_OFLAGS_TRUNC != 0;

    if is_trunc {
        // Windows does not support append mode when opening for truncation
        // This is because truncation requires `GENERIC_WRITE` access, which will override the removal
        // of the `FILE_WRITE_DATA` permission.
        if fdflags & wasi::__WASI_FDFLAGS_APPEND != 0 {
            return Err(Error::ENOTSUP);
        }
    }

    // convert open flags
    // note: the calls to `write(true)` are to bypass an internal OpenOption check
    // the write flag will ultimately be ignored when `access_mode` is calculated below.
    let mut opts = OpenOptions::new();
    match creation_disposition_from_oflags(oflags) {
        CreationDisposition::CREATE_ALWAYS => {
            opts.create(true).write(true);
        }
        CreationDisposition::CREATE_NEW => {
            opts.create_new(true).write(true);
        }
        CreationDisposition::TRUNCATE_EXISTING => {
            opts.truncate(true).write(true);
        }
        _ => {}
    }

    let path = resolved.concatenate()?;

    match path.symlink_metadata().map(|metadata| metadata.file_type()) {
        Ok(file_type) => {
            // check if we are trying to open a symlink
            if file_type.is_symlink() {
                return Err(Error::ELOOP);
            }
            // check if we are trying to open a file as a dir
            if file_type.is_file() && oflags & wasi::__WASI_OFLAGS_DIRECTORY != 0 {
                return Err(Error::ENOTDIR);
            }
        }
        Err(e) => match e.raw_os_error() {
            Some(e) => {
                use winx::winerror::WinError;
                log::debug!("path_open at symlink_metadata error code={:?}", e);
                let e = WinError::from_u32(e as u32);

                if e != WinError::ERROR_FILE_NOT_FOUND {
                    return Err(e.into());
                }
                // file not found, let it proceed to actually
                // trying to open it
            }
            None => {
                log::debug!("Inconvertible OS error: {}", e);
                return Err(Error::EIO);
            }
        },
    }

    let mut access_mode = file_access_mode_from_fdflags(fdflags, read, write);

    // Truncation requires the special `GENERIC_WRITE` bit set (this is why it doesn't work with append-only mode)
    if is_trunc {
        access_mode |= AccessMode::GENERIC_WRITE;
    }

    opts.access_mode(access_mode.bits())
        .custom_flags(file_flags_from_fdflags(fdflags).bits())
        .open(&path)
        .map_err(Into::into)
}

fn creation_disposition_from_oflags(oflags: wasi::__wasi_oflags_t) -> CreationDisposition {
    if oflags & wasi::__WASI_OFLAGS_CREAT != 0 {
        if oflags & wasi::__WASI_OFLAGS_EXCL != 0 {
            CreationDisposition::CREATE_NEW
        } else {
            CreationDisposition::CREATE_ALWAYS
        }
    } else if oflags & wasi::__WASI_OFLAGS_TRUNC != 0 {
        CreationDisposition::TRUNCATE_EXISTING
    } else {
        CreationDisposition::OPEN_EXISTING
    }
}

fn file_access_mode_from_fdflags(
    fdflags: wasi::__wasi_fdflags_t,
    read: bool,
    write: bool,
) -> AccessMode {
    let mut access_mode = AccessMode::READ_CONTROL;

    // Note that `GENERIC_READ` and `GENERIC_WRITE` cannot be used to properly support append-only mode
    // The file-specific flags `FILE_GENERIC_READ` and `FILE_GENERIC_WRITE` are used here instead
    // These flags have the same semantic meaning for file objects, but allow removal of specific permissions (see below)
    if read {
        access_mode.insert(AccessMode::FILE_GENERIC_READ);
    }

    if write {
        access_mode.insert(AccessMode::FILE_GENERIC_WRITE);
    }

    // For append, grant the handle FILE_APPEND_DATA access but *not* FILE_WRITE_DATA.
    // This makes the handle "append only".
    // Changes to the file pointer will be ignored (like POSIX's O_APPEND behavior).
    if fdflags & wasi::__WASI_FDFLAGS_APPEND != 0 {
        access_mode.insert(AccessMode::FILE_APPEND_DATA);
        access_mode.remove(AccessMode::FILE_WRITE_DATA);
    }

    access_mode
}

fn file_flags_from_fdflags(fdflags: wasi::__wasi_fdflags_t) -> Flags {
    // Enable backup semantics so directories can be opened as files
    let mut flags = Flags::FILE_FLAG_BACKUP_SEMANTICS;

    // Note: __WASI_FDFLAGS_NONBLOCK is purposely being ignored for files
    // While Windows does inherently support a non-blocking mode on files, the WASI API will
    // treat I/O operations on files as synchronous. WASI might have an async-io API in the future.

    // Technically, Windows only supports __WASI_FDFLAGS_SYNC, but treat all the flags as the same.
    if fdflags & wasi::__WASI_FDFLAGS_DSYNC != 0
        || fdflags & wasi::__WASI_FDFLAGS_RSYNC != 0
        || fdflags & wasi::__WASI_FDFLAGS_SYNC != 0
    {
        flags.insert(Flags::FILE_FLAG_WRITE_THROUGH);
    }

    flags
}

fn dirent_from_path<P: AsRef<Path>>(
    path: P,
    name: &str,
    cookie: wasi::__wasi_dircookie_t,
) -> Result<Dirent> {
    let path = path.as_ref();
    trace!("dirent_from_path: opening {}", path.to_string_lossy());

    // To open a directory on Windows, FILE_FLAG_BACKUP_SEMANTICS flag must be used
    let file = OpenOptions::new()
        .custom_flags(Flags::FILE_FLAG_BACKUP_SEMANTICS.bits())
        .read(true)
        .open(path)?;
    let ty = file.metadata()?.file_type();
    Ok(Dirent {
        ftype: host_impl::filetype_from_std(&ty),
        name: name.to_owned(),
        cookie,
        ino: host_impl::file_serial_no(&file)?,
    })
}

// On Windows there is apparently no support for seeking the directory stream in the OS.
// cf. https://github.com/WebAssembly/WASI/issues/61
//
// The implementation here may perform in O(n^2) if the host buffer is O(1)
// and the number of directory entries is O(n).
// TODO: Add a heuristic optimization to achieve O(n) time in the most common case
//      where fd_readdir is resumed where it previously finished
//
// Correctness of this approach relies upon one assumption: that the order of entries
// returned by `FindNextFileW` is stable, i.e. doesn't change if the directory
// contents stay the same. This invariant is crucial to be able to implement
// any kind of seeking whatsoever without having to read the whole directory at once
// and then return the data from cache. (which leaks memory)
//
// The MSDN documentation explicitly says that the order in which the search returns the files
// is not guaranteed, and is dependent on the file system.
// cf. https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-findnextfilew
//
// This stackoverflow post suggests that `FindNextFileW` is indeed stable and that
// the order of directory entries depends **only** on the filesystem used, but the
// MSDN documentation is not clear about this.
// cf. https://stackoverflow.com/questions/47380739/is-findfirstfile-and-findnextfile-order-random-even-for-dvd
//
// Implementation details:
// Cookies for the directory entries start from 1. (0 is reserved by wasi::__WASI_DIRCOOKIE_START)
// .        gets cookie = 1
// ..       gets cookie = 2
// other entries, in order they were returned by FindNextFileW get subsequent integers as their cookies
pub(crate) fn fd_readdir(
    fd: &File,
    cookie: wasi::__wasi_dircookie_t,
) -> Result<impl Iterator<Item = Result<Dirent>>> {
    use winx::file::get_file_path;

    let cookie = cookie.try_into()?;
    let path = get_file_path(fd)?;
    // std::fs::ReadDir doesn't return . and .., so we need to emulate it
    let path = Path::new(&path);
    // The directory /.. is the same as / on Unix (at least on ext4), so emulate this behavior too
    let parent = path.parent().unwrap_or(path);
    let dot = dirent_from_path(path, ".", 1)?;
    let dotdot = dirent_from_path(parent, "..", 2)?;

    trace!("    | fd_readdir impl: executing std::fs::ReadDir");
    let iter = path.read_dir()?.zip(3..).map(|(dir, no)| {
        let dir: std::fs::DirEntry = dir?;

        Ok(Dirent {
            name: path_from_host(dir.file_name())?,
            ftype: host_impl::filetype_from_std(&dir.file_type()?),
            ino: File::open(dir.path()).and_then(|f| host_impl::file_serial_no(&f))?,
            cookie: no,
        })
    });

    // into_iter for arrays is broken and returns references instead of values,
    // so we need to use vec![...] and do heap allocation
    // See https://github.com/rust-lang/rust/issues/25725
    let iter = vec![dot, dotdot].into_iter().map(Ok).chain(iter);

    // Emulate seekdir(). This may give O(n^2) complexity if used with a
    // small host_buf, but this is difficult to implement efficiently.
    //
    // See https://github.com/WebAssembly/WASI/issues/61 for more details.
    Ok(iter.skip(cookie))
}

pub(crate) fn path_readlink(resolved: PathGet, buf: &mut [u8]) -> Result<usize> {
    use winx::file::get_file_path;

    let path = resolved.concatenate()?;
    let target_path = path.read_link()?;

    // since on Windows we are effectively emulating 'at' syscalls
    // we need to strip the prefix from the absolute path
    // as otherwise we will error out since WASI is not capable
    // of dealing with absolute paths
    let dir_path = get_file_path(resolved.dirfd())?;
    let dir_path = PathBuf::from(strip_extended_prefix(dir_path));
    let target_path = target_path
        .strip_prefix(dir_path)
        .map_err(|_| Error::ENOTCAPABLE)
        .and_then(|path| path.to_str().map(String::from).ok_or(Error::EILSEQ))?;

    if buf.len() > 0 {
        let mut chars = target_path.chars();
        let mut nread = 0usize;

        for i in 0..buf.len() {
            match chars.next() {
                Some(ch) => {
                    buf[i] = ch as u8;
                    nread += 1;
                }
                None => break,
            }
        }

        Ok(nread)
    } else {
        Ok(0)
    }
}

fn strip_trailing_slashes_and_concatenate(resolved: &PathGet) -> Result<Option<PathBuf>> {
    if resolved.path().ends_with('/') {
        let suffix = resolved.path().trim_end_matches('/');
        concatenate(resolved.dirfd(), Path::new(suffix)).map(Some)
    } else {
        Ok(None)
    }
}

pub(crate) fn path_rename(resolved_old: PathGet, resolved_new: PathGet) -> Result<()> {
    use std::fs;

    let old_path = resolved_old.concatenate()?;
    let new_path = resolved_new.concatenate()?;

    // First sanity check: check we're not trying to rename dir to file or vice versa.
    // NB on Windows, the former is actually permitted [std::fs::rename].
    //
    // [std::fs::rename]: https://doc.rust-lang.org/std/fs/fn.rename.html
    if old_path.is_dir() && new_path.is_file() {
        return Err(Error::ENOTDIR);
    }
    // Second sanity check: check we're not trying to rename a file into a path
    // ending in a trailing slash.
    if old_path.is_file() && resolved_new.path().ends_with('/') {
        return Err(Error::ENOTDIR);
    }

    // TODO handle symlinks

    fs::rename(&old_path, &new_path).or_else(|e| match e.raw_os_error() {
        Some(e) => {
            use winx::winerror::WinError;

            log::debug!("path_rename at rename error code={:?}", e);
            match WinError::from_u32(e as u32) {
                WinError::ERROR_ACCESS_DENIED => {
                    // So most likely dealing with new_path == dir.
                    // Eliminate case old_path == file first.
                    if old_path.is_file() {
                        Err(Error::EISDIR)
                    } else {
                        // Ok, let's try removing an empty dir at new_path if it exists
                        // and is a nonempty dir.
                        fs::remove_dir(&new_path)
                            .and_then(|()| fs::rename(old_path, new_path))
                            .map_err(Into::into)
                    }
                }
                WinError::ERROR_INVALID_NAME => {
                    // If source contains trailing slashes, check if we are dealing with
                    // a file instead of a dir, and if so, throw ENOTDIR.
                    if let Some(path) = strip_trailing_slashes_and_concatenate(&resolved_old)? {
                        if path.is_file() {
                            return Err(Error::ENOTDIR);
                        }
                    }
                    Err(WinError::ERROR_INVALID_NAME.into())
                }
                e => Err(e.into()),
            }
        }
        None => {
            log::debug!("Inconvertible OS error: {}", e);
            Err(Error::EIO)
        }
    })
}

pub(crate) fn fd_filestat_get(file: &std::fs::File) -> Result<wasi::__wasi_filestat_t> {
    host_impl::filestat_from_win(file)
}

pub(crate) fn path_filestat_get(
    resolved: PathGet,
    dirflags: wasi::__wasi_lookupflags_t,
) -> Result<wasi::__wasi_filestat_t> {
    let path = resolved.concatenate()?;
    let file = File::open(path)?;
    host_impl::filestat_from_win(&file)
}

pub(crate) fn path_filestat_set_times(
    resolved: PathGet,
    dirflags: wasi::__wasi_lookupflags_t,
    st_atim: wasi::__wasi_timestamp_t,
    mut st_mtim: wasi::__wasi_timestamp_t,
    fst_flags: wasi::__wasi_fstflags_t,
) -> Result<()> {
    use winx::file::AccessMode;
    let path = resolved.concatenate()?;
    let file = OpenOptions::new()
        .access_mode(AccessMode::FILE_WRITE_ATTRIBUTES.bits())
        .open(path)?;
    fd_filestat_set_times_impl(&file, st_atim, st_mtim, fst_flags)
}

pub(crate) fn path_symlink(old_path: &str, resolved: PathGet) -> Result<()> {
    use std::os::windows::fs::{symlink_dir, symlink_file};
    use winx::winerror::WinError;

    let old_path = concatenate(resolved.dirfd(), Path::new(old_path))?;
    let new_path = resolved.concatenate()?;

    // try creating a file symlink
    symlink_file(&old_path, &new_path).or_else(|e| {
        match e.raw_os_error() {
            Some(e) => {
                log::debug!("path_symlink at symlink_file error code={:?}", e);
                match WinError::from_u32(e as u32) {
                    WinError::ERROR_NOT_A_REPARSE_POINT => {
                        // try creating a dir symlink instead
                        symlink_dir(old_path, new_path).map_err(Into::into)
                    }
                    WinError::ERROR_ACCESS_DENIED => {
                        // does the target exist?
                        if new_path.exists() {
                            Err(Error::EEXIST)
                        } else {
                            Err(WinError::ERROR_ACCESS_DENIED.into())
                        }
                    }
                    WinError::ERROR_INVALID_NAME => {
                        // does the target without trailing slashes exist?
                        if let Some(path) = strip_trailing_slashes_and_concatenate(&resolved)? {
                            if path.exists() {
                                return Err(Error::EEXIST);
                            }
                        }
                        Err(WinError::ERROR_INVALID_NAME.into())
                    }
                    e => Err(e.into()),
                }
            }
            None => {
                log::debug!("Inconvertible OS error: {}", e);
                Err(Error::EIO)
            }
        }
    })
}

pub(crate) fn path_unlink_file(resolved: PathGet) -> Result<()> {
    use std::fs;
    use winx::winerror::WinError;

    let path = resolved.concatenate()?;
    let file_type = path
        .symlink_metadata()
        .map(|metadata| metadata.file_type())?;

    // check if we're unlinking a symlink
    // NB this will get cleaned up a lot when [std::os::windows::fs::FileTypeExt]
    // stabilises
    //
    // [std::os::windows::fs::FileTypeExt]: https://doc.rust-lang.org/std/os/windows/fs/trait.FileTypeExt.html
    if file_type.is_symlink() {
        fs::remove_file(&path).or_else(|e| {
            match e.raw_os_error() {
                Some(e) => {
                    log::debug!("path_unlink_file at symlink_file error code={:?}", e);
                    match WinError::from_u32(e as u32) {
                        WinError::ERROR_ACCESS_DENIED => {
                            // try unlinking a dir symlink instead
                            fs::remove_dir(path).map_err(Into::into)
                        }
                        e => Err(e.into()),
                    }
                }
                None => {
                    log::debug!("Inconvertible OS error: {}", e);
                    Err(Error::EIO)
                }
            }
        })
    } else if file_type.is_dir() {
        Err(Error::EISDIR)
    } else if file_type.is_file() {
        fs::remove_file(path).map_err(Into::into)
    } else {
        Err(Error::EINVAL)
    }
}

pub(crate) fn path_remove_directory(resolved: PathGet) -> Result<()> {
    let path = resolved.concatenate()?;
    std::fs::remove_dir(&path).map_err(Into::into)
}