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