diff --git a/.gitignore b/.gitignore index b5ed3c89a..b4ac787d7 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,4 @@ __pycache__/ .typos-oxendict-base.json .typos-oxendict-base.toml *.swo +.hypothesis/ diff --git a/Cargo.toml b/Cargo.toml index 4c8185f79..5b61f5954 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -118,6 +118,7 @@ digest = "0.11" sha2 = "0.11" sha1 = { version = "0.11", optional = true } md5 = { package = "md-5", version = "0.11", optional = true } +rustix = { version = "1.0.8", features = ["fs"] } itoa = "1" itertools = "0.12" indexmap = { version = "2.5", features = ["serde"] } @@ -183,11 +184,6 @@ regex = "1.12.2" # the version `ortho_config` resolves, or the `FluentValue` types would differ. fluent-bundle = "0.16.0" -# Target-specific dev-deps -[target.'cfg(unix)'.dev-dependencies] -# Used only to construct FIFOs and device nodes in tests. -rustix = { version = "1.0.8", features = ["fs"] } - [workspace] members = ["test_support"] diff --git a/docs/security-network-command-audit.md b/docs/security-network-command-audit.md index a0b189283..6f931bc30 100644 --- a/docs/security-network-command-audit.md +++ b/docs/security-network-command-audit.md @@ -99,6 +99,30 @@ introduces, and concrete remediation tasks that would harden the helpers. budgets incrementally so long-running commands fail fast once the configured allowance is exceeded. +## File helper findings + +- [x] **File-reading filters read unbounded, untrusted entries.** *(Status: + remediated in the bounded file-read policy.)* The `contents`, `linecount`, + `hash`, and `digest` filters opened caller-supplied paths and read them to + EOF without a byte budget or a file-type check. A contributor who replaced a + trusted manifest's input path with a huge regular file, a symlink to + `/dev/zero`, or a FIFO could exhaust Netsuke's memory, consume unbounded CPU + and I/O, or block a build worker indefinitely. *Remediation tasks:* + - Enforce a configurable byte budget while streaming, not only from metadata + observed before the read. + - Open the final entry without following symlinks and verify the opened + object is a regular file. + - Count lines incrementally instead of loading the entire file. + - **Remediation:** the reading filters now share one policy. The final path + component is opened with `O_NOFOLLOW` (a pre-open symlink check on + Windows), the opened handle must be a regular file, and `contents`, + `linecount`, `hash`, and `digest` stream against a running byte total + anchored to `StdlibConfig::with_file_max_read_bytes` (default 8 MiB). + `linecount` counts terminators incrementally instead of materializing the + file. Per-call `max_bytes` may narrow the ceiling and a named + `follow_symlinks=true` opt-in permits link following; rejections surface + localized diagnostics naming the path and limit without file contents. + ## Next steps The remaining command-helper hardening tasks can be implemented incrementally. diff --git a/docs/stdlib-yaml-and-jinja-guide.md b/docs/stdlib-yaml-and-jinja-guide.md index 13848c887..663b33991 100644 --- a/docs/stdlib-yaml-and-jinja-guide.md +++ b/docs/stdlib-yaml-and-jinja-guide.md @@ -156,6 +156,22 @@ read. Relative paths are resolved from the workspace in which Netsuke runs. defaults to `8` and the algorithm defaults to `sha256`. Example: `{{ 'fixtures/message.txt' | digest(12, 'sha512') }}`. +All four filters share one safety policy: the final path component is opened +without following symlinks, the opened object must be a regular file, and each +read stops at a shared byte budget (8 MiB by default). A read that exceeds the +budget, or a path that names a symlink, FIFO, or device, fails with a localized +diagnostic quoting the path and the applicable limit. Two optional keyword +arguments narrow a call without touching the operator ceiling: + +- `max_bytes` lowers the budget for one call (a value above the configured + budget is clamped to it). Example: + `{{ 'fixtures/big.bin' | contents(max_bytes=1024) }}`. +- `follow_symlinks=true` permits the final component to be a symlink. Example: + `{{ 'link/version.txt' | contents(follow_symlinks=true) }}`. + +See the users' guide section on file reading limits for the defaults, the +symlink policy, and the trust model these limits assume. + MD5 and SHA-1 are available only in builds compiled with Cargo feature `legacy-digests`. Without that feature, `hash('md5')`, `hash('sha1')`, and their `digest` equivalents fail with a feature-specific diagnostic. New manifests diff --git a/docs/users-guide.md b/docs/users-guide.md index 76ff11276..712c2f8a3 100644 --- a/docs/users-guide.md +++ b/docs/users-guide.md @@ -1650,6 +1650,41 @@ Avoid placing secrets in URLs. Netsuke logs hosts and cache keys rather than complete URLs, but downloaded content and commands still run within the host trust boundary. +## Configure file reading limits + +The `contents`, `linecount`, `hash`, and `digest` template filters read files +through a shared byte budget so a checkout entry cannot exhaust Netsuke's +memory or CPU. The default budget is 8 MiB per read, matching the `fetch()` +response limit. Hosts embedding Netsuke can raise or lower it with +`StdlibConfig::with_file_max_read_bytes` before registering the standard +library. + +The reading filters also refuse to follow a symlink as the final path component +and reject anything that is not a regular file once opened, including FIFOs and +device nodes. A symlinked directory used *inside* a path is unaffected; only +the final entry is checked. Templates that deliberately read through a final +symlink can pass `follow_symlinks=true` to accept the link: + + + +```jinja +{{ 'generated/version.txt' | contents(follow_symlinks=true) }} +``` + +A single call may lower the budget with `max_bytes`, but never raise it above +the configured ceiling: + + + +```jinja +{{ 'fixtures/big.bin' | hash(max_bytes=1024) }} +``` + +Reads that exceed the budget fail with a diagnostic naming the path and the +limit, never the file contents. Raise the operator budget when legitimate +builds hash large artefacts; prefer per-call `max_bytes` narrowing when a +manifest merely wants to bound one input. + ## Interpret failures Netsuke reports failures at the earliest stage that can identify them: @@ -1688,6 +1723,10 @@ Netsuke reduces some common quoting mistakes, but it is not a sandbox: On Unix, scripts use `/bin/sh -e`. - `shell`, `grep`, `fetch`, filesystem helpers, and ordinary recipes interact with the host. +- The file-reading filters (`contents`, `linecount`, `hash`, `digest`) read at + most the configured byte budget, open the final path entry without following + symlinks, and require the opened object to be a regular file. See + [Configure file reading limits](#configure-file-reading-limits). - `glob` restricts its filesystem metadata access to a capability handle scoped to the pattern's literal directory prefix, so it cannot inspect anything outside the subtree the pattern can match; the pattern match walk diff --git a/locales/ar/messages.ftl b/locales/ar/messages.ftl index 9b6732f98..04a134b6d 100644 --- a/locales/ar/messages.ftl +++ b/locales/ar/messages.ftl @@ -210,6 +210,7 @@ network_policy.host.not_allowlisted = المضيف «{ $host }» ليس ضمن stdlib.config.default_fetch_cache_invalid = يجب أن يكون المسار الافتراضي لذاكرة fetch المخبّأة نسبيًا. stdlib.config.default_which_cache_invalid = يجب أن تكون السعة الافتراضية لذاكرة which المخبّأة موجبة. stdlib.config.workspace_root_absolute = يجب أن يكون مسار جذر مساحة العمل مطلقًا. +stdlib.config.file_read_limit_positive = يجب أن يكون حدّ قراءة الملفات موجبًا. stdlib.config.fetch_response_limit_positive = يجب أن يكون حدّ استجابة fetch موجبًا. stdlib.config.command_output_limit_positive = يجب أن يكون حدّ التقاط مخرجات الأوامر موجبًا. stdlib.config.command_stream_limit_positive = يجب أن يكون حدّ تدفّق الأوامر موجبًا. @@ -318,6 +319,8 @@ stdlib.path.with_suffix.empty_separator = يتطلّب with_suffix فاصلًا stdlib.path.relative_to.mismatch = المسار { $path } ليس نسبيًا إلى { $root }. stdlib.path.expanduser.unsupported = توسيع ~ لمستخدم بعينه غير مدعوم. stdlib.path.expanduser.no_home = تعذّر توسيع ~: لم يُضبط أي متغيّر بيئة لدليل المنزل. +stdlib.path.contents.file_too_large = تجاوز الملف '{ $path }' حدّ القراءة البالغ { $limit } بايت. +stdlib.path.contents.not_regular_file = الملف '{ $path }' ليس ملفًا عاديًا. stdlib.path.contents.unsupported_encoding = ترميز غير مدعوم: «{ $encoding }». stdlib.path.hash.unsupported_algorithm = خوارزمية تلبيد غير مدعومة: «{ $algorithm }». stdlib.path.hash.unsupported_algorithm_legacy = خوارزمية تلبيد غير مدعومة: «{ $algorithm }» (فعّل الميزة «{ $feature }»). diff --git a/locales/cs/messages.ftl b/locales/cs/messages.ftl index 9c96221fc..0c5956c78 100644 --- a/locales/cs/messages.ftl +++ b/locales/cs/messages.ftl @@ -210,6 +210,7 @@ network_policy.host.not_allowlisted = Hostitel „{ $host }“ není na seznamu stdlib.config.default_fetch_cache_invalid = Výchozí cesta mezipaměti fetch musí být relativní. stdlib.config.default_which_cache_invalid = Výchozí kapacita mezipaměti which musí být kladná. stdlib.config.workspace_root_absolute = Kořenová cesta pracovního prostoru musí být absolutní. +stdlib.config.file_read_limit_positive = File read limit must be positive. stdlib.config.fetch_response_limit_positive = Limit odpovědi fetch musí být kladný. stdlib.config.command_output_limit_positive = Limit zachyceného výstupu příkazů musí být kladný. stdlib.config.command_stream_limit_positive = Limit proudu příkazů musí být kladný. @@ -318,6 +319,8 @@ stdlib.path.with_suffix.empty_separator = with_suffix vyžaduje neprázdný odd stdlib.path.relative_to.mismatch = { $path } není relativní vůči { $root }. stdlib.path.expanduser.unsupported = Rozvoj znaku ~ pro konkrétního uživatele není podporován. stdlib.path.expanduser.no_home = Znak ~ nelze rozvinout: není nastavena žádná proměnná prostředí domovského adresáře. +stdlib.path.contents.file_too_large = File '{ $path }' exceeded the read limit of { $limit } bytes. +stdlib.path.contents.not_regular_file = File '{ $path }' is not a regular file. stdlib.path.contents.unsupported_encoding = Nepodporované kódování „{ $encoding }“. stdlib.path.hash.unsupported_algorithm = Nepodporovaný hashovací algoritmus „{ $algorithm }“. stdlib.path.hash.unsupported_algorithm_legacy = Nepodporovaný hashovací algoritmus „{ $algorithm }“ (zapněte funkci „{ $feature }“). diff --git a/locales/cy/messages.ftl b/locales/cy/messages.ftl index 00ea1586d..b0e1421a5 100644 --- a/locales/cy/messages.ftl +++ b/locales/cy/messages.ftl @@ -210,6 +210,7 @@ network_policy.host.not_allowlisted = Nid yw'r gwesteiwr ‘{ $host }’ ar y rh stdlib.config.default_fetch_cache_invalid = Rhaid i lwybr rhagosodedig storfa fetch fod yn gymharol. stdlib.config.default_which_cache_invalid = Rhaid i gynhwysedd rhagosodedig storfa which fod yn bositif. stdlib.config.workspace_root_absolute = Rhaid i lwybr gwraidd y gweithle fod yn absoliwt. +stdlib.config.file_read_limit_positive = File read limit must be positive. stdlib.config.fetch_response_limit_positive = Rhaid i derfyn ymateb fetch fod yn bositif. stdlib.config.command_output_limit_positive = Rhaid i derfyn dal allbwn gorchmynion fod yn bositif. stdlib.config.command_stream_limit_positive = Rhaid i derfyn ffrwd y gorchmynion fod yn bositif. @@ -318,6 +319,8 @@ stdlib.path.with_suffix.empty_separator = Mae with_suffix angen gwahanydd nad yw stdlib.path.relative_to.mismatch = Nid yw { $path } yn gymharol i { $root }. stdlib.path.expanduser.unsupported = Ni chefnogir ehangu ~ ar gyfer defnyddiwr penodol. stdlib.path.expanduser.no_home = Ni ellir ehangu ~: nid oes newidyn amgylchedd cyfeiriadur cartref wedi'i osod. +stdlib.path.contents.file_too_large = File '{ $path }' exceeded the read limit of { $limit } bytes. +stdlib.path.contents.not_regular_file = File '{ $path }' is not a regular file. stdlib.path.contents.unsupported_encoding = Amgodiad nas cefnogir: ‘{ $encoding }’. stdlib.path.hash.unsupported_algorithm = Algorithm stwnsio nas cefnogir: ‘{ $algorithm }’. stdlib.path.hash.unsupported_algorithm_legacy = Algorithm stwnsio nas cefnogir: ‘{ $algorithm }’ (galluogwch y nodwedd ‘{ $feature }’). diff --git a/locales/da/messages.ftl b/locales/da/messages.ftl index 9098c4e23..1000becc2 100644 --- a/locales/da/messages.ftl +++ b/locales/da/messages.ftl @@ -210,6 +210,7 @@ network_policy.host.not_allowlisted = Værten "{ $host }" står ikke på listen stdlib.config.default_fetch_cache_invalid = Standardstien til fetch-mellemlageret skal være relativ. stdlib.config.default_which_cache_invalid = Standardkapaciteten for which-mellemlageret skal være positiv. stdlib.config.workspace_root_absolute = Rodstien for arbejdsområdet skal være absolut. +stdlib.config.file_read_limit_positive = File read limit must be positive. stdlib.config.fetch_response_limit_positive = Svargrænsen for fetch skal være positiv. stdlib.config.command_output_limit_positive = Grænsen for opsamlet kommandooutput skal være positiv. stdlib.config.command_stream_limit_positive = Strømgrænsen for kommandoer skal være positiv. @@ -318,6 +319,8 @@ stdlib.path.with_suffix.empty_separator = with_suffix kræver en adskiller, der stdlib.path.relative_to.mismatch = { $path } er ikke relativ til { $root }. stdlib.path.expanduser.unsupported = Brugerspecifik udvidelse af ~ understøttes ikke. stdlib.path.expanduser.no_home = ~ kan ikke udvides: der er ingen miljøvariabler for hjemmemappen. +stdlib.path.contents.file_too_large = File '{ $path }' exceeded the read limit of { $limit } bytes. +stdlib.path.contents.not_regular_file = File '{ $path }' is not a regular file. stdlib.path.contents.unsupported_encoding = Tegnkodningen "{ $encoding }" understøttes ikke. stdlib.path.hash.unsupported_algorithm = Hash-algoritmen "{ $algorithm }" understøttes ikke. stdlib.path.hash.unsupported_algorithm_legacy = Hash-algoritmen "{ $algorithm }" understøttes ikke (slå funktionen "{ $feature }" til). diff --git a/locales/de/messages.ftl b/locales/de/messages.ftl index b749b8ff7..4b296c96b 100644 --- a/locales/de/messages.ftl +++ b/locales/de/messages.ftl @@ -210,6 +210,7 @@ network_policy.host.not_allowlisted = Der Host „{ $host }“ steht nicht auf d stdlib.config.default_fetch_cache_invalid = Der voreingestellte Pfad des fetch-Caches muss relativ sein. stdlib.config.default_which_cache_invalid = Die voreingestellte Kapazität des which-Caches muss positiv sein. stdlib.config.workspace_root_absolute = Der Wurzelpfad des Arbeitsbereichs muss absolut sein. +stdlib.config.file_read_limit_positive = File read limit must be positive. stdlib.config.fetch_response_limit_positive = Das Antwortlimit von fetch muss positiv sein. stdlib.config.command_output_limit_positive = Das Limit für erfasste Befehlsausgaben muss positiv sein. stdlib.config.command_stream_limit_positive = Das Stream-Limit für Befehle muss positiv sein. @@ -318,6 +319,8 @@ stdlib.path.with_suffix.empty_separator = with_suffix benötigt ein nicht leeres stdlib.path.relative_to.mismatch = { $path } ist nicht relativ zu { $root }. stdlib.path.expanduser.unsupported = Die benutzerspezifische Erweiterung von ~ wird nicht unterstützt. stdlib.path.expanduser.no_home = ~ kann nicht erweitert werden: Es sind keine Umgebungsvariablen für das Heimatverzeichnis gesetzt. +stdlib.path.contents.file_too_large = File '{ $path }' exceeded the read limit of { $limit } bytes. +stdlib.path.contents.not_regular_file = File '{ $path }' is not a regular file. stdlib.path.contents.unsupported_encoding = Nicht unterstützte Kodierung „{ $encoding }“. stdlib.path.hash.unsupported_algorithm = Nicht unterstützter Hash-Algorithmus „{ $algorithm }“. stdlib.path.hash.unsupported_algorithm_legacy = Nicht unterstützter Hash-Algorithmus „{ $algorithm }“ (aktivieren Sie das Feature „{ $feature }“). diff --git a/locales/el/messages.ftl b/locales/el/messages.ftl index 064f9da9d..2409b84ac 100644 --- a/locales/el/messages.ftl +++ b/locales/el/messages.ftl @@ -211,6 +211,7 @@ network_policy.host.not_allowlisted = Ο κόμβος «{ $host }» δεν πε stdlib.config.default_fetch_cache_invalid = Η προεπιλεγμένη διαδρομή της κρυφής μνήμης fetch πρέπει να είναι σχετική. stdlib.config.default_which_cache_invalid = Η προεπιλεγμένη χωρητικότητα της κρυφής μνήμης which πρέπει να είναι θετική. stdlib.config.workspace_root_absolute = Η ριζική διαδρομή του χώρου εργασίας πρέπει να είναι απόλυτη. +stdlib.config.file_read_limit_positive = File read limit must be positive. stdlib.config.fetch_response_limit_positive = Το όριο απόκρισης του fetch πρέπει να είναι θετικό. stdlib.config.command_output_limit_positive = Το όριο καταγραφής της εξόδου εντολών πρέπει να είναι θετικό. stdlib.config.command_stream_limit_positive = Το όριο ροής εντολών πρέπει να είναι θετικό. @@ -319,6 +320,8 @@ stdlib.path.with_suffix.empty_separator = Το with_suffix απαιτεί μη stdlib.path.relative_to.mismatch = Το { $path } δεν είναι σχετικό ως προς το { $root }. stdlib.path.expanduser.unsupported = Η ανάπτυξη του ~ για συγκεκριμένο χρήστη δεν υποστηρίζεται. stdlib.path.expanduser.no_home = Δεν είναι δυνατή η ανάπτυξη του ~: δεν έχει οριστεί καμία μεταβλητή περιβάλλοντος για τον προσωπικό κατάλογο. +stdlib.path.contents.file_too_large = File '{ $path }' exceeded the read limit of { $limit } bytes. +stdlib.path.contents.not_regular_file = File '{ $path }' is not a regular file. stdlib.path.contents.unsupported_encoding = Μη υποστηριζόμενη κωδικοποίηση «{ $encoding }». stdlib.path.hash.unsupported_algorithm = Μη υποστηριζόμενος αλγόριθμος κατακερματισμού «{ $algorithm }». stdlib.path.hash.unsupported_algorithm_legacy = Μη υποστηριζόμενος αλγόριθμος κατακερματισμού «{ $algorithm }» (ενεργοποιήστε τη δυνατότητα «{ $feature }»). diff --git a/locales/en-GB/messages.ftl b/locales/en-GB/messages.ftl index e13390eb4..b576fd5f1 100644 --- a/locales/en-GB/messages.ftl +++ b/locales/en-GB/messages.ftl @@ -210,6 +210,7 @@ network_policy.host.not_allowlisted = Host '{ $host }' is not on the allowlist. stdlib.config.default_fetch_cache_invalid = Default fetch cache path must be relative. stdlib.config.default_which_cache_invalid = Default which cache capacity must be positive. stdlib.config.workspace_root_absolute = Workspace root path must be absolute. +stdlib.config.file_read_limit_positive = File read limit must be positive. stdlib.config.fetch_response_limit_positive = Fetch response limit must be positive. stdlib.config.command_output_limit_positive = Command output capture limit must be positive. stdlib.config.command_stream_limit_positive = Command stream limit must be positive. @@ -318,6 +319,8 @@ stdlib.path.with_suffix.empty_separator = with_suffix requires a non-empty separ stdlib.path.relative_to.mismatch = { $path } is not relative to { $root }. stdlib.path.expanduser.unsupported = User-specific ~ expansion is unsupported. stdlib.path.expanduser.no_home = Cannot expand ~: no home directory environment variables are set. +stdlib.path.contents.file_too_large = File '{ $path }' exceeded the read limit of { $limit } bytes. +stdlib.path.contents.not_regular_file = File '{ $path }' is not a regular file. stdlib.path.contents.unsupported_encoding = Unsupported encoding '{ $encoding }'. stdlib.path.hash.unsupported_algorithm = Unsupported hash algorithm '{ $algorithm }'. stdlib.path.hash.unsupported_algorithm_legacy = Unsupported hash algorithm '{ $algorithm }' (enable feature '{ $feature }'). diff --git a/locales/en-US/messages.ftl b/locales/en-US/messages.ftl index 4a72828f8..27932019c 100644 --- a/locales/en-US/messages.ftl +++ b/locales/en-US/messages.ftl @@ -211,6 +211,7 @@ stdlib.config.default_fetch_cache_invalid = Default fetch cache path must be rel stdlib.config.default_which_cache_invalid = Default which cache capacity must be positive. stdlib.config.workspace_root_absolute = Workspace root path must be absolute. stdlib.config.fetch_response_limit_positive = Fetch response limit must be positive. +stdlib.config.file_read_limit_positive = File read limit must be positive. stdlib.config.command_output_limit_positive = Command output capture limit must be positive. stdlib.config.command_stream_limit_positive = Command stream limit must be positive. stdlib.config.which_cache_capacity_positive = Which cache capacity must be positive. @@ -319,6 +320,8 @@ stdlib.path.relative_to.mismatch = { $path } is not relative to { $root }. stdlib.path.expanduser.unsupported = User-specific ~ expansion is unsupported. stdlib.path.expanduser.no_home = Cannot expand ~: no home directory environment variables are set. stdlib.path.contents.unsupported_encoding = Unsupported encoding '{ $encoding }'. +stdlib.path.contents.file_too_large = File '{ $path }' exceeded the read limit of { $limit } bytes. +stdlib.path.contents.not_regular_file = File '{ $path }' is not a regular file. stdlib.path.hash.unsupported_algorithm = Unsupported hash algorithm '{ $algorithm }'. stdlib.path.hash.unsupported_algorithm_legacy = Unsupported hash algorithm '{ $algorithm }' (enable feature '{ $feature }'). diff --git a/locales/es-419/messages.ftl b/locales/es-419/messages.ftl index 714511d94..26b0a81e5 100644 --- a/locales/es-419/messages.ftl +++ b/locales/es-419/messages.ftl @@ -211,6 +211,7 @@ network_policy.host.not_allowlisted = El host '{ $host }' no está en la lista d stdlib.config.default_fetch_cache_invalid = La ruta predeterminada de la caché de fetch debe ser relativa. stdlib.config.default_which_cache_invalid = La capacidad predeterminada de la caché de which debe ser positiva. stdlib.config.workspace_root_absolute = La ruta raíz del espacio de trabajo debe ser absoluta. +stdlib.config.file_read_limit_positive = File read limit must be positive. stdlib.config.fetch_response_limit_positive = El límite de respuesta de fetch debe ser positivo. stdlib.config.command_output_limit_positive = El límite de captura de salida de comandos debe ser positivo. stdlib.config.command_stream_limit_positive = El límite de transmisión de comandos debe ser positivo. @@ -319,6 +320,8 @@ stdlib.path.with_suffix.empty_separator = with_suffix requiere un separador no v stdlib.path.relative_to.mismatch = { $path } no es relativo a { $root }. stdlib.path.expanduser.unsupported = La expansión de ~ para un usuario específico no es compatible. stdlib.path.expanduser.no_home = No se puede expandir ~: no hay variables de entorno del directorio de inicio definidas. +stdlib.path.contents.file_too_large = File '{ $path }' exceeded the read limit of { $limit } bytes. +stdlib.path.contents.not_regular_file = File '{ $path }' is not a regular file. stdlib.path.contents.unsupported_encoding = Codificación no admitida '{ $encoding }'. stdlib.path.hash.unsupported_algorithm = Algoritmo de hash no admitido '{ $algorithm }'. stdlib.path.hash.unsupported_algorithm_legacy = Algoritmo de hash no admitido '{ $algorithm }' (habilite la característica '{ $feature }'). diff --git a/locales/es-ES/messages.ftl b/locales/es-ES/messages.ftl index 57976eb71..3cb1deafe 100644 --- a/locales/es-ES/messages.ftl +++ b/locales/es-ES/messages.ftl @@ -210,6 +210,7 @@ network_policy.host.not_allowlisted = El host '{ $host }' no está en la lista d stdlib.config.default_fetch_cache_invalid = La ruta de caché de fetch por defecto debe ser relativa. stdlib.config.default_which_cache_invalid = La capacidad de caché de which por defecto debe ser positiva. stdlib.config.workspace_root_absolute = La ruta raíz del workspace debe ser absoluta. +stdlib.config.file_read_limit_positive = File read limit must be positive. stdlib.config.fetch_response_limit_positive = El límite de respuesta de fetch debe ser positivo. stdlib.config.command_output_limit_positive = El límite de captura de salida del comando debe ser positivo. stdlib.config.command_stream_limit_positive = El límite de transmisión del comando debe ser positivo. @@ -318,6 +319,8 @@ stdlib.path.with_suffix.empty_separator = with_suffix requiere un separador no v stdlib.path.relative_to.mismatch = { $path } no es relativo a { $root }. stdlib.path.expanduser.unsupported = La expansión ~ específica de usuario no es compatible. stdlib.path.expanduser.no_home = No se puede expandir ~: no hay variables de entorno de hogar. +stdlib.path.contents.file_too_large = File '{ $path }' exceeded the read limit of { $limit } bytes. +stdlib.path.contents.not_regular_file = File '{ $path }' is not a regular file. stdlib.path.contents.unsupported_encoding = Codificación no compatible '{ $encoding }'. stdlib.path.hash.unsupported_algorithm = Algoritmo hash no compatible '{ $algorithm }'. stdlib.path.hash.unsupported_algorithm_legacy = Algoritmo hash no compatible '{ $algorithm }' (habilite la función '{ $feature }'). diff --git a/locales/fa/messages.ftl b/locales/fa/messages.ftl index 52a5aa6eb..d8493d371 100644 --- a/locales/fa/messages.ftl +++ b/locales/fa/messages.ftl @@ -210,6 +210,7 @@ network_policy.host.not_allowlisted = میزبان «{ $host }» در فهرست stdlib.config.default_fetch_cache_invalid = مسیر پیش‌فرض نهانگاه fetch باید نسبی باشد. stdlib.config.default_which_cache_invalid = ظرفیت پیش‌فرض نهانگاه which باید مثبت باشد. stdlib.config.workspace_root_absolute = مسیر ریشهٔ فضای کاری باید مطلق باشد. +stdlib.config.file_read_limit_positive = کران خواندن پرونده باید مثبت باشد. stdlib.config.fetch_response_limit_positive = کران پاسخ fetch باید مثبت باشد. stdlib.config.command_output_limit_positive = کران ضبط خروجی فرمان‌ها باید مثبت باشد. stdlib.config.command_stream_limit_positive = کران جریان فرمان‌ها باید مثبت باشد. @@ -318,6 +319,8 @@ stdlib.path.with_suffix.empty_separator = ‏with_suffix به جداکننده stdlib.path.relative_to.mismatch = ‏{ $path } نسبت به { $root } نسبی نیست. stdlib.path.expanduser.unsupported = گسترش ~ برای کاربری معین پشتیبانی نمی‌شود. stdlib.path.expanduser.no_home = گسترش ~ ممکن نیست: هیچ متغیر محیطی برای شاخهٔ خانگی تنظیم نشده است. +stdlib.path.contents.file_too_large = پرونده '{ $path }' از کران خواندن { $limit } بایتی فراتر رفت. +stdlib.path.contents.not_regular_file = پرونده '{ $path }' یک پرونده معمولی نیست. stdlib.path.contents.unsupported_encoding = رمزگذاری پشتیبانی‌نشده: «{ $encoding }». stdlib.path.hash.unsupported_algorithm = الگوریتم درهم‌سازی پشتیبانی‌نشده: «{ $algorithm }». stdlib.path.hash.unsupported_algorithm_legacy = الگوریتم درهم‌سازی پشتیبانی‌نشده: «{ $algorithm }» (ویژگی «{ $feature }» را فعال کنید). diff --git a/locales/fi/messages.ftl b/locales/fi/messages.ftl index 9a3819f00..8e777fad1 100644 --- a/locales/fi/messages.ftl +++ b/locales/fi/messages.ftl @@ -210,6 +210,7 @@ network_policy.host.not_allowlisted = Isäntä ”{ $host }” ei ole sallittuje stdlib.config.default_fetch_cache_invalid = fetch-välimuistin oletuspolun on oltava suhteellinen. stdlib.config.default_which_cache_invalid = which-välimuistin oletuskapasiteetin on oltava positiivinen. stdlib.config.workspace_root_absolute = Työtilan juuripolun on oltava absoluuttinen. +stdlib.config.file_read_limit_positive = File read limit must be positive. stdlib.config.fetch_response_limit_positive = fetch-vastauksen rajan on oltava positiivinen. stdlib.config.command_output_limit_positive = Komennon tulosteen talteenoton rajan on oltava positiivinen. stdlib.config.command_stream_limit_positive = Komennon virtausrajan on oltava positiivinen. @@ -318,6 +319,8 @@ stdlib.path.with_suffix.empty_separator = with_suffix vaatii erottimen, joka ei stdlib.path.relative_to.mismatch = { $path } ei ole suhteellinen polkuun { $root } nähden. stdlib.path.expanduser.unsupported = Käyttäjäkohtaista ~-laajennusta ei tueta. stdlib.path.expanduser.no_home = Merkkiä ~ ei voi laajentaa: kotihakemiston ympäristömuuttujia ei ole asetettu. +stdlib.path.contents.file_too_large = File '{ $path }' exceeded the read limit of { $limit } bytes. +stdlib.path.contents.not_regular_file = File '{ $path }' is not a regular file. stdlib.path.contents.unsupported_encoding = Merkistökoodausta ”{ $encoding }” ei tueta. stdlib.path.hash.unsupported_algorithm = Tiivistealgoritmia ”{ $algorithm }” ei tueta. stdlib.path.hash.unsupported_algorithm_legacy = Tiivistealgoritmia ”{ $algorithm }” ei tueta (ota käyttöön ominaisuus ”{ $feature }”). diff --git a/locales/fr/messages.ftl b/locales/fr/messages.ftl index 0f5e7058f..799aca0d5 100644 --- a/locales/fr/messages.ftl +++ b/locales/fr/messages.ftl @@ -211,6 +211,7 @@ network_policy.host.not_allowlisted = L'hôte « { $host } » ne figure pas dans stdlib.config.default_fetch_cache_invalid = Le chemin de cache fetch par défaut doit être relatif. stdlib.config.default_which_cache_invalid = La capacité de cache which par défaut doit être positive. stdlib.config.workspace_root_absolute = Le chemin racine de l'espace de travail doit être absolu. +stdlib.config.file_read_limit_positive = File read limit must be positive. stdlib.config.fetch_response_limit_positive = La limite de réponse de fetch doit être positive. stdlib.config.command_output_limit_positive = La limite de capture de sortie des commandes doit être positive. stdlib.config.command_stream_limit_positive = La limite de flux des commandes doit être positive. @@ -319,6 +320,8 @@ stdlib.path.with_suffix.empty_separator = with_suffix exige un séparateur non v stdlib.path.relative_to.mismatch = { $path } n'est pas relatif à { $root }. stdlib.path.expanduser.unsupported = L'expansion de ~ propre à un utilisateur n'est pas prise en charge. stdlib.path.expanduser.no_home = Impossible d'étendre ~ : aucune variable d'environnement de répertoire personnel n'est définie. +stdlib.path.contents.file_too_large = File '{ $path }' exceeded the read limit of { $limit } bytes. +stdlib.path.contents.not_regular_file = File '{ $path }' is not a regular file. stdlib.path.contents.unsupported_encoding = Encodage non pris en charge « { $encoding } ». stdlib.path.hash.unsupported_algorithm = Algorithme de hachage non pris en charge « { $algorithm } ». stdlib.path.hash.unsupported_algorithm_legacy = Algorithme de hachage non pris en charge « { $algorithm } » (activez la fonctionnalité « { $feature } »). diff --git a/locales/gd/messages.ftl b/locales/gd/messages.ftl index e71ba660d..e24fbf821 100644 --- a/locales/gd/messages.ftl +++ b/locales/gd/messages.ftl @@ -210,6 +210,7 @@ network_policy.host.not_allowlisted = Chan eil an t-òstair “{ $host }” air stdlib.config.default_fetch_cache_invalid = Feumaidh slighe bhunaiteach tasgadan fetch a bhith coimeasach. stdlib.config.default_which_cache_invalid = Feumaidh tomhas bunaiteach tasgadan which a bhith dearbh. stdlib.config.workspace_root_absolute = Feumaidh slighe freumh an raoin-obrach a bhith absaloideach. +stdlib.config.file_read_limit_positive = File read limit must be positive. stdlib.config.fetch_response_limit_positive = Feumaidh crìoch freagairt fetch a bhith dearbh. stdlib.config.command_output_limit_positive = Feumaidh crìoch glacadh às-chur nan àitheantan a bhith dearbh. stdlib.config.command_stream_limit_positive = Feumaidh crìoch sruth nan àitheantan a bhith dearbh. @@ -318,6 +319,8 @@ stdlib.path.with_suffix.empty_separator = Tha with_suffix ag iarraidh sgaradair stdlib.path.relative_to.mismatch = Chan eil { $path } coimeasach ri { $root }. stdlib.path.expanduser.unsupported = Chan eil taic ann do leudachadh ~ airson cleachdaiche sònraichte. stdlib.path.expanduser.no_home = Chan urrainnear ~ a leudachadh: chan eil caochladair àrainneachd sam bith ann airson a' phasgain dhachaigh. +stdlib.path.contents.file_too_large = File '{ $path }' exceeded the read limit of { $limit } bytes. +stdlib.path.contents.not_regular_file = File '{ $path }' is not a regular file. stdlib.path.contents.unsupported_encoding = Còdachadh gun taic: “{ $encoding }”. stdlib.path.hash.unsupported_algorithm = Algairim hais gun taic: “{ $algorithm }”. stdlib.path.hash.unsupported_algorithm_legacy = Algairim hais gun taic: “{ $algorithm }” (cuir an comas am feart “{ $feature }”). diff --git a/locales/he/messages.ftl b/locales/he/messages.ftl index b796e4db2..3773e19d0 100644 --- a/locales/he/messages.ftl +++ b/locales/he/messages.ftl @@ -210,6 +210,7 @@ network_policy.host.not_allowlisted = המארח „{ $host }” אינו ברש stdlib.config.default_fetch_cache_invalid = נתיב ברירת המחדל של מטמון fetch חייב להיות יחסי. stdlib.config.default_which_cache_invalid = קיבולת ברירת המחדל של מטמון which חייבת להיות חיובית. stdlib.config.workspace_root_absolute = נתיב השורש של סביבת העבודה חייב להיות מוחלט. +stdlib.config.file_read_limit_positive = מגבלת קריאת הקבצים חייבת להיות חיובית. stdlib.config.fetch_response_limit_positive = מגבלת התגובה של fetch חייבת להיות חיובית. stdlib.config.command_output_limit_positive = מגבלת לכידת פלט הפקודות חייבת להיות חיובית. stdlib.config.command_stream_limit_positive = מגבלת הזרימה של הפקודות חייבת להיות חיובית. @@ -318,6 +319,8 @@ stdlib.path.with_suffix.empty_separator = ‏with_suffix מחייב מפריד stdlib.path.relative_to.mismatch = ‏{ $path } אינו יחסי אל { $root }. stdlib.path.expanduser.unsupported = הרחבת ~ עבור משתמש מסוים אינה נתמכת. stdlib.path.expanduser.no_home = לא ניתן להרחיב את ~: לא הוגדר אף משתנה סביבה לספריית הבית. +stdlib.path.contents.file_too_large = הקובץ '{ $path }' חרג ממגבלת הקריאה של { $limit } בייטים. +stdlib.path.contents.not_regular_file = הקובץ '{ $path }' אינו קובץ רגיל. stdlib.path.contents.unsupported_encoding = קידוד שאינו נתמך: „{ $encoding }”. stdlib.path.hash.unsupported_algorithm = אלגוריתם גיבוב שאינו נתמך: „{ $algorithm }”. stdlib.path.hash.unsupported_algorithm_legacy = אלגוריתם גיבוב שאינו נתמך: „{ $algorithm }” (הפעילו את התכונה „{ $feature }”). diff --git a/locales/hi/messages.ftl b/locales/hi/messages.ftl index 0ed615e4b..b24d4ad16 100644 --- a/locales/hi/messages.ftl +++ b/locales/hi/messages.ftl @@ -210,6 +210,7 @@ network_policy.host.not_allowlisted = होस्ट “{ $host }” अनु stdlib.config.default_fetch_cache_invalid = fetch कैश का डिफ़ॉल्ट पथ सापेक्ष होना चाहिए। stdlib.config.default_which_cache_invalid = which कैश की डिफ़ॉल्ट क्षमता धनात्मक होनी चाहिए। stdlib.config.workspace_root_absolute = कार्यक्षेत्र का मूल पथ निरपेक्ष होना चाहिए। +stdlib.config.file_read_limit_positive = File read limit must be positive. stdlib.config.fetch_response_limit_positive = fetch की अनुक्रिया सीमा धनात्मक होनी चाहिए। stdlib.config.command_output_limit_positive = आदेश के निर्गम को संचित करने की सीमा धनात्मक होनी चाहिए। stdlib.config.command_stream_limit_positive = आदेशों की धारा सीमा धनात्मक होनी चाहिए। @@ -318,6 +319,8 @@ stdlib.path.with_suffix.empty_separator = with_suffix को अरिक्त stdlib.path.relative_to.mismatch = { $path } { $root } के सापेक्ष नहीं है। stdlib.path.expanduser.unsupported = किसी विशेष उपयोक्ता के लिए ~ का विस्तार समर्थित नहीं है। stdlib.path.expanduser.no_home = ~ का विस्तार नहीं हो सकता: गृह निर्देशिका का कोई परिवेश चर निर्धारित नहीं है। +stdlib.path.contents.file_too_large = File '{ $path }' exceeded the read limit of { $limit } bytes. +stdlib.path.contents.not_regular_file = File '{ $path }' is not a regular file. stdlib.path.contents.unsupported_encoding = असमर्थित कूटलेखन: “{ $encoding }”। stdlib.path.hash.unsupported_algorithm = असमर्थित हैश कलनविधि: “{ $algorithm }”। stdlib.path.hash.unsupported_algorithm_legacy = असमर्थित हैश कलनविधि: “{ $algorithm }” (“{ $feature }” सुविधा सक्षम करें)। diff --git a/locales/hu/messages.ftl b/locales/hu/messages.ftl index 92c819d19..112029eab 100644 --- a/locales/hu/messages.ftl +++ b/locales/hu/messages.ftl @@ -210,6 +210,7 @@ network_policy.host.not_allowlisted = A(z) „{ $host }” gép nem szerepel az stdlib.config.default_fetch_cache_invalid = A fetch gyorsítótárának alapértelmezett útvonalának viszonylagosnak kell lennie. stdlib.config.default_which_cache_invalid = A which gyorsítótárának alapértelmezett kapacitásának pozitívnak kell lennie. stdlib.config.workspace_root_absolute = A munkaterület gyökérútvonalának abszolútnak kell lennie. +stdlib.config.file_read_limit_positive = File read limit must be positive. stdlib.config.fetch_response_limit_positive = A fetch válaszkorlátjának pozitívnak kell lennie. stdlib.config.command_output_limit_positive = A parancskimenet rögzítési korlátjának pozitívnak kell lennie. stdlib.config.command_stream_limit_positive = A parancsok folyamkorlátjának pozitívnak kell lennie. @@ -318,6 +319,8 @@ stdlib.path.with_suffix.empty_separator = A with_suffix nem üres elválasztót stdlib.path.relative_to.mismatch = A(z) { $path } nem viszonyítható ehhez: { $root }. stdlib.path.expanduser.unsupported = A ~ jel adott felhasználóra vonatkozó kibontása nem támogatott. stdlib.path.expanduser.no_home = A ~ jel nem bontható ki: nincs beállítva a saját könyvtárra vonatkozó környezeti változó. +stdlib.path.contents.file_too_large = File '{ $path }' exceeded the read limit of { $limit } bytes. +stdlib.path.contents.not_regular_file = File '{ $path }' is not a regular file. stdlib.path.contents.unsupported_encoding = Nem támogatott kódolás: „{ $encoding }”. stdlib.path.hash.unsupported_algorithm = Nem támogatott kivonatoló algoritmus: „{ $algorithm }”. stdlib.path.hash.unsupported_algorithm_legacy = Nem támogatott kivonatoló algoritmus: „{ $algorithm }” (kapcsolja be a(z) „{ $feature }” szolgáltatást). diff --git a/locales/id/messages.ftl b/locales/id/messages.ftl index 6db8e6ac3..39579b1a7 100644 --- a/locales/id/messages.ftl +++ b/locales/id/messages.ftl @@ -210,6 +210,7 @@ network_policy.host.not_allowlisted = Host "{ $host }" tidak ada dalam daftar ya stdlib.config.default_fetch_cache_invalid = Jalur bawaan singgahan fetch harus relatif. stdlib.config.default_which_cache_invalid = Kapasitas bawaan singgahan which harus positif. stdlib.config.workspace_root_absolute = Jalur akar ruang kerja harus absolut. +stdlib.config.file_read_limit_positive = File read limit must be positive. stdlib.config.fetch_response_limit_positive = Batas tanggapan fetch harus positif. stdlib.config.command_output_limit_positive = Batas penangkapan keluaran perintah harus positif. stdlib.config.command_stream_limit_positive = Batas aliran perintah harus positif. @@ -318,6 +319,8 @@ stdlib.path.with_suffix.empty_separator = with_suffix memerlukan pemisah yang ti stdlib.path.relative_to.mismatch = { $path } tidak relatif terhadap { $root }. stdlib.path.expanduser.unsupported = Ekspansi ~ untuk pengguna tertentu tidak didukung. stdlib.path.expanduser.no_home = ~ tidak dapat diekspansi: tidak ada variabel lingkungan direktori beranda yang disetel. +stdlib.path.contents.file_too_large = File '{ $path }' exceeded the read limit of { $limit } bytes. +stdlib.path.contents.not_regular_file = File '{ $path }' is not a regular file. stdlib.path.contents.unsupported_encoding = Pengodean tidak didukung: "{ $encoding }". stdlib.path.hash.unsupported_algorithm = Algoritme hash tidak didukung: "{ $algorithm }". stdlib.path.hash.unsupported_algorithm_legacy = Algoritme hash tidak didukung: "{ $algorithm }" (aktifkan fitur "{ $feature }"). diff --git a/locales/it/messages.ftl b/locales/it/messages.ftl index b8a7f3b74..1bf896e1f 100644 --- a/locales/it/messages.ftl +++ b/locales/it/messages.ftl @@ -211,6 +211,7 @@ network_policy.host.not_allowlisted = L'host «{ $host }» non è nell'elenco de stdlib.config.default_fetch_cache_invalid = Il percorso predefinito della cache di fetch deve essere relativo. stdlib.config.default_which_cache_invalid = La capacità predefinita della cache di which deve essere positiva. stdlib.config.workspace_root_absolute = Il percorso radice dell'area di lavoro deve essere assoluto. +stdlib.config.file_read_limit_positive = File read limit must be positive. stdlib.config.fetch_response_limit_positive = Il limite di risposta di fetch deve essere positivo. stdlib.config.command_output_limit_positive = Il limite di cattura dell'output dei comandi deve essere positivo. stdlib.config.command_stream_limit_positive = Il limite di streaming dei comandi deve essere positivo. @@ -319,6 +320,8 @@ stdlib.path.with_suffix.empty_separator = with_suffix richiede un separatore non stdlib.path.relative_to.mismatch = { $path } non è relativo a { $root }. stdlib.path.expanduser.unsupported = L'espansione di ~ per uno specifico utente non è supportata. stdlib.path.expanduser.no_home = Impossibile espandere ~: non è impostata alcuna variabile d'ambiente per la directory home. +stdlib.path.contents.file_too_large = File '{ $path }' exceeded the read limit of { $limit } bytes. +stdlib.path.contents.not_regular_file = File '{ $path }' is not a regular file. stdlib.path.contents.unsupported_encoding = Codifica non supportata «{ $encoding }». stdlib.path.hash.unsupported_algorithm = Algoritmo di hash non supportato «{ $algorithm }». stdlib.path.hash.unsupported_algorithm_legacy = Algoritmo di hash non supportato «{ $algorithm }» (abilita la funzionalità «{ $feature }»). diff --git a/locales/ja/messages.ftl b/locales/ja/messages.ftl index 15f61db92..3dafd8880 100644 --- a/locales/ja/messages.ftl +++ b/locales/ja/messages.ftl @@ -210,6 +210,7 @@ network_policy.host.not_allowlisted = ホスト「{ $host }」は許可リスト stdlib.config.default_fetch_cache_invalid = fetch キャッシュの既定のパスは相対パスでなければなりません。 stdlib.config.default_which_cache_invalid = which キャッシュの既定の容量は正の値でなければなりません。 stdlib.config.workspace_root_absolute = ワークスペースのルートパスは絶対パスでなければなりません。 +stdlib.config.file_read_limit_positive = File read limit must be positive. stdlib.config.fetch_response_limit_positive = fetch の応答上限は正の値でなければなりません。 stdlib.config.command_output_limit_positive = コマンド出力の取り込み上限は正の値でなければなりません。 stdlib.config.command_stream_limit_positive = コマンドのストリーム上限は正の値でなければなりません。 @@ -318,6 +319,8 @@ stdlib.path.with_suffix.empty_separator = with_suffix には空でない区切 stdlib.path.relative_to.mismatch = { $path } は { $root } からの相対パスではありません。 stdlib.path.expanduser.unsupported = 特定ユーザーに対する ~ の展開には対応していません。 stdlib.path.expanduser.no_home = ~ を展開できません。ホームディレクトリーの環境変数が設定されていません。 +stdlib.path.contents.file_too_large = File '{ $path }' exceeded the read limit of { $limit } bytes. +stdlib.path.contents.not_regular_file = File '{ $path }' is not a regular file. stdlib.path.contents.unsupported_encoding = 対応していない文字符号化「{ $encoding }」です。 stdlib.path.hash.unsupported_algorithm = 対応していないハッシュアルゴリズム「{ $algorithm }」です。 stdlib.path.hash.unsupported_algorithm_legacy = 対応していないハッシュアルゴリズム「{ $algorithm }」です(機能「{ $feature }」を有効にしてください)。 diff --git a/locales/ko/messages.ftl b/locales/ko/messages.ftl index 553a78802..59d7124d3 100644 --- a/locales/ko/messages.ftl +++ b/locales/ko/messages.ftl @@ -210,6 +210,7 @@ network_policy.host.not_allowlisted = 호스트 '{ $host }'은(는) 허용 목 stdlib.config.default_fetch_cache_invalid = fetch 캐시의 기본 경로는 상대 경로여야 합니다. stdlib.config.default_which_cache_invalid = which 캐시의 기본 용량은 양수여야 합니다. stdlib.config.workspace_root_absolute = 작업 공간의 루트 경로는 절대 경로여야 합니다. +stdlib.config.file_read_limit_positive = File read limit must be positive. stdlib.config.fetch_response_limit_positive = fetch의 응답 한도는 양수여야 합니다. stdlib.config.command_output_limit_positive = 명령 출력의 수집 한도는 양수여야 합니다. stdlib.config.command_stream_limit_positive = 명령의 스트림 한도는 양수여야 합니다. @@ -318,6 +319,8 @@ stdlib.path.with_suffix.empty_separator = with_suffix에는 비어 있지 않은 stdlib.path.relative_to.mismatch = { $path }은(는) { $root }에 대한 상대 경로가 아닙니다. stdlib.path.expanduser.unsupported = 특정 사용자에 대한 ~ 확장은 지원하지 않습니다. stdlib.path.expanduser.no_home = ~을(를) 확장할 수 없습니다. 홈 디렉터리 환경 변수가 설정되지 않았습니다. +stdlib.path.contents.file_too_large = File '{ $path }' exceeded the read limit of { $limit } bytes. +stdlib.path.contents.not_regular_file = File '{ $path }' is not a regular file. stdlib.path.contents.unsupported_encoding = 지원하지 않는 인코딩 '{ $encoding }'입니다. stdlib.path.hash.unsupported_algorithm = 지원하지 않는 해시 알고리즘 '{ $algorithm }'입니다. stdlib.path.hash.unsupported_algorithm_legacy = 지원하지 않는 해시 알고리즘 '{ $algorithm }'입니다('{ $feature }' 기능을 켜세요). diff --git a/locales/nb/messages.ftl b/locales/nb/messages.ftl index 217d4f9e8..88e53807a 100644 --- a/locales/nb/messages.ftl +++ b/locales/nb/messages.ftl @@ -210,6 +210,7 @@ network_policy.host.not_allowlisted = Verten «{ $host }» står ikke på listen stdlib.config.default_fetch_cache_invalid = Standardstien til fetch-hurtiglageret må være relativ. stdlib.config.default_which_cache_invalid = Standardkapasiteten for which-hurtiglageret må være positiv. stdlib.config.workspace_root_absolute = Rotstien til arbeidsområdet må være absolutt. +stdlib.config.file_read_limit_positive = File read limit must be positive. stdlib.config.fetch_response_limit_positive = Svargrensen for fetch må være positiv. stdlib.config.command_output_limit_positive = Grensen for fanget kommandoutdata må være positiv. stdlib.config.command_stream_limit_positive = Strømgrensen for kommandoer må være positiv. @@ -318,6 +319,8 @@ stdlib.path.with_suffix.empty_separator = with_suffix krever et skilletegn som i stdlib.path.relative_to.mismatch = { $path } er ikke relativ til { $root }. stdlib.path.expanduser.unsupported = Brukerspesifikk utvidelse av ~ støttes ikke. stdlib.path.expanduser.no_home = ~ kan ikke utvides: ingen miljøvariabler for hjemmekatalogen er satt. +stdlib.path.contents.file_too_large = File '{ $path }' exceeded the read limit of { $limit } bytes. +stdlib.path.contents.not_regular_file = File '{ $path }' is not a regular file. stdlib.path.contents.unsupported_encoding = Tegnkodingen «{ $encoding }» støttes ikke. stdlib.path.hash.unsupported_algorithm = Hash-algoritmen «{ $algorithm }» støttes ikke. stdlib.path.hash.unsupported_algorithm_legacy = Hash-algoritmen «{ $algorithm }» støttes ikke (slå på funksjonen «{ $feature }»). diff --git a/locales/nl/messages.ftl b/locales/nl/messages.ftl index 16e00c986..0baabea23 100644 --- a/locales/nl/messages.ftl +++ b/locales/nl/messages.ftl @@ -210,6 +210,7 @@ network_policy.host.not_allowlisted = Host ‘{ $host }’ staat niet op de lijs stdlib.config.default_fetch_cache_invalid = Het standaardpad van de fetch-cache moet relatief zijn. stdlib.config.default_which_cache_invalid = De standaardcapaciteit van de which-cache moet positief zijn. stdlib.config.workspace_root_absolute = Het hoofdpad van de werkruimte moet absoluut zijn. +stdlib.config.file_read_limit_positive = File read limit must be positive. stdlib.config.fetch_response_limit_positive = De antwoordlimiet van fetch moet positief zijn. stdlib.config.command_output_limit_positive = De limiet voor vastgelegde opdrachtuitvoer moet positief zijn. stdlib.config.command_stream_limit_positive = De streamlimiet voor opdrachten moet positief zijn. @@ -318,6 +319,8 @@ stdlib.path.with_suffix.empty_separator = with_suffix vereist een scheidingsteke stdlib.path.relative_to.mismatch = { $path } is niet relatief ten opzichte van { $root }. stdlib.path.expanduser.unsupported = Gebruikerspecifieke uitbreiding van ~ wordt niet ondersteund. stdlib.path.expanduser.no_home = ~ kan niet worden uitgebreid: er zijn geen omgevingsvariabelen voor de thuismap ingesteld. +stdlib.path.contents.file_too_large = File '{ $path }' exceeded the read limit of { $limit } bytes. +stdlib.path.contents.not_regular_file = File '{ $path }' is not a regular file. stdlib.path.contents.unsupported_encoding = De tekencodering ‘{ $encoding }’ wordt niet ondersteund. stdlib.path.hash.unsupported_algorithm = Het hash-algoritme ‘{ $algorithm }’ wordt niet ondersteund. stdlib.path.hash.unsupported_algorithm_legacy = Het hash-algoritme ‘{ $algorithm }’ wordt niet ondersteund (schakel functie ‘{ $feature }’ in). diff --git a/locales/pl/messages.ftl b/locales/pl/messages.ftl index 0e99e391a..1447cb782 100644 --- a/locales/pl/messages.ftl +++ b/locales/pl/messages.ftl @@ -210,6 +210,7 @@ network_policy.host.not_allowlisted = Hosta „{ $host }” nie ma na liście do stdlib.config.default_fetch_cache_invalid = Domyślna ścieżka pamięci podręcznej fetch musi być względna. stdlib.config.default_which_cache_invalid = Domyślna pojemność pamięci podręcznej which musi być dodatnia. stdlib.config.workspace_root_absolute = Ścieżka główna obszaru roboczego musi być bezwzględna. +stdlib.config.file_read_limit_positive = File read limit must be positive. stdlib.config.fetch_response_limit_positive = Limit odpowiedzi fetch musi być dodatni. stdlib.config.command_output_limit_positive = Limit przechwytywanego wyjścia poleceń musi być dodatni. stdlib.config.command_stream_limit_positive = Limit strumienia poleceń musi być dodatni. @@ -318,6 +319,8 @@ stdlib.path.with_suffix.empty_separator = with_suffix wymaga niepustego separato stdlib.path.relative_to.mismatch = Ścieżka { $path } nie jest względna względem { $root }. stdlib.path.expanduser.unsupported = Rozwijanie ~ dla konkretnego użytkownika nie jest obsługiwane. stdlib.path.expanduser.no_home = Nie można rozwinąć ~: nie ustawiono żadnej zmiennej środowiskowej katalogu domowego. +stdlib.path.contents.file_too_large = File '{ $path }' exceeded the read limit of { $limit } bytes. +stdlib.path.contents.not_regular_file = File '{ $path }' is not a regular file. stdlib.path.contents.unsupported_encoding = Nieobsługiwane kodowanie „{ $encoding }”. stdlib.path.hash.unsupported_algorithm = Nieobsługiwany algorytm skrótu „{ $algorithm }”. stdlib.path.hash.unsupported_algorithm_legacy = Nieobsługiwany algorytm skrótu „{ $algorithm }” (włącz funkcję „{ $feature }”). diff --git a/locales/pt-BR/messages.ftl b/locales/pt-BR/messages.ftl index 028733d2a..4db2a04ac 100644 --- a/locales/pt-BR/messages.ftl +++ b/locales/pt-BR/messages.ftl @@ -211,6 +211,7 @@ network_policy.host.not_allowlisted = O host "{ $host }" não está na lista de stdlib.config.default_fetch_cache_invalid = O caminho padrão do cache do fetch deve ser relativo. stdlib.config.default_which_cache_invalid = A capacidade padrão do cache do which deve ser positiva. stdlib.config.workspace_root_absolute = O caminho da raiz do workspace deve ser absoluto. +stdlib.config.file_read_limit_positive = File read limit must be positive. stdlib.config.fetch_response_limit_positive = O limite de resposta do fetch deve ser positivo. stdlib.config.command_output_limit_positive = O limite de captura da saída dos comandos deve ser positivo. stdlib.config.command_stream_limit_positive = O limite de streaming dos comandos deve ser positivo. @@ -319,6 +320,8 @@ stdlib.path.with_suffix.empty_separator = with_suffix exige um separador não va stdlib.path.relative_to.mismatch = { $path } não é relativo a { $root }. stdlib.path.expanduser.unsupported = A expansão de ~ para um usuário específico não tem suporte. stdlib.path.expanduser.no_home = Não é possível expandir ~: nenhuma variável de ambiente do diretório pessoal está definida. +stdlib.path.contents.file_too_large = File '{ $path }' exceeded the read limit of { $limit } bytes. +stdlib.path.contents.not_regular_file = File '{ $path }' is not a regular file. stdlib.path.contents.unsupported_encoding = Codificação sem suporte "{ $encoding }". stdlib.path.hash.unsupported_algorithm = Algoritmo de hash sem suporte "{ $algorithm }". stdlib.path.hash.unsupported_algorithm_legacy = Algoritmo de hash sem suporte "{ $algorithm }" (habilite o recurso "{ $feature }"). diff --git a/locales/pt-PT/messages.ftl b/locales/pt-PT/messages.ftl index 25bd51516..1d19cb40d 100644 --- a/locales/pt-PT/messages.ftl +++ b/locales/pt-PT/messages.ftl @@ -211,6 +211,7 @@ network_policy.host.not_allowlisted = O anfitrião «{ $host }» não consta da stdlib.config.default_fetch_cache_invalid = O caminho predefinido da cache do fetch tem de ser relativo. stdlib.config.default_which_cache_invalid = A capacidade predefinida da cache do which tem de ser positiva. stdlib.config.workspace_root_absolute = O caminho de raiz da área de trabalho tem de ser absoluto. +stdlib.config.file_read_limit_positive = File read limit must be positive. stdlib.config.fetch_response_limit_positive = O limite de resposta do fetch tem de ser positivo. stdlib.config.command_output_limit_positive = O limite de captura da saída dos comandos tem de ser positivo. stdlib.config.command_stream_limit_positive = O limite de fluxo dos comandos tem de ser positivo. @@ -319,6 +320,8 @@ stdlib.path.with_suffix.empty_separator = with_suffix exige um separador não va stdlib.path.relative_to.mismatch = { $path } não é relativo a { $root }. stdlib.path.expanduser.unsupported = A expansão de ~ para um utilizador específico não é suportada. stdlib.path.expanduser.no_home = Não é possível expandir ~: não há variáveis de ambiente da pasta pessoal definidas. +stdlib.path.contents.file_too_large = File '{ $path }' exceeded the read limit of { $limit } bytes. +stdlib.path.contents.not_regular_file = File '{ $path }' is not a regular file. stdlib.path.contents.unsupported_encoding = Codificação não suportada «{ $encoding }». stdlib.path.hash.unsupported_algorithm = Algoritmo de hash não suportado «{ $algorithm }». stdlib.path.hash.unsupported_algorithm_legacy = Algoritmo de hash não suportado «{ $algorithm }» (ative a funcionalidade «{ $feature }»). diff --git a/locales/ro/messages.ftl b/locales/ro/messages.ftl index fc7e87348..573bad756 100644 --- a/locales/ro/messages.ftl +++ b/locales/ro/messages.ftl @@ -210,6 +210,7 @@ network_policy.host.not_allowlisted = Gazda „{ $host }” nu se află pe lista stdlib.config.default_fetch_cache_invalid = Calea implicită a memoriei cache fetch trebuie să fie relativă. stdlib.config.default_which_cache_invalid = Capacitatea implicită a memoriei cache which trebuie să fie pozitivă. stdlib.config.workspace_root_absolute = Calea rădăcină a spațiului de lucru trebuie să fie absolută. +stdlib.config.file_read_limit_positive = File read limit must be positive. stdlib.config.fetch_response_limit_positive = Limita răspunsului fetch trebuie să fie pozitivă. stdlib.config.command_output_limit_positive = Limita ieșirii capturate a comenzilor trebuie să fie pozitivă. stdlib.config.command_stream_limit_positive = Limita fluxului comenzilor trebuie să fie pozitivă. @@ -318,6 +319,8 @@ stdlib.path.with_suffix.empty_separator = with_suffix necesită un separator car stdlib.path.relative_to.mismatch = { $path } nu este relativ la { $root }. stdlib.path.expanduser.unsupported = Extinderea lui ~ pentru un anumit utilizator nu este acceptată. stdlib.path.expanduser.no_home = Nu se poate extinde ~: nu este setată nicio variabilă de mediu pentru directorul personal. +stdlib.path.contents.file_too_large = File '{ $path }' exceeded the read limit of { $limit } bytes. +stdlib.path.contents.not_regular_file = File '{ $path }' is not a regular file. stdlib.path.contents.unsupported_encoding = Codificare neacceptată „{ $encoding }”. stdlib.path.hash.unsupported_algorithm = Algoritm de dispersie neacceptat „{ $algorithm }”. stdlib.path.hash.unsupported_algorithm_legacy = Algoritm de dispersie neacceptat „{ $algorithm }” (activați funcționalitatea „{ $feature }”). diff --git a/locales/ru/messages.ftl b/locales/ru/messages.ftl index 3f5520e50..74c411027 100644 --- a/locales/ru/messages.ftl +++ b/locales/ru/messages.ftl @@ -210,6 +210,7 @@ network_policy.host.not_allowlisted = Узла «{ $host }» нет в спис stdlib.config.default_fetch_cache_invalid = Путь кэша fetch по умолчанию должен быть относительным. stdlib.config.default_which_cache_invalid = Ёмкость кэша which по умолчанию должна быть положительной. stdlib.config.workspace_root_absolute = Корневой путь рабочего пространства должен быть абсолютным. +stdlib.config.file_read_limit_positive = File read limit must be positive. stdlib.config.fetch_response_limit_positive = Ограничение на ответ fetch должно быть положительным. stdlib.config.command_output_limit_positive = Ограничение на перехватываемый вывод команд должно быть положительным. stdlib.config.command_stream_limit_positive = Ограничение на поток команд должно быть положительным. @@ -318,6 +319,8 @@ stdlib.path.with_suffix.empty_separator = with_suffix требует непус stdlib.path.relative_to.mismatch = { $path } не является относительным к { $root }. stdlib.path.expanduser.unsupported = Раскрытие ~ для конкретного пользователя не поддерживается. stdlib.path.expanduser.no_home = Не удаётся раскрыть ~: не задана ни одна переменная окружения домашнего каталога. +stdlib.path.contents.file_too_large = File '{ $path }' exceeded the read limit of { $limit } bytes. +stdlib.path.contents.not_regular_file = File '{ $path }' is not a regular file. stdlib.path.contents.unsupported_encoding = Неподдерживаемая кодировка «{ $encoding }». stdlib.path.hash.unsupported_algorithm = Неподдерживаемый алгоритм хеширования «{ $algorithm }». stdlib.path.hash.unsupported_algorithm_legacy = Неподдерживаемый алгоритм хеширования «{ $algorithm }» (включите возможность «{ $feature }»). diff --git a/locales/sv/messages.ftl b/locales/sv/messages.ftl index ffc4c6280..09086610a 100644 --- a/locales/sv/messages.ftl +++ b/locales/sv/messages.ftl @@ -210,6 +210,7 @@ network_policy.host.not_allowlisted = Värden ”{ $host }” finns inte på lis stdlib.config.default_fetch_cache_invalid = Standardsökvägen till fetch-cachen måste vara relativ. stdlib.config.default_which_cache_invalid = Standardkapaciteten för which-cachen måste vara positiv. stdlib.config.workspace_root_absolute = Arbetsytans rotsökväg måste vara absolut. +stdlib.config.file_read_limit_positive = File read limit must be positive. stdlib.config.fetch_response_limit_positive = Svarsgränsen för fetch måste vara positiv. stdlib.config.command_output_limit_positive = Gränsen för fångad kommandoutdata måste vara positiv. stdlib.config.command_stream_limit_positive = Strömgränsen för kommandon måste vara positiv. @@ -318,6 +319,8 @@ stdlib.path.with_suffix.empty_separator = with_suffix kräver en avgränsare som stdlib.path.relative_to.mismatch = { $path } är inte relativ till { $root }. stdlib.path.expanduser.unsupported = Användarspecifik expansion av ~ stöds inte. stdlib.path.expanduser.no_home = ~ kan inte expanderas: inga miljövariabler för hemkatalogen är satta. +stdlib.path.contents.file_too_large = File '{ $path }' exceeded the read limit of { $limit } bytes. +stdlib.path.contents.not_regular_file = File '{ $path }' is not a regular file. stdlib.path.contents.unsupported_encoding = Teckenkodningen ”{ $encoding }” stöds inte. stdlib.path.hash.unsupported_algorithm = Hashalgoritmen ”{ $algorithm }” stöds inte. stdlib.path.hash.unsupported_algorithm_legacy = Hashalgoritmen ”{ $algorithm }” stöds inte (aktivera funktionen ”{ $feature }”). diff --git a/locales/th/messages.ftl b/locales/th/messages.ftl index a9bf85be9..725ac02a3 100644 --- a/locales/th/messages.ftl +++ b/locales/th/messages.ftl @@ -210,6 +210,7 @@ network_policy.host.not_allowlisted = โฮสต์ “{ $host }” ไม่ stdlib.config.default_fetch_cache_invalid = เส้นทางแคชของ fetch โดยปริยายต้องเป็นเส้นทางสัมพัทธ์ stdlib.config.default_which_cache_invalid = ความจุแคชของ which โดยปริยายต้องเป็นจำนวนบวก stdlib.config.workspace_root_absolute = เส้นทางรากของพื้นที่ทำงานต้องเป็นเส้นทางสัมบูรณ์ +stdlib.config.file_read_limit_positive = File read limit must be positive. stdlib.config.fetch_response_limit_positive = ขีดจำกัดการตอบสนองของ fetch ต้องเป็นจำนวนบวก stdlib.config.command_output_limit_positive = ขีดจำกัดการเก็บผลลัพธ์ของคำสั่งต้องเป็นจำนวนบวก stdlib.config.command_stream_limit_positive = ขีดจำกัดสายข้อมูลของคำสั่งต้องเป็นจำนวนบวก @@ -318,6 +319,8 @@ stdlib.path.with_suffix.empty_separator = with_suffix ต้องมีตั stdlib.path.relative_to.mismatch = { $path } ไม่ได้สัมพัทธ์กับ { $root } stdlib.path.expanduser.unsupported = ไม่รองรับการขยาย ~ สำหรับผู้ใช้รายใดรายหนึ่ง stdlib.path.expanduser.no_home = ขยาย ~ ไม่ได้: ไม่มีการตั้งค่าตัวแปรสภาพแวดล้อมของไดเรกทอรีบ้าน +stdlib.path.contents.file_too_large = File '{ $path }' exceeded the read limit of { $limit } bytes. +stdlib.path.contents.not_regular_file = File '{ $path }' is not a regular file. stdlib.path.contents.unsupported_encoding = ไม่รองรับการเข้ารหัส “{ $encoding }” stdlib.path.hash.unsupported_algorithm = ไม่รองรับขั้นตอนวิธีแฮช “{ $algorithm }” stdlib.path.hash.unsupported_algorithm_legacy = ไม่รองรับขั้นตอนวิธีแฮช “{ $algorithm }” (โปรดเปิดใช้คุณลักษณะ “{ $feature }”) diff --git a/locales/tr/messages.ftl b/locales/tr/messages.ftl index 08da86883..2a927a28b 100644 --- a/locales/tr/messages.ftl +++ b/locales/tr/messages.ftl @@ -210,6 +210,7 @@ network_policy.host.not_allowlisted = "{ $host }" makinesi izin verilenler liste stdlib.config.default_fetch_cache_invalid = Varsayılan fetch önbellek yolu göreli olmalıdır. stdlib.config.default_which_cache_invalid = Varsayılan which önbellek kapasitesi pozitif olmalıdır. stdlib.config.workspace_root_absolute = Çalışma alanının kök yolu mutlak olmalıdır. +stdlib.config.file_read_limit_positive = File read limit must be positive. stdlib.config.fetch_response_limit_positive = fetch yanıt sınırı pozitif olmalıdır. stdlib.config.command_output_limit_positive = Komut çıktısı yakalama sınırı pozitif olmalıdır. stdlib.config.command_stream_limit_positive = Komut akış sınırı pozitif olmalıdır. @@ -318,6 +319,8 @@ stdlib.path.with_suffix.empty_separator = with_suffix boş olmayan bir ayırıc stdlib.path.relative_to.mismatch = { $path }, { $root } konumuna göreli değil. stdlib.path.expanduser.unsupported = ~ işaretinin belirli bir kullanıcı için genişletilmesi desteklenmiyor. stdlib.path.expanduser.no_home = ~ genişletilemiyor: ev dizinine ilişkin hiçbir ortam değişkeni ayarlı değil. +stdlib.path.contents.file_too_large = File '{ $path }' exceeded the read limit of { $limit } bytes. +stdlib.path.contents.not_regular_file = File '{ $path }' is not a regular file. stdlib.path.contents.unsupported_encoding = Desteklenmeyen kodlama: "{ $encoding }". stdlib.path.hash.unsupported_algorithm = Desteklenmeyen özet algoritması: "{ $algorithm }". stdlib.path.hash.unsupported_algorithm_legacy = Desteklenmeyen özet algoritması: "{ $algorithm }" ("{ $feature }" özelliğini etkinleştirin). diff --git a/locales/uk/messages.ftl b/locales/uk/messages.ftl index 53f757319..0c41cd456 100644 --- a/locales/uk/messages.ftl +++ b/locales/uk/messages.ftl @@ -210,6 +210,7 @@ network_policy.host.not_allowlisted = Вузла «{ $host }» немає в п stdlib.config.default_fetch_cache_invalid = Типовий шлях кешу fetch має бути відносним. stdlib.config.default_which_cache_invalid = Типова місткість кешу which має бути додатною. stdlib.config.workspace_root_absolute = Кореневий шлях робочої області має бути абсолютним. +stdlib.config.file_read_limit_positive = File read limit must be positive. stdlib.config.fetch_response_limit_positive = Обмеження на відповідь fetch має бути додатним. stdlib.config.command_output_limit_positive = Обмеження на перехоплений вивід команд має бути додатним. stdlib.config.command_stream_limit_positive = Обмеження на потік команд має бути додатним. @@ -318,6 +319,8 @@ stdlib.path.with_suffix.empty_separator = with_suffix потребує непо stdlib.path.relative_to.mismatch = { $path } не є відносним до { $root }. stdlib.path.expanduser.unsupported = Розкриття ~ для конкретного користувача не підтримується. stdlib.path.expanduser.no_home = Не вдається розкрити ~: не задано жодної змінної середовища домашнього каталогу. +stdlib.path.contents.file_too_large = File '{ $path }' exceeded the read limit of { $limit } bytes. +stdlib.path.contents.not_regular_file = File '{ $path }' is not a regular file. stdlib.path.contents.unsupported_encoding = Непідтримуване кодування «{ $encoding }». stdlib.path.hash.unsupported_algorithm = Непідтримуваний алгоритм хешування «{ $algorithm }». stdlib.path.hash.unsupported_algorithm_legacy = Непідтримуваний алгоритм хешування «{ $algorithm }» (увімкніть можливість «{ $feature }»). diff --git a/locales/vi/messages.ftl b/locales/vi/messages.ftl index abe3979b9..6b91d566e 100644 --- a/locales/vi/messages.ftl +++ b/locales/vi/messages.ftl @@ -210,6 +210,7 @@ network_policy.host.not_allowlisted = Máy chủ “{ $host }” không nằm tr stdlib.config.default_fetch_cache_invalid = Đường dẫn bộ nhớ đệm fetch mặc định phải là tương đối. stdlib.config.default_which_cache_invalid = Dung lượng bộ nhớ đệm which mặc định phải là số dương. stdlib.config.workspace_root_absolute = Đường dẫn gốc của không gian làm việc phải là tuyệt đối. +stdlib.config.file_read_limit_positive = File read limit must be positive. stdlib.config.fetch_response_limit_positive = Giới hạn phản hồi của fetch phải là số dương. stdlib.config.command_output_limit_positive = Giới hạn thu nhận đầu ra lệnh phải là số dương. stdlib.config.command_stream_limit_positive = Giới hạn luồng lệnh phải là số dương. @@ -318,6 +319,8 @@ stdlib.path.with_suffix.empty_separator = with_suffix cần một dấu phân t stdlib.path.relative_to.mismatch = { $path } không tương đối so với { $root }. stdlib.path.expanduser.unsupported = Không hỗ trợ mở rộng ~ cho một người dùng cụ thể. stdlib.path.expanduser.no_home = Không mở rộng được ~: chưa đặt biến môi trường nào cho thư mục cá nhân. +stdlib.path.contents.file_too_large = File '{ $path }' exceeded the read limit of { $limit } bytes. +stdlib.path.contents.not_regular_file = File '{ $path }' is not a regular file. stdlib.path.contents.unsupported_encoding = Bảng mã không được hỗ trợ: “{ $encoding }”. stdlib.path.hash.unsupported_algorithm = Thuật toán băm không được hỗ trợ: “{ $algorithm }”. stdlib.path.hash.unsupported_algorithm_legacy = Thuật toán băm không được hỗ trợ: “{ $algorithm }” (hãy bật tính năng “{ $feature }”). diff --git a/locales/zh-Hans/messages.ftl b/locales/zh-Hans/messages.ftl index d3ffc1c6e..8b3ca10be 100644 --- a/locales/zh-Hans/messages.ftl +++ b/locales/zh-Hans/messages.ftl @@ -209,6 +209,7 @@ network_policy.host.not_allowlisted = 主机“{ $host }”不在允许列表中 stdlib.config.default_fetch_cache_invalid = fetch 缓存的默认路径必须是相对路径。 stdlib.config.default_which_cache_invalid = which 缓存的默认容量必须为正数。 stdlib.config.workspace_root_absolute = 工作区根路径必须是绝对路径。 +stdlib.config.file_read_limit_positive = File read limit must be positive. stdlib.config.fetch_response_limit_positive = fetch 的响应上限必须为正数。 stdlib.config.command_output_limit_positive = 命令输出的捕获上限必须为正数。 stdlib.config.command_stream_limit_positive = 命令的流式上限必须为正数。 @@ -317,6 +318,8 @@ stdlib.path.with_suffix.empty_separator = with_suffix 需要非空的分隔符 stdlib.path.relative_to.mismatch = { $path } 不是相对于 { $root } 的路径。 stdlib.path.expanduser.unsupported = 不支持针对特定用户展开 ~。 stdlib.path.expanduser.no_home = 无法展开 ~:未设置任何主目录环境变量。 +stdlib.path.contents.file_too_large = File '{ $path }' exceeded the read limit of { $limit } bytes. +stdlib.path.contents.not_regular_file = File '{ $path }' is not a regular file. stdlib.path.contents.unsupported_encoding = 不支持的编码“{ $encoding }”。 stdlib.path.hash.unsupported_algorithm = 不支持的散列算法“{ $algorithm }”。 stdlib.path.hash.unsupported_algorithm_legacy = 不支持的散列算法“{ $algorithm }”(请启用特性“{ $feature }”)。 diff --git a/locales/zh-Hant/messages.ftl b/locales/zh-Hant/messages.ftl index 9ff6d61d2..79887dd2c 100644 --- a/locales/zh-Hant/messages.ftl +++ b/locales/zh-Hant/messages.ftl @@ -209,6 +209,7 @@ network_policy.host.not_allowlisted = 主機「{ $host }」不在允許清單中 stdlib.config.default_fetch_cache_invalid = fetch 快取的預設路徑必須是相對路徑。 stdlib.config.default_which_cache_invalid = which 快取的預設容量必須為正數。 stdlib.config.workspace_root_absolute = 工作區的根路徑必須是絕對路徑。 +stdlib.config.file_read_limit_positive = File read limit must be positive. stdlib.config.fetch_response_limit_positive = fetch 的回應上限必須為正數。 stdlib.config.command_output_limit_positive = 命令輸出的擷取上限必須為正數。 stdlib.config.command_stream_limit_positive = 命令的串流上限必須為正數。 @@ -317,6 +318,8 @@ stdlib.path.with_suffix.empty_separator = with_suffix 需要非空的分隔字 stdlib.path.relative_to.mismatch = { $path } 不是相對於 { $root } 的路徑。 stdlib.path.expanduser.unsupported = 不支援針對特定使用者展開 ~。 stdlib.path.expanduser.no_home = 無法展開 ~:未設定任何家目錄環境變數。 +stdlib.path.contents.file_too_large = File '{ $path }' exceeded the read limit of { $limit } bytes. +stdlib.path.contents.not_regular_file = File '{ $path }' is not a regular file. stdlib.path.contents.unsupported_encoding = 不支援的編碼「{ $encoding }」。 stdlib.path.hash.unsupported_algorithm = 不支援的雜湊演算法「{ $algorithm }」。 stdlib.path.hash.unsupported_algorithm_legacy = 不支援的雜湊演算法「{ $algorithm }」(請啟用特性「{ $feature }」)。 diff --git a/src/localization/keys.rs b/src/localization/keys.rs index 72af08d79..58460b6d7 100644 --- a/src/localization/keys.rs +++ b/src/localization/keys.rs @@ -183,6 +183,7 @@ define_keys! { STDLIB_DEFAULT_WHICH_CACHE_INVALID => "stdlib.config.default_which_cache_invalid", STDLIB_WORKSPACE_ROOT_ABSOLUTE => "stdlib.config.workspace_root_absolute", STDLIB_FETCH_RESPONSE_LIMIT_POSITIVE => "stdlib.config.fetch_response_limit_positive", + STDLIB_FILE_READ_LIMIT_POSITIVE => "stdlib.config.file_read_limit_positive", STDLIB_COMMAND_OUTPUT_LIMIT_POSITIVE => "stdlib.config.command_output_limit_positive", STDLIB_COMMAND_STREAM_LIMIT_POSITIVE => "stdlib.config.command_stream_limit_positive", STDLIB_WHICH_CACHE_CAPACITY_POSITIVE => "stdlib.config.which_cache_capacity_positive", @@ -285,6 +286,8 @@ define_keys! { STDLIB_PATH_EXPANDUSER_UNSUPPORTED => "stdlib.path.expanduser.unsupported", STDLIB_PATH_EXPANDUSER_NO_HOME => "stdlib.path.expanduser.no_home", STDLIB_PATH_UNSUPPORTED_ENCODING => "stdlib.path.contents.unsupported_encoding", + STDLIB_PATH_FILE_TOO_LARGE => "stdlib.path.contents.file_too_large", + STDLIB_PATH_NOT_REGULAR_FILE => "stdlib.path.contents.not_regular_file", STDLIB_PATH_HASH_UNSUPPORTED_ALGORITHM => "stdlib.path.hash.unsupported_algorithm", STDLIB_PATH_HASH_UNSUPPORTED_ALGORITHM_LEGACY => "stdlib.path.hash.unsupported_algorithm_legacy", STDLIB_COLLECTIONS_FLATTEN_EXPECTED_SEQUENCE => "stdlib.collections.flatten.expected_sequence", diff --git a/src/stdlib/config/mod.rs b/src/stdlib/config/mod.rs index c0064ab4c..7d1fbb573 100644 --- a/src/stdlib/config/mod.rs +++ b/src/stdlib/config/mod.rs @@ -6,8 +6,8 @@ mod which; use super::config_types::HomeDirectory; pub use super::config_types::{ DEFAULT_COMMAND_MAX_OUTPUT_BYTES, DEFAULT_COMMAND_MAX_STREAM_BYTES, DEFAULT_COMMAND_TEMP_DIR, - DEFAULT_FETCH_CACHE_DIR, DEFAULT_FETCH_MAX_RESPONSE_BYTES, DEFAULT_WHICH_CACHE_CAPACITY, - NetworkConfig, + DEFAULT_FETCH_CACHE_DIR, DEFAULT_FETCH_MAX_RESPONSE_BYTES, DEFAULT_FILE_MAX_READ_BYTES, + DEFAULT_WHICH_CACHE_CAPACITY, FileConfig, NetworkConfig, }; use super::{command, network::NetworkPolicy, which::WORKSPACE_SKIP_DIRS}; use crate::localization::{self, keys}; @@ -29,6 +29,8 @@ pub struct StdlibConfig { network_policy: NetworkPolicy, /// Maximum size (in bytes) of HTTP responses fetched by network helpers. fetch_max_response_bytes: u64, + /// Maximum size (in bytes) read by the file-reading path filters. + file_max_read_bytes: u64, /// Maximum captured stdout size (in bytes) for command helpers. command_max_output_bytes: u64, /// Maximum streamed stdout size (in bytes) for command helpers. @@ -79,6 +81,7 @@ impl StdlibConfig { fetch_cache_relative: default, network_policy: NetworkPolicy::default(), fetch_max_response_bytes: DEFAULT_FETCH_MAX_RESPONSE_BYTES, + file_max_read_bytes: DEFAULT_FILE_MAX_READ_BYTES, command_max_output_bytes: DEFAULT_COMMAND_MAX_OUTPUT_BYTES, command_max_stream_bytes: DEFAULT_COMMAND_MAX_STREAM_BYTES, which_cache_capacity, @@ -164,6 +167,31 @@ impl StdlibConfig { Ok(self) } + /// Override the maximum size in bytes the file-reading filters may read. + /// + /// This budget bounds the `contents`, `linecount`, `hash`, and `digest` + /// filters so a manifest cannot exhaust memory or CPU through an + /// unexpectedly large input. Raise it for builds that legitimately hash + /// large artefacts. + /// + /// # Errors + /// + /// Returns an error when `max_bytes` is zero. + pub fn with_file_max_read_bytes(mut self, max_bytes: u64) -> anyhow::Result { + ensure!( + max_bytes > 0, + "{}", + localization::message(keys::STDLIB_FILE_READ_LIMIT_POSITIVE) + ); + self.file_max_read_bytes = max_bytes; + Ok(self) + } + + /// The configured maximum size in bytes for file-reading filters. + pub(crate) const fn file_max_read_bytes(&self) -> u64 { + self.file_max_read_bytes + } + /// Override the maximum streamed stdout size for stdlib command helpers. /// /// # Errors @@ -253,13 +281,14 @@ impl StdlibConfig { } /// Consume the configuration and expose component modules with owned state. - pub(crate) fn into_components(self) -> (NetworkConfig, command::CommandConfig) { + pub(crate) fn into_components(self) -> (NetworkConfig, FileConfig, command::CommandConfig) { let Self { workspace_root, workspace_root_path, fetch_cache_relative, network_policy, fetch_max_response_bytes, + file_max_read_bytes, command_max_output_bytes, command_max_stream_bytes, command_path_override, @@ -267,6 +296,9 @@ impl StdlibConfig { } = self; let command_root = Arc::clone(&workspace_root); + let files = FileConfig { + max_read_bytes: file_max_read_bytes, + }; let network = NetworkConfig { cache_root: workspace_root, cache_relative: fetch_cache_relative, @@ -282,7 +314,7 @@ impl StdlibConfig { command_path_override, }); - (network, command) + (network, files, command) } /// Validate that a cache path is a non-empty relative path which stays diff --git a/src/stdlib/config_tests.rs b/src/stdlib/config_tests.rs index 36b48be25..62c2fa34c 100644 --- a/src/stdlib/config_tests.rs +++ b/src/stdlib/config_tests.rs @@ -118,7 +118,7 @@ fn command_limits_propagate_into_components(base_config: Result) - .context("set capture limit")? .with_command_max_stream_bytes(131_072) .context("set streaming limit")?; - let (_network, command) = config.into_components(); + let (_network, _files, command) = config.into_components(); ensure!( command.max_capture_bytes == 4_096, "capture limit {} did not match 4096", diff --git a/src/stdlib/config_types.rs b/src/stdlib/config_types.rs index 50ad2201d..15945a650 100644 --- a/src/stdlib/config_types.rs +++ b/src/stdlib/config_types.rs @@ -9,8 +9,13 @@ use super::network::NetworkPolicy; /// Default relative path for the fetch cache within the workspace. pub const DEFAULT_FETCH_CACHE_DIR: &str = ".netsuke/fetch"; -/// Default upper bound for network helper responses (8 MiB). +/// Default upper bound for network helper responses (8 MiB). pub const DEFAULT_FETCH_MAX_RESPONSE_BYTES: u64 = 8 * 1024 * 1024; +/// Default upper bound for file-reading filters such as `contents` (8 MiB). +/// +/// Operators can raise this limit through `StdlibConfig` when legitimate +/// manifests need to hash or read larger artefacts. +pub const DEFAULT_FILE_MAX_READ_BYTES: u64 = 8 * 1024 * 1024; /// Default upper bound for captured command output (1 MiB). pub const DEFAULT_COMMAND_MAX_OUTPUT_BYTES: u64 = 1024 * 1024; /// Default upper bound for streamed command output files (64 MiB). @@ -43,3 +48,14 @@ pub struct NetworkConfig { /// Maximum allowed size for HTTP responses. pub max_response_bytes: u64, } + +/// Internal configuration passed to the path filters that read file contents. +#[derive(Clone, Copy)] +pub struct FileConfig { + /// Maximum allowed size (in bytes) for reads performed by the + /// `contents`, `linecount`, `hash`, and `digest` filters. + /// + /// Per-call `max_bytes` arguments can narrow this ceiling but never + /// raise it. + pub max_read_bytes: u64, +} diff --git a/src/stdlib/path/filters.rs b/src/stdlib/path/filters.rs index 069cac633..441620fdf 100644 --- a/src/stdlib/path/filters.rs +++ b/src/stdlib/path/filters.rs @@ -4,11 +4,12 @@ //! `relative_to`, `realpath`, `expanduser`, `size`, `contents`, //! `linecount`, `hash`, and `digest`. use camino::Utf8Path; -use minijinja::{Environment, Error, ErrorKind}; +use minijinja::{Environment, Error, ErrorKind, value::Kwargs}; use super::{fs_utils, hash_utils, path_utils}; use crate::localization::{self, keys}; use crate::stdlib::config_types::HomeDirectory; +use crate::stdlib::path::fs_utils::FileReadLimits; /// Register the `expanduser` filter. /// @@ -66,7 +67,11 @@ pub(crate) fn register_query_filters(env: &mut Environment<'_>) { } /// Register the file-inspecting path filters and the `expanduser` filter. -pub(crate) fn register_filters(env: &mut Environment<'_>, home_directory: HomeDirectory) { +pub(crate) fn register_filters( + env: &mut Environment<'_>, + home_directory: HomeDirectory, + file_max_read_bytes: u64, +) { register_lexical_filters(env); env.add_filter("realpath", |raw: String| -> Result { path_utils::canonicalize_any(Utf8Path::new(&raw)).map(camino::Utf8PathBuf::into_string) @@ -78,10 +83,14 @@ pub(crate) fn register_filters(env: &mut Environment<'_>, home_directory: HomeDi // Templates using `contents` read from the ambient file system; enable the stdlib only for trusted templates. env.add_filter( "contents", - |raw: String, encoding: Option| -> Result { + move |raw: String, encoding: Option, kwargs: Kwargs| -> Result { let chosen_encoding = encoding.unwrap_or_else(|| "utf-8".to_owned()); match chosen_encoding.to_ascii_lowercase().as_str() { - "utf-8" | "utf8" => fs_utils::read_utf8(Utf8Path::new(&raw)), + "utf-8" | "utf8" => { + let limits = path_call_limits(&kwargs, file_max_read_bytes)?; + kwargs.assert_all_used()?; + fs_utils::read_utf8(Utf8Path::new(&raw), &limits) + } other => Err(Error::new( ErrorKind::InvalidOperation, localization::message(keys::STDLIB_PATH_UNSUPPORTED_ENCODING) @@ -91,22 +100,55 @@ pub(crate) fn register_filters(env: &mut Environment<'_>, home_directory: HomeDi } }, ); - env.add_filter("linecount", |raw: String| -> Result { - fs_utils::linecount(Utf8Path::new(&raw)) - }); + env.add_filter( + "linecount", + move |raw: String, kwargs: Kwargs| -> Result { + let limits = path_call_limits(&kwargs, file_max_read_bytes)?; + kwargs.assert_all_used()?; + fs_utils::linecount(Utf8Path::new(&raw), &limits) + }, + ); env.add_filter( "hash", - |raw: String, alg: Option| -> Result { + move |raw: String, alg: Option, kwargs: Kwargs| -> Result { let algorithm = alg.unwrap_or_else(|| "sha256".to_owned()); - hash_utils::compute_hash(Utf8Path::new(&raw), &algorithm) + let limits = path_call_limits(&kwargs, file_max_read_bytes)?; + kwargs.assert_all_used()?; + hash_utils::compute_hash(Utf8Path::new(&raw), &algorithm, &limits) }, ); env.add_filter( "digest", - |raw: String, len: Option, alg: Option| -> Result { + move |raw: String, + len: Option, + alg: Option, + kwargs: Kwargs| + -> Result { let digest_len = len.unwrap_or(8); let algorithm = alg.unwrap_or_else(|| "sha256".to_owned()); - hash_utils::compute_digest(Utf8Path::new(&raw), digest_len, &algorithm) + let limits = path_call_limits(&kwargs, file_max_read_bytes)?; + kwargs.assert_all_used()?; + hash_utils::compute_digest(Utf8Path::new(&raw), digest_len, &algorithm, &limits) }, ); } + +/// Resolve the per-call read limits from `max_bytes` and `follow_symlinks` kwargs. +/// +/// `max_bytes` may only narrow the operator-configured ceiling: a call that +/// asks for more bytes than the configured budget is clamped to the budget +/// rather than granted a larger read. +fn path_call_limits( + kwargs: &Kwargs, + configured_max_read_bytes: u64, +) -> Result { + let max_bytes: Option = kwargs.get("max_bytes")?; + let follow_symlinks: Option = kwargs.get("follow_symlinks")?; + Ok(FileReadLimits { + max_bytes: match max_bytes { + Some(requested) if requested < configured_max_read_bytes => requested, + _ => configured_max_read_bytes, + }, + follow_symlinks: follow_symlinks.unwrap_or(false), + }) +} diff --git a/src/stdlib/path/fs_utils.rs b/src/stdlib/path/fs_utils.rs index c6d155216..e6500d4f4 100644 --- a/src/stdlib/path/fs_utils.rs +++ b/src/stdlib/path/fs_utils.rs @@ -1,13 +1,17 @@ //! UTF-8 file-system helpers for stdlib filters using cap-std Dir handles: metadata queries, //! opening files for streaming, and safe error translation. -use std::io; +use std::io::{self, BufRead, BufReader, Read}; use camino::{Utf8Path, Utf8PathBuf}; +#[cfg(unix)] +use cap_std::fs_utf8::OpenOptionsExt; use cap_std::{ ambient_authority, fs, fs_utf8::{Dir, File, OpenOptions}, }; use minijinja::Error; +#[cfg(unix)] +use rustix::fs::OFlags; use crate::localization::{self, keys}; @@ -24,6 +28,209 @@ pub(super) struct ParentDir { pub dir_path: Utf8PathBuf, } +/// Per-call limits for the file-reading filters. +#[derive(Clone, Copy, Debug)] +pub(crate) struct FileReadLimits { + /// Maximum number of bytes the read may consume. + pub max_bytes: u64, + /// Whether the final path component may be a symlink. + pub follow_symlinks: bool, +} + +/// Read a bounded chunk of `file`, rejecting reads that exceed `max_bytes`. +/// +/// Returns the bytes read so far, or `None` when the source has been +/// exhausted. Returns an error once the running total passes `max_bytes`. +pub(crate) fn read_bounded_chunk<'a>( + state: &mut BoundedRead, + file: &mut File, + buffer: &'a mut [u8], + path: &Utf8Path, +) -> Result, Error> { + let read = file.read(buffer).map_err(|err| { + io_to_error( + path, + &localization::message(keys::STDLIB_PATH_ACTION_READ), + err, + ) + })?; + if read == 0 { + return Ok(None); + } + state.total = state + .total + .saturating_add(u64::try_from(read).unwrap_or(u64::MAX)); + if state.total > state.max_bytes { + return Err(file_too_large_error(path, state.max_bytes)); + } + // `Read::read` cannot report more than the buffer holds, so index the + // slice defensively and treat an over-report as an empty chunk. + Ok(Some(buffer.get(..read).unwrap_or(&[]))) +} + +/// Running byte total for a bounded read against the configured ceiling. +pub(crate) struct BoundedRead { + /// Bytes consumed so far by this read. + total: u64, + /// The budget this read may not exceed. + max_bytes: u64, +} + +impl BoundedRead { + /// Start a bounded read with a fresh running total under `max_bytes`. + pub(crate) const fn new(max_bytes: u64) -> Self { + Self { + total: 0, + max_bytes, + } + } +} + +/// Build the localized byte-budget diagnostic for `path` and `limit`. +pub(crate) fn file_too_large_error(path: &Utf8Path, limit: u64) -> Error { + Error::new( + minijinja::ErrorKind::InvalidOperation, + localization::message(keys::STDLIB_PATH_FILE_TOO_LARGE) + .with_arg("path", path.as_str()) + .with_arg("limit", limit) + .to_string(), + ) +} + +/// Build the localized non-regular-file diagnostic for `path`. +pub(crate) fn not_regular_file_error(path: &Utf8Path) -> Error { + Error::new( + minijinja::ErrorKind::InvalidOperation, + localization::message(keys::STDLIB_PATH_NOT_REGULAR_FILE) + .with_arg("path", path.as_str()) + .to_string(), + ) +} + +/// Open `path` for reading under the file-reading safety policy. +/// +/// The final path component is opened without following symlinks unless +/// `limits.follow_symlinks` opts in, and the opened object must be a regular +/// file, checked on the opened handle so devices and FIFOs are rejected +/// race-free. +/// +/// # Errors +/// +/// Returns a template error when the parent directory cannot be opened, the +/// target cannot be opened, the final component is a symlink while following +/// is disabled, or the opened object is not a regular file. +pub(crate) fn open_file_checked(path: &Utf8Path, limits: &FileReadLimits) -> Result { + let parent = open_parent_dir(path)?; + let mut options = OpenOptions::new(); + options.read(true); + // Open non-blocking on Unix so a FIFO or device final component cannot + // wedge the render worker inside `open`; the flag is cleared once the + // opened object is confirmed to be a regular file. + #[cfg(unix)] + if !limits.follow_symlinks { + apply_unix_open_flags(&mut options, path)?; + } + #[cfg(windows)] + if !limits.follow_symlinks { + reject_windows_symlink(&parent, path)?; + } + let file = parent + .handle + .open_with(Utf8Path::new(&parent.entry), &options) + .map_err(|err| { + io_to_error( + path, + &localization::message(keys::STDLIB_PATH_ACTION_OPEN_FILE), + err, + ) + })?; + let metadata = file.metadata().map_err(|err| { + io_to_error( + path, + &localization::message(keys::STDLIB_PATH_ACTION_STAT), + err, + ) + })?; + if !metadata.is_file() { + return Err(not_regular_file_error(path)); + } + #[cfg(unix)] + if !limits.follow_symlinks { + restore_blocking(&file, path)?; + } + Ok(file) +} + +/// Set `O_NOFOLLOW | O_NONBLOCK` on the open options for a policy open. +/// +/// # Errors +/// +/// Returns a template error when the platform flag bits do not fit an `i32`. +#[cfg(unix)] +fn apply_unix_open_flags(options: &mut OpenOptions, path: &Utf8Path) -> Result<(), Error> { + let flags = i32::try_from((OFlags::NOFOLLOW | OFlags::NONBLOCK).bits()).map_err(|err| { + io_to_error( + path, + &localization::message(keys::STDLIB_PATH_ACTION_OPEN_FILE), + io::Error::new(io::ErrorKind::InvalidInput, err), + ) + })?; + options.custom_flags(flags); + Ok(()) +} + +/// Reject a symlink final component ahead of an open on Windows. +/// +/// Windows exposes no `O_NOFOLLOW` through cap-std, so the pre-open +/// `symlink_metadata` check is the platform's best available guard. +/// +/// # Errors +/// +/// Returns a template error when the metadata cannot be read or names a +/// symlink. +#[cfg(windows)] +fn reject_windows_symlink(parent: &ParentDir, path: &Utf8Path) -> Result<(), Error> { + let metadata = parent + .handle + .symlink_metadata(Utf8Path::new(&parent.entry)) + .map_err(|err| { + io_to_error( + path, + &localization::message(keys::STDLIB_PATH_ACTION_STAT), + err, + ) + })?; + if metadata.file_type().is_symlink() { + return Err(not_regular_file_error(path)); + } + Ok(()) +} + +/// Clear `O_NONBLOCK` from `file` after a non-blocking policy open. +/// +/// # Errors +/// +/// Returns a template error when the flag swap fails; the caller treats this +/// as an unreadable file rather than continuing with non-blocking semantics. +#[cfg(unix)] +fn restore_blocking(file: &File, path: &Utf8Path) -> Result<(), Error> { + let fd = std::os::fd::AsFd::as_fd(file); + let flags = rustix::fs::fcntl_getfl(fd).map_err(|err| { + io_to_error( + path, + &localization::message(keys::STDLIB_PATH_ACTION_OPEN_FILE), + io::Error::from(err), + ) + })?; + rustix::fs::fcntl_setfl(fd, flags & !OFlags::NONBLOCK).map_err(|err| { + io_to_error( + path, + &localization::message(keys::STDLIB_PATH_ACTION_OPEN_FILE), + io::Error::from(err), + ) + }) +} + /// Open a path's parent directory with ambient authority. /// /// # Errors @@ -119,10 +326,23 @@ pub(super) fn file_size(path: &Utf8Path) -> Result { /// # Errors /// /// Returns a template error when the parent directory cannot be opened, the -/// file cannot be read, or its contents are not valid UTF-8. -pub(super) fn read_utf8(path: &Utf8Path) -> Result { - with_parent_dir(path, keys::STDLIB_PATH_ACTION_READ, |handle, entry| { - handle.read_to_string(Utf8Path::new(entry)) +/// file cannot be read, its contents are not valid UTF-8, or the read exceeds +/// the configured byte budget. +pub(crate) fn read_utf8(path: &Utf8Path, limits: &FileReadLimits) -> Result { + let mut file = open_file_checked(path, limits)?; + let mut state = BoundedRead::new(limits.max_bytes); + let mut buffer = [0_u8; 8192]; + let mut bytes = Vec::new(); + while let Some(chunk) = read_bounded_chunk(&mut state, &mut file, &mut buffer, path)? { + bytes.extend_from_slice(chunk); + } + String::from_utf8(bytes).map_err(|_| { + Error::new( + minijinja::ErrorKind::InvalidOperation, + localization::message(keys::STDLIB_PATH_IO_INVALID_DATA) + .with_arg("path", path.as_str()) + .to_string(), + ) }) } @@ -130,22 +350,35 @@ pub(super) fn read_utf8(path: &Utf8Path) -> Result { /// /// # Errors /// -/// Returns a template error when the file cannot be opened or read as UTF-8. -pub(super) fn linecount(path: &Utf8Path) -> Result { - let content = read_utf8(path)?; - Ok(content.lines().count()) -} - -/// Open the file at `path` for reading through a capability handle. -/// -/// # Errors -/// -/// Returns a template error when the parent directory cannot be opened or the -/// target file cannot be opened for reading. -pub(crate) fn open_file(path: &Utf8Path) -> Result { - with_parent_dir(path, keys::STDLIB_PATH_ACTION_OPEN_FILE, |handle, entry| { - let mut options = OpenOptions::new(); - options.read(true); - handle.open_with(Utf8Path::new(entry), &options) - }) +/// Returns a template error when the file cannot be opened or read, or when +/// the read exceeds the configured byte budget. +pub(crate) fn linecount(path: &Utf8Path, limits: &FileReadLimits) -> Result { + let mut file = open_file_checked(path, limits)?; + let mut reader = BufReader::new(&mut file); + let mut lines: usize = 0; + let mut state = BoundedRead::new(limits.max_bytes); + let mut buffer = Vec::new(); + loop { + buffer.clear(); + let read = reader.read_until(b'\n', &mut buffer).map_err(|err| { + io_to_error( + path, + &localization::message(keys::STDLIB_PATH_ACTION_READ), + err, + ) + })?; + if read == 0 { + break; + } + state.total = state + .total + .saturating_add(u64::try_from(read).unwrap_or(u64::MAX)); + if state.total > state.max_bytes { + return Err(file_too_large_error(path, state.max_bytes)); + } + if !buffer.is_empty() { + lines += 1; + } + } + Ok(lines) } diff --git a/src/stdlib/path/hash_utils.rs b/src/stdlib/path/hash_utils.rs index c375aace0..4e4506881 100644 --- a/src/stdlib/path/hash_utils.rs +++ b/src/stdlib/path/hash_utils.rs @@ -3,8 +3,6 @@ //! Streams SHA-256 and SHA-512 digests via cap-std handles, //! enables SHA-1 and MD5 behind the `legacy-digests` feature, //! and always returns lowercase hexadecimal output. -use std::io::Read; - use camino::Utf8Path; use digest::Digest; #[cfg(feature = "legacy-digests")] @@ -14,10 +12,9 @@ use minijinja::{Error, ErrorKind}; use sha1::Sha1; use sha2::{Sha256, Sha512}; -use super::fs_utils; +use super::fs_utils::{self, FileReadLimits}; use crate::hex::to_lower_hex; use crate::localization::{self, keys}; -use crate::stdlib::io_helpers::io_to_error; /// Hash the file at `path` with the named algorithm, returning lowercase hex. /// @@ -25,15 +22,19 @@ use crate::stdlib::io_helpers::io_to_error; /// /// Returns an error when the algorithm is unsupported, is gated behind the /// `legacy-digests` feature when unavailable, or the file cannot be read. -pub(super) fn compute_hash(path: &Utf8Path, alg: &str) -> Result { +pub(super) fn compute_hash( + path: &Utf8Path, + alg: &str, + limits: &FileReadLimits, +) -> Result { if alg.eq_ignore_ascii_case("sha256") { - hash_stream::(path) + hash_stream::(path, limits) } else if alg.eq_ignore_ascii_case("sha512") { - hash_stream::(path) + hash_stream::(path, limits) } else if alg.eq_ignore_ascii_case("sha1") { #[cfg(feature = "legacy-digests")] { - hash_stream::(path) + hash_stream::(path, limits) } #[cfg(not(feature = "legacy-digests"))] { @@ -48,7 +49,7 @@ pub(super) fn compute_hash(path: &Utf8Path, alg: &str) -> Result } else if alg.eq_ignore_ascii_case("md5") { #[cfg(feature = "legacy-digests")] { - hash_stream::(path) + hash_stream::(path, limits) } #[cfg(not(feature = "legacy-digests"))] { @@ -76,8 +77,13 @@ pub(super) fn compute_hash(path: &Utf8Path, alg: &str) -> Result /// Returns an error when `alg` is unsupported, when a legacy algorithm is /// unavailable without the `legacy-digests` feature, or when the file cannot /// be opened or read. -pub(super) fn compute_digest(path: &Utf8Path, len: usize, alg: &str) -> Result { - let mut hash = compute_hash(path, alg)?; +pub(super) fn compute_digest( + path: &Utf8Path, + len: usize, + alg: &str, + limits: &FileReadLimits, +) -> Result { + let mut hash = compute_hash(path, alg, limits)?; if len < hash.len() { hash.truncate(len); } @@ -89,37 +95,19 @@ pub(super) fn compute_digest(path: &Utf8Path, len: usize, alg: &str) -> Result(path: &Utf8Path) -> Result +/// be read, or when the file exceeds the configured byte budget. +fn hash_stream(path: &Utf8Path, limits: &FileReadLimits) -> Result where H: Digest, { - let mut file = fs_utils::open_file(path)?; + let mut file = fs_utils::open_file_checked(path, limits)?; let mut hasher = H::new(); let mut buffer = [0_u8; 8192]; + let mut state = fs_utils::BoundedRead::new(limits.max_bytes); loop { - let read = file.read(&mut buffer).map_err(|err| { - io_to_error( - path, - &localization::message(keys::STDLIB_PATH_ACTION_READ), - err, - ) - })?; - if read == 0 { + let Some(chunk) = fs_utils::read_bounded_chunk(&mut state, &mut file, &mut buffer, path)? + else { break; - } - // A well-behaved Read never reports more bytes than the buffer holds; - // clamp to the full buffer rather than panicking on a misbehaving - // implementation, but surface the anomaly for diagnosis. - let chunk = if let Some(chunk) = buffer.get(..read) { - chunk - } else { - tracing::debug!( - read, - capacity = buffer.len(), - "Read reported more bytes than the buffer holds; clamping to full buffer" - ); - &buffer }; hasher.update(chunk); } @@ -143,6 +131,7 @@ mod tests { use sha2::{Digest, Sha256}; use tempfile::TempDir; + use super::super::fs_utils::FileReadLimits; use super::{compute_hash, to_lower_hex}; /// Name of the fixture file staged inside the temporary directory. @@ -182,7 +171,14 @@ mod tests { let payload = patterned(size); let (_dir, file) = fixture(&payload)?; - let streamed = compute_hash(&file, "sha256")?; + let streamed = compute_hash( + &file, + "sha256", + &FileReadLimits { + max_bytes: u64::MAX, + follow_symlinks: false, + }, + )?; let one_shot = to_lower_hex(&Sha256::digest(&payload)); ensure!( @@ -201,7 +197,14 @@ mod tests { "ba7816bf8f01cfea414140de5dae2223", "b00361a396177a9cb410ff61f20015ad", ); - let digest = compute_hash(&file, "sha256")?; + let digest = compute_hash( + &file, + "sha256", + &FileReadLimits { + max_bytes: u64::MAX, + follow_symlinks: false, + }, + )?; ensure!( digest == expected, "expected the published digest {expected} but streamed {digest}" @@ -222,6 +225,7 @@ mod tests { use proptest::prelude::*; use sha2::{Digest, Sha256}; + use super::super::fs_utils::FileReadLimits; use super::{compute_hash, fixture, to_lower_hex}; proptest! { @@ -235,7 +239,11 @@ mod tests { ) { let (_dir, file) = fixture(&payload).expect("stage the payload"); - let streamed = compute_hash(&file, "sha256").expect("hash the payload"); + let streamed = compute_hash( + &file, + "sha256", + &FileReadLimits { max_bytes: u64::MAX, follow_symlinks: false }, + ).expect("hash the payload"); let one_shot = to_lower_hex(&Sha256::digest(&payload)); prop_assert_eq!(streamed, one_shot); diff --git a/src/stdlib/register.rs b/src/stdlib/register.rs index 4f0f4acc6..4e4ebadce 100644 --- a/src/stdlib/register.rs +++ b/src/stdlib/register.rs @@ -107,8 +107,12 @@ pub fn register_with_config( register_read_only_helpers(env, &config); time::register_functions(env); let impure = state.impure_flag(); - let (network_config, command_config) = config.into_components(); + let (network_config, file_config, command_config) = config.into_components(); network::register_functions(env, Arc::clone(&impure), network_config); + tracing::debug!( + file_max_read_bytes = file_config.max_read_bytes, + "registered stdlib file-reading filters" + ); command::register(env, impure, command_config); Ok(state) } @@ -143,7 +147,11 @@ pub(crate) fn register_manifest_query(env: &mut Environment<'_>) -> StdlibState /// Register helpers that do not execute a command or make a network request. fn register_read_only_helpers(env: &mut Environment<'_>, config: &StdlibConfig) { register_file_tests(env); - path::register_filters(env, config.home_directory().clone()); + path::register_filters( + env, + config.home_directory().clone(), + config.file_max_read_bytes(), + ); collections::register_filters(env); let which_cache_capacity = config.which_cache_capacity(); let which_skip_dirs = WorkspaceSkipList::from_names(config.workspace_skip_dirs()); diff --git a/tests/bdd/steps/stdlib/workspace.rs b/tests/bdd/steps/stdlib/workspace.rs index cd569e8eb..597560fb2 100644 --- a/tests/bdd/steps/stdlib/workspace.rs +++ b/tests/bdd/steps/stdlib/workspace.rs @@ -25,7 +25,12 @@ const LINES_FIXTURE: &str = concat!("one\n", "two\n", "three\n",); /// Returns the workspace root path. If a workspace already exists (cached in /// `world.stdlib_root`), returns that path immediately. Otherwise, creates a /// new temporary directory with standard test fixtures (`file`, `lines.txt`, -/// and a symlink on Unix or fallback file on Windows). +/// and, on Unix, a real `link` symlink). +/// +/// The workspace never substitutes a regular file for `link`. Only the +/// Unix-only scenarios consume it, and a copy would invert what they prove: a +/// special-file fixture must create the requested file type or be absent. It +/// must not substitute a regular file. /// /// The workspace is cached in the `TestWorld` state, so subsequent calls /// within the same test scenario return the same directory. @@ -52,10 +57,6 @@ pub(crate) fn ensure_workspace(world: &TestWorld) -> Result { handle .symlink("file", "link") .context("create stdlib symlink fixture")?; - #[cfg(not(unix))] - handle - .write("link", b"data") - .context("write stdlib link fixture")?; world.temp_dir.set_value(temp); world.stdlib_root.set(root.clone()); Ok(root) diff --git a/tests/documentation_examples_tests.rs b/tests/documentation_examples_tests.rs index 675f76349..8deed5a49 100644 --- a/tests/documentation_examples_tests.rs +++ b/tests/documentation_examples_tests.rs @@ -27,6 +27,8 @@ const EXPECTED_EXAMPLE_IDS: &[&str] = &[ "guide-crates-io-install", "guide-direct-command-list", "guide-env-reader-snippet", + "guide-file-follow-symlinks-expression", + "guide-file-max-bytes-expression", "guide-first-build-commands", "guide-first-build-manifest", "guide-foreach-manifest", diff --git a/tests/std_filter_tests.rs b/tests/std_filter_tests.rs index 1236f590c..b66eca8a8 100644 --- a/tests/std_filter_tests.rs +++ b/tests/std_filter_tests.rs @@ -12,6 +12,8 @@ mod io_filters; mod network_functions; #[path = "std_filter_tests/path_filters.rs"] mod path_filters; +#[path = "std_filter_tests/read_policy_filters.rs"] +mod read_policy_filters; #[path = "std_filter_tests/support.rs"] mod support; #[path = "std_filter_tests/which_filter_common.rs"] diff --git a/tests/std_filter_tests/io_filters.rs b/tests/std_filter_tests/io_filters.rs index 4dcef312c..aa78518ff 100644 --- a/tests/std_filter_tests/io_filters.rs +++ b/tests/std_filter_tests/io_filters.rs @@ -1,13 +1,12 @@ //! Exercises standard library I/O filters to ensure they render file contents, //! line counts, and error paths correctly in end-to-end scenarios. +use super::support::fallible; use anyhow::{Context, Result, bail, ensure}; use cap_std::{ambient_authority, fs_utf8::Dir}; use minijinja::{ErrorKind, context}; use rstest::rstest; use test_support::fluent::normalize_fluent_isolates; -use super::support::fallible; - #[rstest] fn contents_and_linecount_filters() -> Result<()> { let (_temp, root) = fallible::filter_workspace()?; diff --git a/tests/std_filter_tests/path_filters.rs b/tests/std_filter_tests/path_filters.rs index 3b09dc525..b6a5d3637 100644 --- a/tests/std_filter_tests/path_filters.rs +++ b/tests/std_filter_tests/path_filters.rs @@ -237,7 +237,9 @@ fn relative_to_filter_outside_root() -> Result<()> { fn realpath_filter() -> Result<()> { let workspace = fallible::filter_workspace()?; with_filter_env(workspace, |root, env| { - let link = root.join("link"); + let Some(link) = fallible::file_symlink_fixture(root)? else { + return Ok(()); // No symlink support; the filter has no link to resolve. + }; let output = fallible::render(env, "realpath", "{{ path | realpath }}", &link) .context("render realpath filter")?; ensure!( diff --git a/tests/std_filter_tests/read_policy_filters.rs b/tests/std_filter_tests/read_policy_filters.rs new file mode 100644 index 000000000..194646a6f --- /dev/null +++ b/tests/std_filter_tests/read_policy_filters.rs @@ -0,0 +1,339 @@ +//! File-reading policy tests for the bounded `contents`, `linecount`, `hash`, +//! and `digest` filters. +//! +//! Covers the byte budget (exact boundary, one byte over, per-call narrowing +//! that can only clamp), the default no-follow regular-file open (symlinks and +//! FIFOs rejected), and the `follow_symlinks` opt-in. +use anyhow::{Context, Result, bail, ensure}; +use camino::Utf8Path; +use cap_std::{ambient_authority, fs_utf8::Dir}; +use minijinja::{ErrorKind, context}; +use rstest::rstest; +use test_support::fluent::normalize_fluent_isolates; + +#[cfg(unix)] +use rustix::fs::{Dev, FileType as RxFileType, Mode, mknodat}; + +use super::support::fallible; + +/// Inputs for one bounded-read render against a policy workspace. +#[derive(Clone, Copy)] +struct PolicyRender<'a> { + /// File-read budget configured on the stdlib environment. + limit: u64, + /// Template registration name. + name: &'a str, + /// Template source exercising a file-reading filter. + template: &'a str, + /// Workspace root the stdlib environment is bound to. + root: &'a camino::Utf8Path, + /// Path the template reads. + path: &'a camino::Utf8Path, +} + +/// Assert that `link` really is a symlink, failing setup when it is not. +/// +/// The metadata read does not follow the link, so a regular file sitting at the +/// same path cannot pass for one. A special-file policy test must create the +/// requested file type or skip because that file type is unavailable; it must +/// not substitute a regular file, which here would invert the assertion — the +/// filters would be expected to reject a perfectly ordinary file. +fn require_real_symlink(root: &Utf8Path, link: &Utf8Path) -> Result<()> { + let dir = Dir::open_ambient_dir(root, ambient_authority()) + .with_context(|| format!("open workspace root {root} to stat the symlink fixture"))?; + let name = link + .file_name() + .with_context(|| format!("symlink fixture {link} has no file name"))?; + let metadata = dir + .symlink_metadata(Utf8Path::new(name)) + .with_context(|| format!("stat symlink fixture {link}"))?; + ensure!( + metadata.file_type().is_symlink(), + "fixture {link} is not a symlink; the symlink policy cannot be exercised \ + without one, and substituting a regular file would invert the assertions" + ); + Ok(()) +} + +/// Render a bounded-read template, returning the raw result for assertions. +fn render_with_file_read_limit( + render: PolicyRender<'_>, +) -> Result> { + let mut env = fallible::stdlib_env_with_root_and_file_read_limit(render.root, render.limit)?; + fallible::register_template(&mut env, render.name, render.template)?; + let registered = env + .get_template(render.name) + .context("fetch policy template")?; + Ok(registered.render(context!(path => render.path.as_str()))) +} + +#[rstest] +fn contents_within_limit_renders_unchanged() -> Result<()> { + let (_temp, root) = fallible::filter_workspace()?; + let file = root.join("file"); + let rendered = render_with_file_read_limit(PolicyRender { + limit: 1024, + name: "contents_within", + template: "{{ path | contents }}", + root: &root, + path: &file, + })? + .context("render within limit")?; + ensure!( + rendered == "data", + "expected contents 'data' within the limit but rendered {rendered}" + ); + Ok(()) +} + +#[rstest] +fn contents_exactly_at_the_limit_renders() -> Result<()> { + let (_temp, root) = fallible::filter_workspace()?; + Dir::open_ambient_dir(&root, ambient_authority())? + .write("exact.bin", b"12345") + .context("write exact-limit fixture")?; + let file = root.join("exact.bin"); + let rendered = render_with_file_read_limit(PolicyRender { + limit: 5, + name: "contents_exact", + template: "{{ path | contents }}", + root: &root, + path: &file, + })? + .context("render at the limit")?; + ensure!( + rendered == "12345", + "expected a file exactly at the limit to render but got {rendered}" + ); + Ok(()) +} + +#[rstest] +fn contents_one_byte_over_the_limit_fails_with_the_limit() -> Result<()> { + let (_temp, root) = fallible::filter_workspace()?; + Dir::open_ambient_dir(&root, ambient_authority())? + .write("over.bin", b"123456") + .context("write over-limit fixture")?; + let file = root.join("over.bin"); + let result = render_with_file_read_limit(PolicyRender { + limit: 5, + name: "contents_over", + template: "{{ path | contents }}", + root: &root, + path: &file, + })?; + let err = match result { + Ok(output) => bail!("expected an over-limit read to fail but rendered {output}"), + Err(err) => err, + }; + ensure!( + err.kind() == ErrorKind::InvalidOperation, + "over-limit reads should report InvalidOperation but was {:?}", + err.kind() + ); + let message = normalize_fluent_isolates(&err.to_string()); + ensure!( + message.contains('5'), + "error should interpolate the limit: {message}" + ); + ensure!( + !message.contains("123456"), + "error must not disclose file contents: {message}" + ); + Ok(()) +} + +#[rstest] +fn linecount_enforces_the_budget_incrementally() -> Result<()> { + let (_temp, root) = fallible::filter_workspace()?; + let over = render_with_file_read_limit(PolicyRender { + limit: 8, + name: "linecount_over", + template: "{{ path | linecount }}", + root: &root, + path: &root.join("lines.txt"), + })?; + ensure!( + over.is_err(), + "a 14-byte file must fail an 8-byte budget: {over:?}" + ); + let message = normalize_fluent_isolates(&over.expect_err("over budget").to_string()); + ensure!(message.contains('8'), "limit should be quoted: {message}"); + Ok(()) +} + +#[rstest] +fn hash_and_digest_enforce_the_budget() -> Result<()> { + let (_temp, root) = fallible::filter_workspace()?; + let over = render_with_file_read_limit(PolicyRender { + limit: 2, + name: "hash_over", + template: "{{ path | hash('sha256') }}", + root: &root, + path: &root.join("file"), + })?; + ensure!( + over.is_err(), + "hashing a 4-byte file must fail a 2-byte budget" + ); + let message = normalize_fluent_isolates(&over.expect_err("over budget").to_string()); + ensure!(message.contains('2'), "limit should be quoted: {message}"); + + let within = render_with_file_read_limit(PolicyRender { + limit: 4, + name: "digest_within", + template: "{{ path | digest(8, 'sha256') }}", + root: &root, + path: &root.join("file"), + })? + .context("digest within the budget")?; + ensure!( + within == "3a6eb079", + "expected the known digest prefix but rendered {within}" + ); + Ok(()) +} + +#[rstest] +fn reading_filters_reject_symlinks_by_default() -> Result<()> { + let (_temp, root) = fallible::filter_workspace()?; + let Some(link) = fallible::file_symlink_fixture(&root)? else { + return Ok(()); // This host cannot create symlinks; nothing to police. + }; + require_real_symlink(&root, &link)?; + for (name, template) in [ + ("contents_symlink", "{{ path | contents }}"), + ("linecount_symlink", "{{ path | linecount }}"), + ("hash_symlink", "{{ path | hash }}"), + ("digest_symlink", "{{ path | digest(8, 'sha256') }}"), + ] { + let result = render_with_file_read_limit(PolicyRender { + limit: 1024, + name, + template, + root: &root, + path: &link, + })?; + let err = match result { + Ok(output) => bail!("expected {name} to reject a symlink but rendered {output}"), + Err(err) => err, + }; + ensure!( + err.kind() == ErrorKind::InvalidOperation, + "{name} should report InvalidOperation for a symlink but was {:?}", + err.kind() + ); + let message = normalize_fluent_isolates(&err.to_string()); + // O_NOFOLLOW surfaces either as our not-regular-file diagnostic or, + // through the platform, as an ELOOP-style open failure; both reject + // the link without reading it. + ensure!( + message.contains("not a regular file") || message.contains("symbolic links"), + "{name} error should explain the rejection: {message}" + ); + } + Ok(()) +} + +#[rstest] +fn follow_symlinks_opt_in_reads_the_link_target() -> Result<()> { + let (_temp, root) = fallible::filter_workspace()?; + let Some(link) = fallible::file_symlink_fixture(&root)? else { + return Ok(()); + }; + require_real_symlink(&root, &link)?; + let rendered = render_with_file_read_limit(PolicyRender { + limit: 1024, + name: "contents_follow", + template: "{{ path | contents(follow_symlinks=true) }}", + root: &root, + path: &link, + })? + .context("render with follow_symlinks")?; + ensure!( + rendered == "data", + "expected the opt-in to follow the link but rendered {rendered}" + ); + Ok(()) +} + +#[rstest] +fn per_call_max_bytes_narrows_the_budget() -> Result<()> { + let (_temp, root) = fallible::filter_workspace()?; + let result = render_with_file_read_limit(PolicyRender { + limit: 1024, + name: "contents_narrow", + template: "{{ path | contents(max_bytes=2) }}", + root: &root, + path: &root.join("file"), + })?; + ensure!( + result.is_err(), + "a per-call budget below the file size must fail: {result:?}" + ); + let message = normalize_fluent_isolates(&result.expect_err("narrowed").to_string()); + ensure!( + message.contains('2'), + "narrowed limit should be quoted: {message}" + ); + Ok(()) +} + +#[rstest] +fn per_call_max_bytes_cannot_raise_the_budget() -> Result<()> { + let (_temp, root) = fallible::filter_workspace()?; + Dir::open_ambient_dir(&root, ambient_authority())? + .write("big.bin", [b'x'; 16]) + .context("write 16-byte fixture")?; + let result = render_with_file_read_limit(PolicyRender { + limit: 4, + name: "contents_raise", + template: "{{ path | contents(max_bytes=4096) }}", + root: &root, + path: &root.join("big.bin"), + })?; + ensure!( + result.is_err(), + "a per-call budget above the configured ceiling must be clamped" + ); + let message = normalize_fluent_isolates(&result.expect_err("clamped").to_string()); + ensure!( + message.contains('4'), + "the configured ceiling should still apply: {message}" + ); + Ok(()) +} + +#[cfg(unix)] +#[rstest] +fn reading_filters_reject_a_fifo() -> Result<()> { + let (_temp, root) = fallible::filter_workspace()?; + let dir = Dir::open_ambient_dir(&root, ambient_authority())?; + mknodat( + &dir, + "pipe", + RxFileType::Fifo, + Mode::RUSR | Mode::WUSR, + Dev::default(), + ) + .map_err(|err| anyhow::anyhow!("create fifo fixture: {err}"))?; + drop(dir); + let pipe = root.join("pipe"); + let result = render_with_file_read_limit(PolicyRender { + limit: 1024, + name: "contents_fifo", + template: "{{ path | contents }}", + root: &root, + path: &pipe, + })?; + let err = match result { + Ok(output) => bail!("expected contents to reject a FIFO but rendered {output}"), + Err(err) => err, + }; + ensure!( + err.kind() == ErrorKind::InvalidOperation, + "contents should report InvalidOperation for a FIFO but was {:?}", + err.kind() + ); + Ok(()) +} diff --git a/tests/std_filter_tests/support.rs b/tests/std_filter_tests/support.rs index 9392b3a7d..e628235be 100644 --- a/tests/std_filter_tests/support.rs +++ b/tests/std_filter_tests/support.rs @@ -12,10 +12,18 @@ pub(crate) type Workspace = (tempfile::TempDir, Utf8PathBuf); pub(crate) mod fallible { //! Fallible fixture builders that preserve setup diagnostics for callers. + //! + //! Fixtures standing in for a special file type — a symlink, a FIFO, a + //! device — obey one invariant: a special-file policy test must create the + //! requested file type or skip because that file type is unavailable. It + //! must not substitute a regular file. A regular-file stand-in silently + //! changes what the test exercises, so the policy goes unverified while the + //! test still reports a verdict — and a fixture that hands back a regular + //! file where a symlink was requested inverts the assertion outright. use super::{Workspace, stdlib}; use anyhow::{Context, Result, anyhow}; - use camino::Utf8PathBuf; + use camino::{Utf8Path, Utf8PathBuf}; use cap_std::{ambient_authority, fs_utf8::Dir}; use minijinja::{Environment, context}; use netsuke::stdlib::{StdlibConfig, StdlibState}; @@ -41,6 +49,22 @@ pub(crate) mod fallible { Ok((env, state)) } + /// Builds a stdlib environment rooted at `root` whose file-read budget is + /// `limit` bytes. + pub(crate) fn stdlib_env_with_root_and_file_read_limit( + root: &camino::Utf8Path, + limit: u64, + ) -> Result> { + let dir = Dir::open_ambient_dir(root, ambient_authority()) + .context("open policy workspace root")?; + let (env, _) = stdlib_env_with_config( + StdlibConfig::new(dir)? + .with_workspace_root_path(root)? + .with_file_max_read_bytes(limit)?, + )?; + Ok(env) + } + pub(crate) fn stdlib_env_with_state() -> Result<(Environment<'static>, StdlibState)> { stdlib_env_with_config(StdlibConfig::from_current_dir()?) } @@ -67,6 +91,12 @@ pub(crate) mod fallible { stdlib_env_with_state().map(|(env, _)| env) } + /// Build a workspace holding the regular `file` fixture (contents `data`) + /// and the `lines.txt` fixture. + /// + /// No symlink is created here: the workspace deliberately holds only file + /// types every platform can provide. Callers needing a symlink ask + /// [`file_symlink_fixture`] for one and honour its availability result. pub(crate) fn filter_workspace() -> Result { let temp = tempdir().context("create standard filter workspace")?; let root = Utf8PathBuf::from_path_buf(temp.path().to_path_buf()) @@ -75,17 +105,69 @@ pub(crate) mod fallible { .context("open filter workspace directory")?; dir.write("file", b"data") .context("write fixture file 'file'")?; - #[cfg(unix)] - dir.symlink("file", "link") - .context("create fixture symlink")?; - #[cfg(not(unix))] - dir.write("link", b"data") - .context("create fixture link copy")?; dir.write("lines.txt", b"one\ntwo\nthree\n") .context("write fixture file 'lines.txt'")?; Ok((temp, root)) } + /// Create the workspace's real file symlink, `/link` -> `file`, and + /// report the link's path. + /// + /// `Ok(None)` means this host cannot provide a file symlink at all: either + /// the platform has no symlink support, or Windows refused for want of + /// `SeCreateSymbolicLinkPrivilege` and Developer Mode — the environmental + /// `ERROR_PRIVILEGE_NOT_HELD` condition. Every other failure is a genuine + /// setup fault and propagates. + /// + /// Callers skip their symlink-specific assertions on `Ok(None)`. They must + /// not fall back to a regular file: a special-file policy test must create + /// the requested file type or skip because that file type is unavailable. + /// It must not substitute a regular file. + /// + /// # Errors + /// + /// Returns the setup error when the platform can create symlinks but this + /// one was not created. + pub(crate) fn file_symlink_fixture(root: &Utf8Path) -> Result> { + #[cfg(unix)] + { + let dir = Dir::open_ambient_dir(root, ambient_authority()) + .context("open filter workspace for the symlink fixture")?; + dir.symlink("file", "link") + .context("create fixture symlink 'link' -> 'file'")?; + Ok(Some(root.join("link"))) + } + #[cfg(windows)] + { + let link = root.join("link"); + match std::os::windows::fs::symlink_file("file", &link) { + Ok(()) => Ok(Some(link)), + Err(err) if is_symlink_privilege_error(&err) => Ok(None), + Err(err) => Err(err).context("create fixture symlink 'link' -> 'file'"), + } + } + #[cfg(not(any(unix, windows)))] + { + let _ = root; + Ok(None) + } + } + + /// Whether `err` is Windows declining a symlink for want of + /// `SeCreateSymbolicLinkPrivilege` and Developer Mode. + /// + /// Only that documented environmental condition reports "symlink + /// unavailable"; callers surface anything else as a setup failure. Matching + /// the raw status rather than `PermissionDenied` keeps an ACL denial on the + /// workspace visible instead of silently skipping the symlink assertions. + #[cfg(windows)] + fn is_symlink_privilege_error(err: &std::io::Error) -> bool { + /// Status `CreateSymbolicLink` reports when neither the privilege nor + /// Developer Mode is available. + const ERROR_PRIVILEGE_NOT_HELD: i32 = 1314; + err.raw_os_error() == Some(ERROR_PRIVILEGE_NOT_HELD) + } + pub(crate) fn render<'a>( env: &mut Environment<'a>, name: &'a str,