wasi-common 0.21.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
use crate::handle::{Fdflags, Filestat, Fstflags, Handle, HandleRights, Oflags, Rights};
use crate::sched::Timestamp;
use crate::sys::osdir::OsDir;
use crate::sys::{fd, AsFile};
use crate::{Error, Result};
use std::convert::TryFrom;
use std::ffi::{OsStr, OsString};
use std::fs::{self, Metadata, OpenOptions};
use std::os::windows::ffi::{OsStrExt, OsStringExt};
use std::os::windows::fs::OpenOptionsExt;
use std::path::{Path, PathBuf};
use winapi::shared::winerror;
use winx::file::AccessMode;

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

fn strip_extended_prefix<P: AsRef<OsStr>>(path: P) -> OsString {
    let path: Vec<u16> = path.as_ref().encode_wide().collect();
    if &[92, 92, 63, 92] == &path[0..4] {
        OsString::from_wide(&path[4..])
    } else {
        OsString::from_wide(&path)
    }
}

fn concatenate<P: AsRef<Path>>(file: &OsDir, path: P) -> Result<PathBuf> {
    use winx::file::get_file_path;

    // WASI is not able to deal with absolute paths
    // so error out if absolute
    if path.as_ref().is_absolute() {
        return Err(Error::Notcapable);
    }

    let dir_path = get_file_path(&*file.as_file()?)?;
    // concatenate paths
    let mut out_path = PathBuf::from(dir_path);
    out_path.push(path.as_ref());
    // strip extended prefix; otherwise we will error out on any relative
    // components with `out_path`
    let out_path = PathBuf::from(strip_extended_prefix(out_path));

    tracing::debug!(out_path = tracing::field::debug(&out_path));

    Ok(out_path)
}

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

    // We always need `FILE_WRITE_ATTRIBUTES` so that we can set attributes such as filetimes, etc.
    access_mode.insert(AccessMode::FILE_WRITE_ATTRIBUTES);

    // 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.contains(&Fdflags::APPEND) {
        access_mode.insert(AccessMode::FILE_APPEND_DATA);
        access_mode.remove(AccessMode::FILE_WRITE_DATA);
    }

    access_mode
}

/// Creates owned WASI path from OS string.
///
/// NB WASI spec requires OS string to be valid UTF-8. Otherwise,
/// `__WASI_ERRNO_ILSEQ` error is returned.
pub(crate) fn from_host<S: AsRef<OsStr>>(s: S) -> Result<String> {
    let vec: Vec<u16> = s.as_ref().encode_wide().collect();
    let s = String::from_utf16(&vec)?;
    Ok(s)
}

pub(crate) fn open_rights(
    input_rights: &HandleRights,
    oflags: Oflags,
    fdflags: Fdflags,
) -> HandleRights {
    // which rights are needed on the dirfd?
    let mut needed_base = Rights::PATH_OPEN;
    let mut needed_inheriting = input_rights.base | input_rights.inheriting;

    // convert open flags
    if oflags.contains(&Oflags::CREAT) {
        needed_base |= Rights::PATH_CREATE_FILE;
    } else if oflags.contains(&Oflags::TRUNC) {
        needed_base |= Rights::PATH_FILESTAT_SET_SIZE;
    }

    // convert file descriptor flags
    if fdflags.contains(&Fdflags::DSYNC)
        || fdflags.contains(&Fdflags::RSYNC)
        || fdflags.contains(&Fdflags::SYNC)
    {
        needed_inheriting |= Rights::FD_DATASYNC;
        needed_inheriting |= Rights::FD_SYNC;
    }

    HandleRights::new(needed_base, needed_inheriting)
}

pub(crate) fn readlinkat(dirfd: &OsDir, s_path: &str) -> Result<String> {
    use winx::file::get_file_path;

    let path = concatenate(dirfd, Path::new(s_path))?;
    let err = match path.read_link() {
        Ok(target_path) => {
            // 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(&*dirfd.as_file()?)?;
            let dir_path = PathBuf::from(strip_extended_prefix(dir_path));
            let target_path = target_path
                .strip_prefix(dir_path)
                .map_err(|_| Error::Notcapable)?;
            let target_path = target_path.to_str().ok_or(Error::Ilseq)?;
            return Ok(target_path.to_owned());
        }
        Err(e) => e,
    };
    if let Some(code) = err.raw_os_error() {
        tracing::debug!("readlinkat error={:?}", code);
        if code as u32 == winerror::ERROR_INVALID_NAME {
            if s_path.ends_with('/') {
                // strip "/" and check if exists
                let path = concatenate(dirfd, Path::new(s_path.trim_end_matches('/')))?;
                if path.exists() && !path.is_dir() {
                    return Err(Error::Notdir);
                }
            }
        }
    }
    Err(err.into())
}

pub(crate) fn create_directory(file: &OsDir, path: &str) -> Result<()> {
    let path = concatenate(file, path)?;
    std::fs::create_dir(&path)?;
    Ok(())
}

pub(crate) fn link(
    old_dirfd: &OsDir,
    old_path: &str,
    new_dirfd: &OsDir,
    new_path: &str,
    follow_symlinks: bool,
) -> Result<()> {
    use std::fs;
    let mut old_path = concatenate(old_dirfd, old_path)?;
    let new_path = concatenate(new_dirfd, new_path)?;
    if follow_symlinks {
        // in particular, this will return an error if the target path doesn't exist
        tracing::debug!(
            old_path = tracing::field::display(old_path.display()),
            "Following symlinks"
        );
        old_path = fs::canonicalize(&old_path).map_err(|e| match e.raw_os_error() {
            // fs::canonicalize under Windows will return:
            // * ERROR_FILE_NOT_FOUND, if it encounters a dangling symlink
            // * ERROR_CANT_RESOLVE_FILENAME, if it encounters a symlink loop
            Some(code) if code as u32 == winerror::ERROR_CANT_RESOLVE_FILENAME => Error::Loop,
            _ => e.into(),
        })?;
    }
    let err = match fs::hard_link(&old_path, &new_path) {
        Ok(()) => return Ok(()),
        Err(e) => e,
    };
    if let Some(code) = err.raw_os_error() {
        tracing::debug!("path_link at fs::hard_link error code={:?}", code);
        if code as u32 == winerror::ERROR_ACCESS_DENIED {
            // If an attempt is made to create a hard link to a directory, POSIX-compliant
            // implementations of link return `EPERM`, but `ERROR_ACCESS_DENIED` is converted
            // to `EACCES`. We detect and correct this case here.
            if fs::metadata(&old_path).map(|m| m.is_dir()).unwrap_or(false) {
                return Err(Error::Perm);
            }
        }
    }
    Err(err.into())
}

pub(crate) fn open(
    dirfd: &OsDir,
    path: &str,
    read: bool,
    write: bool,
    oflags: Oflags,
    fdflags: Fdflags,
) -> Result<Box<dyn Handle>> {
    use winx::file::{AccessMode, CreationDisposition, Flags};

    let is_trunc = oflags.contains(&Oflags::TRUNC);

    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.contains(&Fdflags::APPEND) {
            return Err(Error::Notsup);
        }
    }

    // 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 oflags.into() {
        CreationDisposition::CREATE_ALWAYS => {
            opts.create(true).truncate(true).write(true);
        }
        CreationDisposition::CREATE_NEW => {
            opts.create_new(true).write(true);
        }
        CreationDisposition::TRUNCATE_EXISTING => {
            opts.truncate(true).write(true);
        }
        _ => {}
    }
    let path = concatenate(dirfd, path)?;
    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::Loop);
            }
            // check if we are trying to open a file as a dir
            if file_type.is_file() && oflags.contains(&Oflags::DIRECTORY) {
                return Err(Error::Notdir);
            }
        }
        Err(err) => match err.raw_os_error() {
            Some(code) => {
                tracing::debug!("path_open at symlink_metadata error code={:?}", code);
                match code as u32 {
                    winerror::ERROR_FILE_NOT_FOUND => {
                        // file not found, let it proceed to actually
                        // trying to open it
                    }
                    winerror::ERROR_INVALID_NAME => {
                        // TODO rethink this. For now, migrate how we handled
                        // it in `path::openat` on Windows.
                        return Err(Error::Notdir);
                    }
                    _ => return Err(err.into()),
                };
            }
            None => {
                tracing::debug!("Inconvertible OS error: {}", err);
                return Err(Error::Io);
            }
        },
    }

    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;
    }

    let flags: Flags = fdflags.into();
    let file = opts
        .access_mode(access_mode.bits())
        .custom_flags(flags.bits())
        .open(&path)?;
    let handle = <Box<dyn Handle>>::try_from(file)?;
    Ok(handle)
}

pub(crate) fn readlink(dirfd: &OsDir, path: &str, buf: &mut [u8]) -> Result<usize> {
    use winx::file::get_file_path;

    let path = concatenate(dirfd, path)?;
    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(&*dirfd.as_file()?)?;
    let dir_path = PathBuf::from(strip_extended_prefix(dir_path));
    let target_path = target_path
        .strip_prefix(dir_path)
        .map_err(|_| Error::Notcapable)
        .and_then(|path| path.to_str().map(String::from).ok_or(Error::Ilseq))?;

    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)
    }
}

pub(crate) fn rename(
    old_dirfd: &OsDir,
    old_path_: &str,
    new_dirfd: &OsDir,
    new_path_: &str,
) -> Result<()> {
    use std::fs;

    let old_path = concatenate(old_dirfd, old_path_)?;
    let new_path = concatenate(new_dirfd, new_path_)?;

    // 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::Notdir);
    }
    // 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() && new_path_.ends_with('/') {
        return Err(Error::Notdir);
    }

    // TODO handle symlinks
    let err = match fs::rename(&old_path, &new_path) {
        Ok(()) => return Ok(()),
        Err(e) => e,
    };
    match err.raw_os_error() {
        Some(code) => {
            tracing::debug!("path_rename at rename error code={:?}", code);
            match code 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() {
                        return Err(Error::Isdir);
                    } 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)?;
                        fs::rename(old_path, new_path)?;
                        return Ok(());
                    }
                }
                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(old_dirfd, old_path_)?
                    {
                        if path.is_file() {
                            return Err(Error::Notdir);
                        }
                    }
                }
                _ => {}
            }

            Err(err.into())
        }
        None => {
            tracing::debug!("Inconvertible OS error: {}", err);
            Err(Error::Io)
        }
    }
}

pub(crate) fn symlink(old_path: &str, new_dirfd: &OsDir, new_path_: &str) -> Result<()> {
    use std::os::windows::fs::{symlink_dir, symlink_file};

    let old_path = concatenate(new_dirfd, Path::new(old_path))?;
    let new_path = concatenate(new_dirfd, new_path_)?;

    // Windows distinguishes between file and directory symlinks.
    // If the source doesn't exist or is an exotic file type, we fall back
    // to regular file symlinks.
    let use_dir_symlink = fs::metadata(&new_path)
        .as_ref()
        .map(Metadata::is_dir)
        .unwrap_or(false);

    let res = if use_dir_symlink {
        symlink_dir(&old_path, &new_path)
    } else {
        symlink_file(&old_path, &new_path)
    };

    let err = match res {
        Ok(()) => return Ok(()),
        Err(e) => e,
    };
    match err.raw_os_error() {
        Some(code) => {
            tracing::debug!("path_symlink at symlink_file error code={:?}", code);
            match code as u32 {
                // If the target contains a trailing slash, the Windows API returns
                // ERROR_INVALID_NAME (which corresponds to ENOENT) instead of
                // ERROR_ALREADY_EXISTS (which corresponds to EEXIST)
                //
                // This concerns only trailing slashes (not backslashes) and
                // only symbolic links (not hard links).
                //
                // Since POSIX will return EEXIST in such case, we simulate this behavior
                winerror::ERROR_INVALID_NAME => {
                    if let Some(path) =
                        strip_trailing_slashes_and_concatenate(new_dirfd, new_path_)?
                    {
                        if path.exists() {
                            return Err(Error::Exist);
                        }
                    }
                }
                _ => {}
            }

            Err(err.into())
        }
        None => {
            tracing::debug!("Inconvertible OS error: {}", err);
            Err(Error::Io)
        }
    }
}

pub(crate) fn unlink_file(dirfd: &OsDir, path: &str) -> Result<()> {
    use std::fs;

    let path = concatenate(dirfd, path)?;
    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() {
        let err = match fs::remove_file(&path) {
            Ok(()) => return Ok(()),
            Err(e) => e,
        };
        match err.raw_os_error() {
            Some(code) => {
                tracing::debug!("path_unlink_file at symlink_file error code={:?}", code);
                if code as u32 == winerror::ERROR_ACCESS_DENIED {
                    // try unlinking a dir symlink instead
                    return fs::remove_dir(path).map_err(Into::into);
                }

                Err(err.into())
            }
            None => {
                tracing::debug!("Inconvertible OS error: {}", err);
                Err(Error::Io)
            }
        }
    } else if file_type.is_dir() {
        Err(Error::Isdir)
    } else if file_type.is_file() {
        fs::remove_file(path).map_err(Into::into)
    } else {
        Err(Error::Inval)
    }
}

pub(crate) fn remove_directory(dirfd: &OsDir, path: &str) -> Result<()> {
    let path = concatenate(dirfd, path)?;
    std::fs::remove_dir(&path).map_err(Into::into)
}

pub(crate) fn filestat_get_at(dirfd: &OsDir, path: &str, follow: bool) -> Result<Filestat> {
    use winx::file::Flags;
    let path = concatenate(dirfd, path)?;
    let mut opts = OpenOptions::new();

    if !follow {
        // By specifying FILE_FLAG_OPEN_REPARSE_POINT, we force Windows to *not* dereference symlinks.
        opts.custom_flags(Flags::FILE_FLAG_OPEN_REPARSE_POINT.bits());
    }

    let file = opts.read(true).open(path)?;
    let stat = fd::filestat_get(&file)?;
    Ok(stat)
}

pub(crate) fn filestat_set_times_at(
    dirfd: &OsDir,
    path: &str,
    atim: Timestamp,
    mtim: Timestamp,
    fst_flags: Fstflags,
    follow: bool,
) -> Result<()> {
    use winx::file::{AccessMode, Flags};
    let path = concatenate(dirfd, path)?;
    let mut opts = OpenOptions::new();

    if !follow {
        // By specifying FILE_FLAG_OPEN_REPARSE_POINT, we force Windows to *not* dereference symlinks.
        opts.custom_flags(Flags::FILE_FLAG_OPEN_REPARSE_POINT.bits());
    }

    let file = opts
        .access_mode(AccessMode::FILE_WRITE_ATTRIBUTES.bits())
        .open(path)?;
    fd::filestat_set_times(&file, atim, mtim, fst_flags)?;
    Ok(())
}