dirfdfs.ha (12751B)
1 // SPDX-License-Identifier: MPL-2.0 2 // (c) Hare authors <https://harelang.org> 3 4 use encoding::utf8; 5 use errors; 6 use fs; 7 use io; 8 use path; 9 use rt; 10 use strings; 11 use time; 12 use types::c; 13 14 // Controls how symlinks are followed (or not) in a dirfd filesystem. Support 15 // for this feature varies, you should gate usage of this enum behind a build 16 // tag. 17 // 18 // Note that on Linux, specifying BENEATH or IN_ROOT will also disable magic 19 // symlinks. 20 export type resolve_flag = enum u64 { 21 NORMAL = 0, 22 23 // Does not allow symlink resolution to occur for any symlinks which 24 // would refer to any anscestor of the fd directory. This disables all 25 // absolute symlinks, and any call to open or create with an absolute 26 // path. 27 BENEATH = rt::RESOLVE_BENEATH | rt::RESOLVE_NO_MAGICLINKS, 28 29 // Treat the directory fd as the root directory. This affects 30 // open/create for absolute paths, as well as absolute path resolution 31 // of symlinks. The effects are similar to chroot. 32 IN_ROOT = rt::RESOLVE_IN_ROOT | rt::RESOLVE_NO_MAGICLINKS, 33 34 // Disables symlink resolution entirely. 35 NO_SYMLINKS = rt::RESOLVE_NO_SYMLINKS, 36 37 // Disallows traversal of mountpoints during path resolution. This is 38 // not recommended for general use, as bind mounts are extensively used 39 // on many systems. 40 NO_XDEV = rt::RESOLVE_NO_XDEV, 41 }; 42 43 type os_filesystem = struct { 44 fs: fs::fs, 45 dirfd: int, 46 resolve: resolve_flag, 47 getdents_bufsz: size, 48 }; 49 50 // Opens a file descriptor as an [[fs::fs]]. This file descriptor must be a 51 // directory file. The file will be closed when the fs is closed. 52 export fn dirfdopen( 53 fd: io::file, 54 resolve_flags: resolve_flag = resolve_flag::NORMAL, 55 ) *fs::fs = { 56 let ofs = alloc(os_filesystem { resolve = resolve_flags, ... }); 57 let fs = static_dirfdopen(fd, ofs); 58 fs.close = &fs_close; 59 return fs; 60 }; 61 62 fn static_dirfdopen(fd: io::file, filesystem: *os_filesystem) *fs::fs = { 63 *filesystem = os_filesystem { 64 fs = fs::fs { 65 open = &fs_open, 66 openfile = &fs_open_file, 67 create = &fs_create, 68 createfile = &fs_create_file, 69 remove = &fs_remove, 70 rename = &fs_rename, 71 iter = &fs_iter, 72 stat = &fs_stat, 73 fstat = &fs_fstat, 74 readlink = &fs_readlink, 75 mkdir = &fs_mkdir, 76 rmdir = &fs_rmdir, 77 chmod = &fs_chmod, 78 fchmod = &fs_fchmod, 79 chown = &fs_chown, 80 fchown = &fs_fchown, 81 chtimes = &fs_chtimes, 82 fchtimes = &fs_fchtimes, 83 resolve = &fs_resolve, 84 link = &fs_link, 85 symlink = &fs_symlink, 86 ... 87 }, 88 dirfd = fd, 89 getdents_bufsz = 32768, // 32 KiB 90 ... 91 }; 92 return &filesystem.fs; 93 }; 94 95 // Clones a dirfd filesystem, optionally adding additional [[resolve_flag]] 96 // constraints. 97 export fn dirfs_clone( 98 fs: *fs::fs, 99 resolve_flags: resolve_flag = resolve_flag::NORMAL, 100 ) *fs::fs = { 101 assert(fs.open == &fs_open); 102 let fs = fs: *os_filesystem; 103 let new = alloc(*fs); 104 fs.resolve |= resolve_flags; 105 new.dirfd = rt::fcntl(new.dirfd, rt::F_DUPFD_CLOEXEC, 0) as int; 106 return &new.fs; 107 }; 108 109 // Sets the buffer size to use with the getdents(2) system call, for use with 110 // [[fs::iter]]. A larger buffer requires a larger runtime allocation, but can 111 // scan large directories faster. The default buffer size is 32 KiB. 112 // 113 // This function is not portable. 114 export fn dirfdfs_set_getdents_bufsz(fs: *fs::fs, sz: size) void = { 115 assert(fs.open == &fs_open); 116 let fs = fs: *os_filesystem; 117 fs.getdents_bufsz = sz; 118 }; 119 120 // Returns an [[io::file]] for this filesystem. This function is not portable. 121 export fn dirfile(fs: *fs::fs) io::file = { 122 assert(fs.open == &fs_open); 123 let fs = fs: *os_filesystem; 124 return fs.dirfd; 125 }; 126 127 fn errno_to_fs(err: rt::errno) fs::error = { 128 switch (err) { 129 case rt::ENOENT => 130 return errors::noentry; 131 case rt::EEXIST => 132 return errors::exists; 133 case rt::EACCES => 134 return errors::noaccess; 135 case rt::EBUSY => 136 return errors::busy; 137 case rt::ENOTDIR => 138 return fs::wrongtype; 139 case rt::EOPNOTSUPP, rt::ENOSYS => 140 return errors::unsupported; 141 case rt::EXDEV => 142 return fs::cannotrename; 143 case => 144 return errors::errno(err); 145 }; 146 }; 147 148 fn _fs_open( 149 fs: *fs::fs, 150 path: str, 151 oh: *rt::open_how, 152 ) (io::file | fs::error) = { 153 let fs = fs: *os_filesystem; 154 155 oh.resolve = fs.resolve; 156 157 let fd = match (rt::openat2(fs.dirfd, path, oh, size(rt::open_how))) { 158 case let err: rt::errno => 159 return errno_to_fs(err); 160 case let fd: int => 161 yield fd; 162 }; 163 164 return io::fdopen(fd); 165 }; 166 167 fn fs_open_file( 168 fs: *fs::fs, 169 path: str, 170 flags: fs::flag, 171 ) (io::file | fs::error) = { 172 flags ^= fs::flag::CTTY | fs::flag::NOCLOEXEC; // invert NOCTTY/CLOEXEC 173 174 if ((flags & fs::flag::DIRECTORY) == fs::flag::DIRECTORY) { 175 // This is arch-specific 176 flags &= ~fs::flag::DIRECTORY; 177 flags |= rt::O_DIRECTORY: fs::flag; 178 }; 179 180 let oh = rt::open_how { 181 flags = flags: u64, 182 ... 183 }; 184 return _fs_open(fs, path, &oh); 185 }; 186 187 fn fs_open( 188 fs: *fs::fs, 189 path: str, 190 flags: fs::flag, 191 ) (io::handle | fs::error) = fs_open_file(fs, path, flags)?; 192 193 fn fs_create_file( 194 fs: *fs::fs, 195 path: str, 196 mode: fs::mode, 197 flags: fs::flag, 198 ) (io::file | fs::error) = { 199 flags ^= fs::flag::CTTY | fs::flag::NOCLOEXEC; // invert NOCTTY/CLOEXEC 200 flags |= fs::flag::CREATE; 201 202 let oh = rt::open_how { 203 flags = flags: u64, 204 mode = mode: u64, 205 ... 206 }; 207 return _fs_open(fs, path, &oh)?; 208 }; 209 210 fn fs_create( 211 fs: *fs::fs, 212 path: str, 213 mode: fs::mode, 214 flags: fs::flag, 215 ) (io::handle | fs::error) = { 216 return fs_create_file(fs, path, mode, flags)?; 217 }; 218 219 fn fs_remove(fs: *fs::fs, path: str) (void | fs::error) = { 220 let fs = fs: *os_filesystem; 221 match (rt::unlinkat(fs.dirfd, path, 0)) { 222 case let err: rt::errno => 223 return errno_to_fs(err); 224 case void => void; 225 }; 226 }; 227 228 fn fs_rename(fs: *fs::fs, oldpath: str, newpath: str) (void | fs::error) = { 229 let fs = fs: *os_filesystem; 230 match (rt::renameat(fs.dirfd, oldpath, fs.dirfd, newpath, 0)) { 231 case let err: rt::errno => 232 return errno_to_fs(err); 233 case void => void; 234 }; 235 }; 236 237 fn fs_stat(fs: *fs::fs, path: str) (fs::filestat | fs::error) = { 238 let fs = fs: *os_filesystem; 239 let st = rt::st { ... }; 240 match (rt::fstatat(fs.dirfd, path, &st, rt::AT_SYMLINK_NOFOLLOW)) { 241 case let err: rt::errno => 242 return errno_to_fs(err); 243 case void => void; 244 }; 245 return st_to_filestat(&st); 246 }; 247 248 fn fs_fstat(fs: *fs::fs, fd: io::file) (fs::filestat | fs::error) = { 249 let fs = fs: *os_filesystem; 250 let st = rt::st { ... }; 251 match (rt::fstatat(fd, "", &st, rt::AT_EMPTY_PATH)) { 252 case let err: rt::errno => 253 return errno_to_fs(err); 254 case void => void; 255 }; 256 return st_to_filestat(&st); 257 }; 258 259 fn st_to_filestat(st: *rt::st) fs::filestat = { 260 return fs::filestat { 261 mask = fs::stat_mask::UID 262 | fs::stat_mask::GID 263 | fs::stat_mask::SIZE 264 | fs::stat_mask::INODE 265 | fs::stat_mask::ATIME 266 | fs::stat_mask::MTIME 267 | fs::stat_mask::CTIME, 268 mode = st.mode: fs::mode, 269 uid = st.uid, 270 gid = st.gid, 271 sz = st.sz, 272 inode = st.ino, 273 atime = time::instant { 274 sec = st.atime.tv_sec, 275 nsec = st.atime.tv_nsec, 276 }, 277 mtime = time::instant { 278 sec = st.mtime.tv_sec, 279 nsec = st.mtime.tv_nsec, 280 }, 281 ctime = time::instant { 282 sec = st.ctime.tv_sec, 283 nsec = st.ctime.tv_nsec, 284 }, 285 }; 286 }; 287 288 fn fs_readlink(fs: *fs::fs, path: str) (str | fs::error) = { 289 let fs = fs: *os_filesystem; 290 static let buf: [rt::PATH_MAX]u8 = [0...]; 291 let z = match (rt::readlinkat(fs.dirfd, path, buf[..])) { 292 case let err: rt::errno => 293 switch (err) { 294 case rt::EINVAL => 295 return fs::wrongtype; 296 case => 297 return errno_to_fs(err); 298 }; 299 case let z: size => 300 yield z; 301 }; 302 return strings::fromutf8(buf[..z])!; 303 }; 304 305 fn fs_rmdir(fs: *fs::fs, path: str) (void | fs::error) = { 306 let fs = fs: *os_filesystem; 307 match (rt::unlinkat(fs.dirfd, path, rt::AT_REMOVEDIR)) { 308 case let err: rt::errno => 309 return errno_to_fs(err); 310 case void => void; 311 }; 312 }; 313 314 fn fs_mkdir(fs: *fs::fs, path: str, mode: fs::mode) (void | fs::error) = { 315 let fs = fs: *os_filesystem; 316 match (rt::mkdirat(fs.dirfd, path, mode: uint)) { 317 case let err: rt::errno => 318 return errno_to_fs(err); 319 case void => void; 320 }; 321 }; 322 323 fn fs_chmod(fs: *fs::fs, path: str, mode: fs::mode) (void | fs::error) = { 324 let fs = fs: *os_filesystem; 325 match (rt::fchmodat(fs.dirfd, path, mode: uint, 0)) { 326 case let err: rt::errno => 327 return errno_to_fs(err); 328 case void => void; 329 }; 330 }; 331 332 fn fs_fchmod(fd: io::file, mode: fs::mode) (void | fs::error) = { 333 match (rt::fchmod(fd, mode: uint)) { 334 case let err: rt::errno => 335 return errno_to_fs(err); 336 case void => void; 337 }; 338 }; 339 340 fn fs_chown(fs: *fs::fs, path: str, uid: uint, gid: uint) (void | fs::error) = { 341 let fs = fs: *os_filesystem; 342 match (rt::fchownat(fs.dirfd, path, uid, gid, 0)) { 343 case let err: rt::errno => 344 return errno_to_fs(err); 345 case void => void; 346 }; 347 }; 348 349 fn fs_fchown(fd: io::file, uid: uint, gid: uint) (void | fs::error) = { 350 match (rt::fchown(fd, uid, gid)) { 351 case let err: rt::errno => 352 return errno_to_fs(err); 353 case void => void; 354 }; 355 }; 356 357 fn instant_to_timespec(time: (time::instant | void)) rt::timespec = { 358 match (time) { 359 case let t: time::instant => 360 return time::instant_to_timespec(t); 361 case void => 362 return rt::timespec{ 363 tv_sec = rt::UTIME_OMIT, 364 tv_nsec = rt::UTIME_OMIT 365 }; 366 }; 367 }; 368 369 fn fs_chtimes(fs: *fs::fs, path: str, atime: (time::instant | void), 370 mtime: (time::instant | void)) (void | fs::error) = { 371 let utimes: [2]rt::timespec = [ 372 instant_to_timespec(atime), 373 instant_to_timespec(mtime), 374 ]; 375 let fs = fs: *os_filesystem; 376 match (rt::utimensat(fs.dirfd, path, &utimes, 0)) { 377 case let err: rt::errno => 378 return errno_to_fs(err); 379 case void => void; 380 }; 381 }; 382 383 fn fs_fchtimes(fd: io::file, atime: (time::instant | void), 384 mtime: (time::instant | void)) (void | fs::error) = { 385 let utimes: [2]rt::timespec = [ 386 instant_to_timespec(atime), 387 instant_to_timespec(mtime), 388 ]; 389 match (rt::futimens(fd, &utimes)) { 390 case let err: rt::errno => 391 return errno_to_fs(err); 392 case void => void; 393 }; 394 }; 395 396 fn fs_resolve(fs: *fs::fs, path: str) str = { 397 if (path::abs(path)) { 398 return path; 399 }; 400 // XXX: This approach might not be right if this fs is based on a subdir 401 static let buf = path::buffer { ... }; 402 path::set(&buf, getcwd(), path)!; 403 return path::string(&buf); 404 }; 405 406 fn fs_link(fs: *fs::fs, old: str, new: str) (void | fs::error) = { 407 let fs = fs: *os_filesystem; 408 match (rt::linkat(fs.dirfd, old, fs.dirfd, new, 0)) { 409 case let err: rt::errno => 410 return errno_to_fs(err); 411 case void => void; 412 }; 413 }; 414 415 fn fs_symlink(fs: *fs::fs, target: str, path: str) (void | fs::error) = { 416 let fs = fs: *os_filesystem; 417 match (rt::symlinkat(target, fs.dirfd, path)) { 418 case let err: rt::errno => 419 return errno_to_fs(err); 420 case void => void; 421 }; 422 }; 423 424 fn fs_close(fs: *fs::fs) void = { 425 let fs = fs: *os_filesystem; 426 rt::close(fs.dirfd)!; 427 free(fs); 428 }; 429 430 // Based on musl's readdir 431 type os_iterator = struct { 432 iter: fs::iterator, 433 fd: int, 434 buf_pos: size, 435 buf_end: size, 436 buf: []u8, 437 }; 438 439 fn fs_iter(fs: *fs::fs, path: str) (*fs::iterator | fs::error) = { 440 let fs = fs: *os_filesystem; 441 let oh = rt::open_how { 442 flags = (rt::O_RDONLY | rt::O_CLOEXEC | rt::O_DIRECTORY): u64, 443 ... 444 }; 445 let fd: int = match (rt::openat2(fs.dirfd, path, 446 &oh, size(rt::open_how))) { 447 case let err: rt::errno => 448 return errno_to_fs(err); 449 case let fd: int => 450 yield fd; 451 }; 452 453 let buf = match (rt::malloc(fs.getdents_bufsz)) { 454 case let v: *opaque => 455 yield v: *[*]u8; 456 case null => 457 return errors::nomem; 458 }; 459 let iter = alloc(os_iterator { 460 iter = fs::iterator { 461 next = &iter_next, 462 finish = &iter_finish, 463 }, 464 fd = fd, 465 buf = buf[..fs.getdents_bufsz], 466 ... 467 }); 468 return &iter.iter; 469 }; 470 471 fn iter_next(iter: *fs::iterator) (fs::dirent | done | fs::error) = { 472 let iter = iter: *os_iterator; 473 if (iter.buf_pos >= iter.buf_end) { 474 let n = match (rt::getdents64(iter.fd, 475 iter.buf: *[*]u8, len(iter.buf))) { 476 case let err: rt::errno => 477 return errno_to_fs(err); 478 case let n: size => 479 yield n; 480 }; 481 if (n == 0) { 482 return done; 483 }; 484 iter.buf_end = n; 485 iter.buf_pos = 0; 486 }; 487 let de = &iter.buf[iter.buf_pos]: *rt::dirent64; 488 iter.buf_pos += de.d_reclen; 489 let name = c::tostr(&de.d_name: *const c::char)?; 490 if (name == "." || name == "..") { 491 return iter_next(iter); 492 }; 493 494 let ftype: fs::mode = switch (de.d_type) { 495 case rt::DT_UNKNOWN => 496 yield fs::mode::UNKNOWN; 497 case rt::DT_FIFO => 498 yield fs::mode::FIFO; 499 case rt::DT_CHR => 500 yield fs::mode::CHR; 501 case rt::DT_DIR => 502 yield fs::mode::DIR; 503 case rt::DT_BLK => 504 yield fs::mode::BLK; 505 case rt::DT_REG => 506 yield fs::mode::REG; 507 case rt::DT_LNK => 508 yield fs::mode::LINK; 509 case rt::DT_SOCK => 510 yield fs::mode::SOCK; 511 case => 512 yield fs::mode::UNKNOWN; 513 }; 514 return fs::dirent { 515 name = name, 516 ftype = ftype, 517 }; 518 }; 519 520 fn iter_finish(iter: *fs::iterator) void = { 521 let iter = iter: *os_iterator; 522 rt::close(iter.fd)!; 523 free(iter.buf); 524 free(iter); 525 };