Ez az alfejezet a C++20 std::filesystem szerződését a
POSIX/Linux szemantikához viszonyítja. A laborprofil: Linux, x86-64, GCC
11+ vagy Clang 14+, -std=c++20, libstdc++ vagy libc++. A
<filesystem> már C++17-ben bekerült a standardba, de
a C++20-as szöveg több részletet pontosít (például a
u8string() char8_t-re váltását), ezért a
példák C++20-at használnak. A kimenet szándékosan
std::cout-tal történik; a <print> és a
<format> részleteit a charconv,
format és C++23 print (32.2) tárgyalja.
A fejezet központi állítása: a std::filesystem
kényelmes, hordozható névfeloldást és hibakód-leképezést ad, de nem
emeli be az operációs rendszer atomitási, tartóssági és biztonsági
szerződését. Aki ezt összekeveri, az a fájlbejáró laborban látványos, de
nehezen reprodukálható hibákat kap.
A std::filesystem::path értékszemantikájú típus, amely
egy útvonalat nem karakterláncként, hanem komponensek sorozataként
tárol. A value_type a platform natív karaktertípusa
(POSIX-on char, Windowson wchar_t), a
string_type pedig a hozzá tartozó
std::basic_string. A native() a natív, az
operációs rendszer API-ja által várt alakot adja vissza, a
c_str() ennek null-terminált változatát. A
generic_string() és a C++20-as
generic_u8string() ugyanezt / elválasztóval,
illetve std::u8string-ként adja vissza; ez utóbbi C++17-ben
még std::string volt, tehát a két standardverzió között itt
inkompatibilitás van.
A path natív kódolása implementáció által meghatározott. POSIX-on a
fájlrendszeri név egyszerűen bájtok sorozata, a / és a null
bájt kivételével; a kernel nem dekódolja UTF-8-ként, és nem normalizál.
A std::filesystem sem végez Unicode-normalizálást
(NFC/NFD), nem ellenőrzi az UTF-8 érvényességét, és nem alakít át
kódolást. Ha a programod UTF-8-at vár, az konvenció, nem a standard
garanciája. A u8string() csak annyit tesz, hogy a natív
reprezentációt char8_t-es sztringgé alakítja; ha a natív
bájtok nem UTF-8-at kódolnak, az eredmény nem lesz érvényes UTF-8.
Linuxon a UTF-8 gyakori konvenció, de nem nyelvi garancia.
A lexikális műveletek (filename(), stem(),
extension(), parent_path(),
lexically_normal(), lexically_relative())
tisztán szövegesek, nem érik el a fájlrendszert. A
lexically_normal() a . és ..
komponenseket szövegesen oldja fel: "/tmp/link/../x"
eredménye "/tmp/x", még akkor is, ha a link
symlink, és a valóságos feloldás más lenne. A canonical()
ezzel szemben feloldja a symlinkeket, abszolút utat ad, és megköveteli,
hogy a cél létezzen; a weakly_canonical() a létező prefixet
oldja fel, a maradékot lexikálisan kezeli. Az
equivalent(p1, p2) nem lexikális: a
status()-ok alapján dönti el, hogy két név ugyanarra az
objektumra mutat-e, tehát symlinket követ, és versenyérzékeny.
A file_status két dolgot hordoz: a
type()-ot és a permissions()-t. A
file_type enumeráció értékei: none,
not_found, regular, directory,
symlink, block, character,
fifo, socket, unknown. A
status(p) követi a symlinket (a POSIX stat
mintájára), a symlink_status(p) nem (a lstat
mintájára). Ha a symlink célja nem létezik, a status(p)
file_type::not_found-ot ad; ez nem feltétlenül hiba, az
error_code üres maradhat. Az exists(status) a
not_found-ra hamis.
A klasszikus hiba a „check, then act” minta:
namespace fs = std::filesystem;
fs::path p = "/var/lib/app/config.toml";
if (fs::exists(p)) { // 1. lépés: stat/lstat
std::ifstream in{p}; // 2. lépés: open
// ...
}A két lépés nem atomi. A kettő között egy másik folyamat törölheti,
kicserélheti vagy symlinkre cserélheti a fájlt; a program így más
objektumot nyit meg, mint amit ellenőrzött. Ez a TOCTOU (time-of-check
to time-of-use) rés. A std::filesystem nem kínál atomi
„check and open” műveletet. POSIX-on a megoldás az open()
O_NOFOLLOW (és O_CLOEXEC) jelzővel, majd
fstat() a megnyitott file descriptoron; így a névfeloldás
egyszer történik, és az ellenőrzés ugyanarra az objektumra vonatkozik,
amit később olvasol. A std::ifstream nem ad hordozhatóan
hozzáférést a mögöttes fd-hez, ezért ehhez POSIX réteg kell.
Hasonló csapda a create_directory(): visszatérési értéke
true, ha létrehozta, és false, ha a könyvtár
már létezett. A false nem hiba. Aki a visszatérési értéket
elhagyja, azt hiheti, hogy ő hozta létre a könyvtárat, holott egy másik
folyamat vagy egy korábbi futás tette meg.
A directory_iterator(dir) a dir közvetlen
bejegyzéseit adja; a sorrend nem specifikált. A
directory_entry egy bejegyzést reprezentál, és a
status(), symlink_status(),
file_size(), is_regular_file() tagfüggvényei
error_code& overloaddal is elérhetők. A C++20-as szöveg
szerint a directory_entry cache-elheti az attribútumokat; a
részletek implementációfüggők, ezért a hordozható minta az, hogy a
szükséges attribútumot explicit módon, error_code-dal kéred
le, és nem feltételezed, hogy a cache friss. A refresh() a
cache-elt értéket frissíti.
A recursive_directory_iterator rekurzívan járja be a
fát. Alapértelmezésben nem követi a symlinkeket; a
follow_directory_symlink opció kapcsolja be a követést.
Alapból kivételt dob a jogosultsági hibára; a
skip_permission_denied opció kihagyja az ilyen
bejegyzéseket. A depth() az aktuális mélységet adja, a
disable_recursion_pending() pedig az aktuális bejegyzés
alatti rekurziót tiltja le.
A symlink-policy a legfontosabb döntés. Ha bekapcsolod a
follow_directory_symlink-et, egy a -> b,
b -> a symlink-pár végtelen rekurziót okozhat; a
standard nem detektál ciklust. A védelem vagy egy mélységkorlát, vagy a
canonical() szerinti halmaz, amelyben a már látott célokat
tárolod. Utóbbi minden híváskor feloldja a symlinkeket, hibát ad, ha a
cél nem létezik, és maga is versenyérzékeny; ezért a bejárás közbeni
használata drága. A legegyszerűbb és legbiztonságosabb alapértelmezés a
symlinkek nem követése, és a követés csak külön kapcsolóval,
mélységkorláttal.
Az iteráció közbeni módosítás veszélyes: a mögöttes könyvtár módosítása nem specifikált viselkedést ad. Ne törölj és ne hozz létre bejegyzéseket a bejárás alatt; ha kell, gyűjtsd össze a neveket, és utána módosíts.
A legtöbb std::filesystem műveletnek két változata van:
(path) és (path, error_code&). A
(path) változat
std::filesystem::filesystem_error-t dob, amely a
std::system_error leszármazottja, és a
path1(), path2(), code() tagokkal
rendelkezik. Az (path, error_code&) változat az
operációsrendszer-szintű hibát ec-ben
jelzi, siker esetén pedig törli az ec-t. Ez azonban nem
jelent noexcept-ot: a szabvány szerint ([C++20]
[fs.err.report]) a tárfoglalási hibát ezek az overloadok is
std::bad_alloc kivétellel jelzik, és csak egy részük
deklarált noexcept (a konkrét overload szignatúráját kell
megnézni). A saját kódunk — például a push_back egy
gyűjtővektorba — szintén dobhat. Ezért ha a program kontrollált hibás
kilépést ígér, a CLI szélén kell egy kivételhatár.
A választás indoklása:
error_code a helyes: a hiba nem
kivételes, és a ciklusban a dobás drága.error_code-ot minden ágon ellenőrizni kell. A
filesystem függvények többsége nem
[[nodiscard]], ezért a figyelmetlen kód csendben elnyeli a
hibát. A hibakód kategóriája rendszerint
std::system_category(), de erre a standard nem ad feltétlen
garanciát minden műveletre.A std::filesystem nem ad tranzakciót, nem ad
fsync-et, és nem garantálja a rename
atomitását. A POSIX rename(2) ugyanazon fájlrendszeren
belül atomi, de ez a platform szemantikája, nem a C++ standard ígérete.
A copy_file nem atomi; a remove_all nem
tranzakcionális, részleges törlés lehetséges. A tartós íráshoz
fsync() kell a fájlra és a könyvtárra; ehhez POSIX
open/fsync szükséges, mert a
std::filesystem erre nem ad API-t. A
current_path() a folyamat globális aktuális könyvtárát
kezeli, tehát több szálon versenyhelyzet forrása lehet.
Ez DevOps szempontból a legfontosabb: a kényelmes API nem
helyettesíti az operációs rendszer atomitási és tartóssági szerződését.
A Fájlrendszer-műveletek, mmap és tartósság
(12.2) részben tárgyalt rename + fsync
minta itt is érvényes; a std::filesystem csak a
névfeloldást és a hibakódokat adja hozzá.
A következő program összegyűjti a reguláris fájlokat, összegzi a
méretüket, nem követi a symlinkeket, és a jogosultság miatt nem
megnyitható könyvtárakat a skip_permission_denied opcióval
kihagyja. Az OS-hibákat error_code-dal kezeli, az
allokációs és egyéb kivételeket a main kivételhatára fogja
meg. A skip_permission_denied csak a könyvtár megnyitásának
jogosultsági hibáját (EACCES) nyeli el; minden más
léptetési hiba (it.increment(ec)) után a bejárás nem
folytatható megbízhatóan, ezért a program megszakítja, az eredményt
részlegesnek jelöli, és 3-as kóddal lép ki — az ec törlése
nem helyreállítás.
// walk.cpp — C++20, Linux
#include <cstdint>
#include <filesystem>
#include <iostream>
#include <system_error>
#include <vector>
namespace fs = std::filesystem;
struct entry_info {
fs::path path;
std::uintmax_t size = 0;
};
static int run(int argc, char** argv) {
if (argc != 2) {
std::cerr << "hasznalat: walk KONYVTAR\n";
return 2;
}
fs::path root{argv[1]};
std::error_code ec;
fs::file_status root_status = fs::symlink_status(root, ec);
if (ec) {
std::cerr << "symlink_status: " << ec.message() << '\n';
return 1;
}
if (!fs::is_directory(root_status)) {
std::cerr << root << ": nem konyvtar\n";
return 1;
}
const auto opts = fs::directory_options::skip_permission_denied;
fs::recursive_directory_iterator it{root, opts, ec};
const fs::recursive_directory_iterator end{};
if (ec) {
std::cerr << "iterator: " << ec.message() << '\n';
return 1;
}
std::vector<entry_info> files;
std::uintmax_t total = 0;
bool partial = false;
while (it != end) {
const fs::directory_entry& de = *it;
std::error_code sec;
fs::file_status st = de.symlink_status(sec);
if (sec) {
std::cerr << "symlink_status(" << de.path() << "): "
<< sec.message() << " (kihagyva)\n";
it.increment(ec);
if (ec) { partial = true; std::cerr << "increment: " << ec.message() << '\n'; break; }
continue;
}
if (fs::is_symlink(st)) {
std::error_code tec;
fs::file_status tgt = de.status(tec);
if (tec) {
std::cerr << "status(" << de.path() << "): "
<< tec.message() << '\n';
} else if (tgt.type() == fs::file_type::not_found) {
std::cerr << "dangling symlink: " << de.path() << '\n';
}
} else if (fs::is_regular_file(st)) {
std::error_code fec;
std::uintmax_t sz = de.file_size(fec);
if (fec) {
std::cerr << "file_size(" << de.path() << "): "
<< fec.message() << '\n';
} else {
total += sz;
files.push_back({de.path(), sz});
}
}
it.increment(ec);
if (ec) {
// A hibás increment után az iterátor állapota nem folytatható
// megbízhatóan: a bejárást lezárjuk, és az eredményt részlegesnek jelöljük.
partial = true;
std::cerr << "increment: " << ec.message() << " (bejaras megszakadt)\n";
break;
}
}
for (const auto& f : files) {
std::cout << f.size << '\t' << f.path.generic_string() << '\n';
}
std::cout << (partial ? "# RESZLEGES: " : "# osszesen ") << files.size()
<< " fajl, " << total << " bajt\n";
return partial ? 3 : 0;
}
int main(int argc, char** argv) {
// Az error_code-os overloadok az OS-hibát jelzik ec-ben, de az allokációs
// hibát (std::bad_alloc) továbbra is kivétellel; a CLI szélén ezt kezeljük.
try {
return run(argc, argv);
} catch (const std::exception& e) {
std::cerr << "walk: " << e.what() << '\n';
return 1;
}
}Fordítás és futtatás a laborban:
g++ -std=c++20 -Wall -Wextra -Wpedantic -O2 walk.cpp -o walk
./walk /usr/include | tail -n 5GCC 8 és korábbi verziók külön -lstdc++fs linkelést
igényeltek; GCC 9+ és a korszerű libc++ nem. Ez verziófüggő részlet,
ezért a laborprofilban rögzíteni kell. Buildjegyzőkönyv
(lab-gcc profil). A program figyelmeztetés nélkül fordult.
Egy három fájlt és egy chmod 000 könyvtárat tartalmazó fán
a kimenet # osszesen 3 fajl, 12 bajt, 0-s kóddal (a
jogosultság nélküli könyvtárat csendben kihagyta). A léptetési hibát egy
tesztbuildben injektáltuk: a bejárás egy alkönyvtárat a belépés előtt
törölt. Erre a program
increment: No such file or directory (bejaras megszakadt)
üzenetet, # RESZLEGES: ... összesítést és 3-as kilépési
kódot adott. A javítás előtti, ec.clear()-rel „továbblépő”
változat ugyanerre # osszesen 0 fajl, 0 bajt kimenettel és
0-s kóddal, teljes eredményként állt le, mert a hibás léptetés után az
iterátor a végére ugrott.
| Hiba | Következmény | Javítás |
|---|---|---|
exists() után open() |
TOCTOU, más objektum nyílik meg | open(O_NOFOLLOW) + fstat |
create_directory visszatérési értékének elhagyása |
azt hiszed, te hoztad létre | bool created = ... és kezelés |
follow_directory_symlink ciklusdetektálás nélkül |
végtelen rekurzió | depth() korlát vagy
disable_recursion_pending() |
Cache-elt directory_entry státusz |
elavult információ | explicit status(ec) / refresh() |
p.string() UTF-8-ként kezelése |
hibás kódolás | native() / u8string() tudatos
használata |
remove_all tranzakcionálisnak feltételezése |
részleges törlés | idempotens újrafuttatás, naplózás |
ec ellenőrzésének elhagyása |
csendes hiba | minden ec ellenőrzése |
1. feladat (magyarázó és kódolási). Az alábbi kódrészlet TOCTOU-t tartalmaz. Nevezd meg a versenyt, és írj POSIX-szintű javítást, amely a megnyitott fd-en ellenőriz, és nem követi a symlinket.
fs::path p = "/var/lib/app/config.toml";
if (fs::exists(p)) {
std::ifstream in{p};
// ...
}2. feladat (kódolási). Írj du-szerű
függvényt, amely összegzi egy fa reguláris fájljainak méretét. Alapból
ne kövesse a symlinkeket, hagyja ki a jogosultsági hibákat, és
opcionális follow módban alkalmazzon mélységkorlátot.
Indokold, miért nem detektál ciklust a standard iterator.
3. feladat (diagnosztikai). Egy bejáró lassú egy hálózati fájlrendszeren. Sorolj fel három lehetséges okot, és mindegyikhez adj egy ellenőrzési módot. Ne állítsd, hogy mértél; fogalmazz hipotézisként.
1. megoldás. A verseny az exists() és
az ifstream megnyitása között van: a névfeloldás kétszer
történik, és a kettő között a fájl kicserélhető. A javítás:
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <cerrno>
int fd = ::open(p.c_str(), O_RDONLY | O_CLOEXEC | O_NOFOLLOW);
if (fd < 0) {
// ENOENT: nincs ilyen; ELOOP: symlink; EACCES: jogosultsag
return -1;
}
struct stat st{};
if (::fstat(fd, &st) != 0 || !S_ISREG(st.st_mode)) {
::close(fd);
return -1;
}
// innen a fd egy ellenorzott, regularis fajlra mutatAz indoklás: az open egyszer oldja fel a nevet, az
fstat ugyanarra az objektumra vonatkozik, amit az fd
azonosít. Az O_NOFOLLOW megakadályozza, hogy a végső
komponens symlink legyen; az O_CLOEXEC megakadályozza a fd
szivárgását exec esetén. Ellenőrzési feltétel:
strace-ben egy openat és egy
fstat látszik, nem stat + open
pár; symlink esetén ELOOP jön. Megjegyzés: a
std::ifstream nem ad hordozhatóan fd-t, ezért itt POSIX
réteg kell; C++23-ban sem standard a „no-follow” nyitás a
std::fstream-ben.
2. megoldás. A függvény váza:
std::uintmax_t dir_size(const fs::path& root, bool follow, int max_depth,
std::error_code& ec) {
std::uintmax_t total = 0;
auto opts = fs::directory_options::skip_permission_denied;
if (follow) opts |= fs::directory_options::follow_directory_symlink;
fs::recursive_directory_iterator it{root, opts, ec};
const fs::recursive_directory_iterator end{};
if (ec) return 0;
while (it != end) {
if (follow && it.depth() >= max_depth) {
it.disable_recursion_pending();
}
const fs::directory_entry& de = *it;
std::error_code sec;
auto st = de.symlink_status(sec);
if (!sec && fs::is_regular_file(st)) {
std::error_code fec;
auto sz = de.file_size(fec);
if (!fec) total += sz;
}
it.increment(ec);
if (ec) ec.clear();
}
return total;
}Az indoklás: follow_directory_symlink nélkül a
könyvtár-symlinkekbe nem lép be, tehát ciklus sem alakul ki.
follow módban a depth() korlát és a
disable_recursion_pending() a védelem; a standard nem
detektál ciklust, a canonical() szerinti halmaz
alternatíva, de drága és versenyérzékeny. Ellenőrzési feltétel: az
eredmény egyezik a
find DIR -type f -printf '%s\n' | awk '{s+=$1} END{print s}'
értékkel, és symlinkes fán nem fut végtelen ideig.
3. megoldás. Lehetséges okok: (a) a
directory_entry attribútum-lekérdezése külön
stat/lstat syscallt válthat, ha a cache nem
tartalmazza, és a hálózati fájlrendszeren a latencia nagy; ellenőrzés:
strace -f -c -e trace=newfstatat,stat,lstat,getdents64 ./walk DIR.
(b) A file_size újabb stat-ot válthat, tehát a
syscall-szám a bejegyzésszámmal nő; ellenőrzés: a syscall-szám
összevetése a bejegyzésszámmal. (c) A
skip_permission_denied nem csökkenti a syscallokat, csak a
kivételt; a jogosultsági hibák továbbra is syscallt igényelnek.
Ellenőrzés: a kivételes és a nem kivételes változat syscall-száma. Ezek
hipotézisek; a tanuló laborja méri őket.
A std::filesystem hordozható névfeloldást, bejárást és
hibakód-leképezést ad, de nem ad atomi „check and act” műveletet, nem ad
fsync-et, és nem garantálja a rename
atomitását. A path natív kódolása implementációfüggő; a
generic_string() csak az elválasztót cseréli, nem
konvertál. A status követi a symlinket, a
symlink_status nem; a kettő közötti különbség és a
TOCTOU-rés a biztonsági hibák fő forrása. A throwing és az
error_code overload közötti választás a hiba várt
gyakoriságán és a kontextuson múlik, nem pusztán a teljesítményen. A
bejárásnál a symlink-policy és a mélységkorlát az elsődleges védelmi
vonal.