diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index ea184fb914a4..117dbc09b2ee 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -9,6 +9,10 @@ on: pull_request_target: types: [closed, labeled] +concurrency: + group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + permissions: contents: read issues: write diff --git a/doc/release-notes/rl-2511.section.md b/doc/release-notes/rl-2511.section.md index 199da1475e87..992bbca17ca0 100644 --- a/doc/release-notes/rl-2511.section.md +++ b/doc/release-notes/rl-2511.section.md @@ -26,6 +26,8 @@ - New hardening flags, `strictflexarrays1` and `strictflexarrays3` were made available, corresponding to the gcc/clang options `-fstrict-flex-arrays=1` and `-fstrict-flex-arrays=3` respectively. +- `vmalert` now supports multiple instances with the option `services.vmalert.instances."".enable` + ## Nixpkgs Library {#sec-nixpkgs-release-25.11-lib} diff --git a/maintainers/team-list.nix b/maintainers/team-list.nix index 35aa999a58be..28f2f1007e40 100644 --- a/maintainers/team-list.nix +++ b/maintainers/team-list.nix @@ -915,6 +915,7 @@ with lib.maintainers; eljamm ethancedwards8 fricklerhandwerk + prince213 wegank ]; scope = "Maintain NGI-supported software."; diff --git a/nixos/doc/manual/release-notes/rl-2305.section.md b/nixos/doc/manual/release-notes/rl-2305.section.md index cae573c3452a..7ffe079977af 100644 --- a/nixos/doc/manual/release-notes/rl-2305.section.md +++ b/nixos/doc/manual/release-notes/rl-2305.section.md @@ -105,7 +105,7 @@ In addition to numerous new and updated packages, this release has the following - [ivpn](https://www.ivpn.net/), a secure, private VPN with fast WireGuard connections. Available as [services.ivpn](#opt-services.ivpn.enable). -- [vmalert](https://victoriametrics.com/), an alerting engine for VictoriaMetrics. Available as [services.vmalert](#opt-services.vmalert.enable). +- [vmalert](https://victoriametrics.com/), an alerting engine for VictoriaMetrics. Available as [services.vmalert.instances](#opt-services.vmalert.instances._name_.enable). - [jellyseerr](https://github.com/Fallenbagel/jellyseerr), a web-based requests manager for Jellyfin, forked from Overseerr. Available as [services.jellyseerr](#opt-services.jellyseerr.enable). diff --git a/nixos/modules/services/monitoring/vmalert.nix b/nixos/modules/services/monitoring/vmalert.nix index 438d067643fb..0baaa039e395 100644 --- a/nixos/modules/services/monitoring/vmalert.nix +++ b/nixos/modules/services/monitoring/vmalert.nix @@ -10,9 +10,9 @@ let format = pkgs.formats.yaml { }; - confOpts = concatStringsSep " \\\n" ( - mapAttrsToList mkLine (filterAttrs (_: v: v != false) cfg.settings) - ); + mkConfOpts = + settings: + concatStringsSep " \\\n" (mapAttrsToList mkLine (filterAttrs (_: v: v != false) settings)); confType = with types; let @@ -33,124 +33,171 @@ let concatMapStringsSep " " (v: "-${key}=${escapeShellArg (toString v)}") value else "-${key}=${escapeShellArg (toString value)}"; + + vmalertName = name: "vmalert" + lib.optionalString (name != "") ("-" + name); + enabledInstances = lib.filterAttrs (name: conf: conf.enable) config.services.vmalert.instances; in { + imports = [ + (lib.mkRenamedOptionModule + [ "services" "vmalert" "enable" ] + [ "services" "vmalert" "instances" "" "enable" ] + ) + (lib.mkRenamedOptionModule + [ "services" "vmalert" "rules" ] + [ "services" "vmalert" "instances" "" "rules" ] + ) + (lib.mkRenamedOptionModule + [ "services" "vmalert" "settings" ] + [ "services" "vmalert" "instances" "" "settings" ] + ) + ]; + # interface - options.services.vmalert = { - enable = lib.mkOption { - type = lib.types.bool; - default = false; - description = '' - Wether to enable VictoriaMetrics's `vmalert`. + options.services.vmalert.package = mkPackageOption pkgs "victoriametrics" { }; - `vmalert` evaluates alerting and recording rules against a data source, sends notifications via Alertmanager. - ''; - }; + options.services.vmalert.instances = mkOption { + default = { }; - package = mkPackageOption pkgs "victoriametrics" { }; + description = '' + Define multiple instances of vmalert. + ''; - settings = mkOption { - type = types.submodule { - freeformType = confType; - options = { + type = types.attrsOf ( + types.submodule ( + { name, config, ... }: + { + options = { + enable = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Wether to enable VictoriaMetrics's `vmalert`. - "datasource.url" = mkOption { - type = types.nonEmptyStr; - example = "http://localhost:8428"; - description = '' - Datasource compatible with Prometheus HTTP API. - ''; + `vmalert` evaluates alerting and recording rules against a data source, sends notifications via Alertmanager. + ''; + }; + + settings = mkOption { + type = types.submodule { + freeformType = confType; + options = { + + "datasource.url" = mkOption { + type = types.nonEmptyStr; + example = "http://localhost:8428"; + description = '' + Datasource compatible with Prometheus HTTP API. + ''; + }; + + "notifier.url" = mkOption { + type = with types; listOf nonEmptyStr; + default = [ ]; + example = [ "http://127.0.0.1:9093" ]; + description = '' + Prometheus Alertmanager URL. List all Alertmanager URLs if it runs in the cluster mode to ensure high availability. + ''; + }; + + "rule" = mkOption { + type = with types; listOf path; + description = '' + Path to the files with alerting and/or recording rules. + + ::: {.note} + Consider using the {option}`services.vmalert.instances..rules` option as a convenient alternative for declaring rules + directly in the `nix` language. + ::: + ''; + }; + + }; + }; + default = { }; + example = { + "datasource.url" = "http://localhost:8428"; + "datasource.disableKeepAlive" = true; + "datasource.showURL" = false; + "rule" = [ + "http:///path/to/rules" + "dir/*.yaml" + ]; + }; + description = '' + `vmalert` configuration, passed via command line flags. Refer to + + for details on supported values. + ''; + }; + + rules = mkOption { + type = format.type; + default = { }; + example = { + group = [ + { + name = "TestGroup"; + rules = [ + { + alert = "ExampleAlertAlwaysFiring"; + expr = '' + sum by(job) + (up == 1) + ''; + } + ]; + } + ]; + }; + description = '' + A list of the given alerting or recording rules against configured `"datasource.url"` compatible with + Prometheus HTTP API for `vmalert` to execute. Refer to + + for details on supported values. + ''; + }; }; - "notifier.url" = mkOption { - type = with types; listOf nonEmptyStr; - default = [ ]; - example = [ "http://127.0.0.1:9093" ]; - description = '' - Prometheus Alertmanager URL. List all Alertmanager URLs if it runs in the cluster mode to ensure high availability. - ''; - }; - - "rule" = mkOption { - type = with types; listOf path; - description = '' - Path to the files with alerting and/or recording rules. - - ::: {.note} - Consider using the {option}`services.vmalert.rules` option as a convenient alternative for declaring rules - directly in the `nix` language. - ::: - ''; - }; - - }; - }; - default = { }; - example = { - "datasource.url" = "http://localhost:8428"; - "datasource.disableKeepAlive" = true; - "datasource.showURL" = false; - "rule" = [ - "http:///path/to/rules" - "dir/*.yaml" - ]; - }; - description = '' - `vmalert` configuration, passed via command line flags. Refer to - - for details on supported values. - ''; - }; - - rules = mkOption { - type = format.type; - default = { }; - example = { - group = [ - { - name = "TestGroup"; - rules = [ - { - alert = "ExampleAlertAlwaysFiring"; - expr = '' - sum by(job) - (up == 1) - ''; - } + config = { + settings.rule = [ + "/etc/${vmalertName name}/rules.yml" ]; - } - ]; - }; - description = '' - A list of the given alerting or recording rules against configured `"datasource.url"` compatible with - Prometheus HTTP API for `vmalert` to execute. Refer to - - for details on supported values. - ''; - }; + }; + } + ) + ); }; # implementation - config = mkIf cfg.enable { + config = mkIf (enabledInstances != { }) { + environment.etc = lib.mapAttrs' ( + name: + { rules, ... }: + lib.nameValuePair "${vmalertName name}/rules.yml" { + source = format.generate "rules.yml" rules; + } + ) enabledInstances; - environment.etc."vmalert/rules.yml".source = format.generate "rules.yml" cfg.rules; + systemd.services = lib.mapAttrs' ( + name: + { settings, ... }: + let + name' = vmalertName name; + in + lib.nameValuePair name' { + description = "vmalert service"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + reloadTriggers = [ config.environment.etc."${name'}/rules.yml".source ]; - services.vmalert.settings.rule = [ - "/etc/vmalert/rules.yml" - ]; - - systemd.services.vmalert = { - description = "vmalert service"; - wantedBy = [ "multi-user.target" ]; - after = [ "network.target" ]; - reloadTriggers = [ config.environment.etc."vmalert/rules.yml".source ]; - - serviceConfig = { - DynamicUser = true; - Restart = "on-failure"; - ExecStart = "${cfg.package}/bin/vmalert ${confOpts}"; - ExecReload = ''${pkgs.coreutils}/bin/kill -SIGHUP "$MAINPID"''; - }; - }; + serviceConfig = { + DynamicUser = true; + Restart = "on-failure"; + ExecStart = "${cfg.package}/bin/vmalert ${mkConfOpts settings}"; + ExecReload = ''${pkgs.coreutils}/bin/kill -SIGHUP "$MAINPID"''; + }; + } + ) enabledInstances; }; } diff --git a/nixos/tests/victoriametrics/vmalert.nix b/nixos/tests/victoriametrics/vmalert.nix index f92c35a82010..aa5026cbd8f6 100644 --- a/nixos/tests/victoriametrics/vmalert.nix +++ b/nixos/tests/victoriametrics/vmalert.nix @@ -55,7 +55,7 @@ import ../make-test-python.nix ( }; }; - services.vmalert = { + services.vmalert.instances."" = { enable = true; settings = { "datasource.url" = "http://localhost:8428"; # victoriametrics' api diff --git a/pkgs/applications/editors/vim/plugins/generated.nix b/pkgs/applications/editors/vim/plugins/generated.nix index a0a7b4bbca53..470cb6f93dfa 100644 --- a/pkgs/applications/editors/vim/plugins/generated.nix +++ b/pkgs/applications/editors/vim/plugins/generated.nix @@ -15133,6 +15133,19 @@ final: prev: { meta.hydraPlatforms = [ ]; }; + ts-autotag-nvim = buildVimPlugin { + pname = "ts-autotag.nvim"; + version = "2025-01-15"; + src = fetchFromGitHub { + owner = "tronikelis"; + repo = "ts-autotag.nvim"; + rev = "0f1dc38fddd99b468ef58938d7cd99ce1d6bcb0e"; + sha256 = "1m0gng9v5dn6mm026nxw0n24bvysr156xhwvf83pv9xqp28xfwar"; + }; + meta.homepage = "https://github.com/tronikelis/ts-autotag.nvim/"; + meta.hydraPlatforms = [ ]; + }; + ts-comments-nvim = buildVimPlugin { pname = "ts-comments.nvim"; version = "2025-02-27"; diff --git a/pkgs/applications/editors/vim/plugins/vim-plugin-names b/pkgs/applications/editors/vim/plugins/vim-plugin-names index 46b6468a5c87..4a01c9bf25e7 100644 --- a/pkgs/applications/editors/vim/plugins/vim-plugin-names +++ b/pkgs/applications/editors/vim/plugins/vim-plugin-names @@ -1161,6 +1161,7 @@ https://github.com/simonmclean/triptych.nvim/,HEAD, https://github.com/folke/trouble.nvim/,, https://github.com/Pocco81/true-zen.nvim/,, https://github.com/tesaguri/trust.vim/,HEAD, +https://github.com/tronikelis/ts-autotag.nvim/,HEAD, https://github.com/folke/ts-comments.nvim/,HEAD, https://github.com/dmmulroy/tsc.nvim/,HEAD, https://github.com/jgdavey/tslime.vim/,, diff --git a/pkgs/applications/video/mpv/scripts/easycrop.nix b/pkgs/applications/video/mpv/scripts/easycrop.nix new file mode 100644 index 000000000000..1ed3d648cb2d --- /dev/null +++ b/pkgs/applications/video/mpv/scripts/easycrop.nix @@ -0,0 +1,44 @@ +{ + lib, + buildLua, + fetchFromGitHub, + unstableGitUpdater, +}: + +buildLua { + pname = "easycrop"; + version = "0-unstable-2018-01-24"; + + src = fetchFromGitHub { + owner = "aidanholm"; + repo = "mpv-easycrop"; + rev = "b8a67bb9039e19dec54d92ea57076c0c98e981aa"; + hash = "sha256-VRQP8j/Z/OvVqrEpvWcLmJFotxbTRynHoqvfIQIQmqY="; + }; + + scriptPath = "easycrop.lua"; + + passthru.updateScript = unstableGitUpdater { }; + + meta = { + description = "Manually crop videos during playback in mpv"; + longDescription = '' + A simple mpv script for manually cropping videos with ease. + + - Works during video playback + - No need to re-encode or modify video files + + Press "c" to begin cropping. Click at one corner of the desired + cropping rectangle, and click a second time at the opposite + corner; the video will be cropped immediately. Pressing "c" again + will undo the current crop. + + If you wish to use a key other than "c" to crop, the keybind + `easy_crop` can be changed. + ''; + homepage = "https://github.com/aidanholm/mpv-easycrop"; + license = lib.licenses.gpl3; + platforms = lib.platforms.all; + maintainers = with lib.maintainers; [ RossSmyth ]; + }; +} diff --git a/pkgs/by-name/bi/biz-ud-gothic/package.nix b/pkgs/by-name/bi/biz-ud-gothic/package.nix new file mode 100644 index 000000000000..17db66a647e5 --- /dev/null +++ b/pkgs/by-name/bi/biz-ud-gothic/package.nix @@ -0,0 +1,41 @@ +{ + lib, + stdenvNoCC, + fetchzip, + nix-update-script, +}: + +stdenvNoCC.mkDerivation (finalAttrs: { + pname = "biz-ud-gothic"; + version = "1.051"; + + src = fetchzip { + # Sticking with this assets file due to ongoing discussions. + # We may switch to a different asset once the issue is resolved or clarifications are provided. + # ref: https://github.com/googlefonts/morisawa-biz-ud-gothic/issues/47 + url = "https://github.com/googlefonts/morisawa-biz-ud-gothic/releases/download/v${finalAttrs.version}/morisawa-biz-ud-gothic-fonts.zip"; + hash = "sha256-7PlIrQX1fnFHXm7mjfoOCVp3GSnLT2GlVZdSoZbh/s4="; + }; + + installPhase = '' + runHook preInstall + + install -Dm444 fonts/ttf/*.ttf -t "$out/share/fonts/truetype/" + + runHook postInstall + ''; + + passthru = { + updateScript = nix-update-script { }; + }; + + meta = { + description = "Universal Design Japanese font"; + homepage = "https://github.com/googlefonts/morisawa-biz-ud-gothic"; + license = lib.licenses.ofl; + maintainers = with lib.maintainers; [ + kachick + ]; + platforms = lib.platforms.all; + }; +}) diff --git a/pkgs/by-name/ci/cie-middleware-linux/deps.json b/pkgs/by-name/ci/cie-middleware-linux/deps.json index a1554a15e26f..593cd2e1b767 100644 --- a/pkgs/by-name/ci/cie-middleware-linux/deps.json +++ b/pkgs/by-name/ci/cie-middleware-linux/deps.json @@ -14,28 +14,28 @@ "jar": "sha256-CV/R3HeIjAc/C+OaAYFW7lJnInmLCd6eKF7yE14W6sQ=", "pom": "sha256-NQkZQkMk4nUKPdwvobzmqQrIziklaYpgqbTR1uSSL/4=" }, - "com/diffplug/durian#durian-swt.os/4.2.2": { - "jar": "sha256-a1Mca0vlgaizLq2GHdwVwsk7IMZl+00z4DgUg8JERfQ=", - "module": "sha256-rVlQLGknZu48M0vkliigDctNka4aSPJjLitxUStDXPk=", - "pom": "sha256-GzxJFP1eLM4pZq1wdWY5ZBFFwdNCB3CTV4Py3yY2kIU=" + "com/diffplug/durian#durian-swt.os/4.3.0": { + "jar": "sha256-geK2Oafkvm3JtyRXE88G9cq1HynbLha5tXZFyW/eKIQ=", + "module": "sha256-IFNqlfL+sr9DBRKMaq7Lb9idxFeYqchfJgK4qAnXUNs=", + "pom": "sha256-Q1z/VXiZht7arXF/aPuo1UgklHhWLc2EsirU1lZvRAs=" }, - "com/diffplug/spotless#com.diffplug.spotless.gradle.plugin/6.25.0": { - "pom": "sha256-9FyCsS+qzYWs1HTrppkyL6XeqIQIskfQ5L3pQSkIIjo=" + "com/diffplug/spotless#com.diffplug.spotless.gradle.plugin/7.0.2": { + "pom": "sha256-7R3td6KWpv4hpQJ5ySbAe+FK98CMJDfTaFxw/Pa7oC0=" }, - "com/diffplug/spotless#spotless-lib-extra/2.45.0": { - "jar": "sha256-YCy7zTgo7pz7LjCn+bMDNcaScTB3FBTUzdKU0h/ly2c=", - "module": "sha256-9pnkNfTlzgPbYJpHaO6wNj1uB8ZfvPrx/GKcTnbuf7A=", - "pom": "sha256-5x2LkRDdSNLn9KVLi/uozlWpbmteu9T0OpJGZJz1b7A=" + "com/diffplug/spotless#spotless-lib-extra/3.0.2": { + "jar": "sha256-sOd3RtYz1EXnhImsPQitLqGzU3xNBk5KvkbMQtYjA+s=", + "module": "sha256-vSVeQkQbWRehU8U9z5fP08IEevN2zF3Yu1Z/aEAWtFk=", + "pom": "sha256-IVesGayscKzQRQH8WbvJZNsZD1tx5O1e/s6o5c9o7Os=" }, - "com/diffplug/spotless#spotless-lib/2.45.0": { - "jar": "sha256-sllply4dmAKAyirlKRl+2bMWCq5ItQbPGTXwG9Exhmc=", - "module": "sha256-+x+8+TUAczrHWcp99E8P9mVTEze0LaAS4on/CINNiQ8=", - "pom": "sha256-WKd8IsQLIc8m29tCEwFu9HrM9bBwchfHkyqQ9D+PMNw=" + "com/diffplug/spotless#spotless-lib/3.0.2": { + "jar": "sha256-P5p/38WwOsIIlINBcJEMFcTyuE7UzjZ3iYowetWJg3w=", + "module": "sha256-E1WLrsCR6gDxYmXNNSOBePT+ejv61zXel214XUF/ss0=", + "pom": "sha256-jxtFo4m6Jeel8DvZ8KS9BKp+dHXgku6C1VUJYrLPdV8=" }, - "com/diffplug/spotless#spotless-plugin-gradle/6.25.0": { - "jar": "sha256-9euQikxdpGKZ51Q/qtoEAtLEt31Yx7Qy1Lblk0mygKM=", - "module": "sha256-RoHRe/PJIF2DeOynBcAAywzJjcx40DATy2iJjGvSx0Q=", - "pom": "sha256-q1ZuPYS2w/rHqPySXy279TzZdZywOvPAfQ3EN9OXqNo=" + "com/diffplug/spotless#spotless-plugin-gradle/7.0.2": { + "jar": "sha256-WaNMT4SkjUyNkp4viZBjaeZUduwEmaQ96Hw+QSeXfNU=", + "module": "sha256-rxC8mydsNqlNcRh+kVhwJ1yyRVZTntzqGYpYL30Tsws=", + "pom": "sha256-JyVoPfbvTNSIr+sgANqJIpQcqQ513D49uFIupxWKaMQ=" }, "com/googlecode/concurrent-trees#concurrent-trees/2.6.1": { "jar": "sha256-BONySYTipcv1VgbPo3KlvT08XSohUzpwBOPN5Tl2H6U=", @@ -59,31 +59,31 @@ "module": "sha256-akesUDZOZZhFlAH7hvm2z832N7mzowRbHMM8v0xAghg=", "pom": "sha256-rrO3CiTBA+0MVFQfNfXFEdJ85gyuN2pZbX1lNpf4zJU=" }, - "commons-codec#commons-codec/1.16.0": { - "jar": "sha256-VllfsgsLhbyR0NUD2tULt/G5r8Du1d/6bLslkpAASE0=", - "pom": "sha256-bLWVeBnfOTlW/TEaOgw/XuwevEm6Wy0J8/ROYWf6PnQ=" + "commons-codec#commons-codec/1.17.0": { + "jar": "sha256-9wDegKwnDQNE/ep0aCAdi5yAXlxkgzHDYZ8u4GfM/Fk=", + "pom": "sha256-wBxM2l5Aj0HtHYPkoKFwz1OAG2M4q6SfD5BHhrwSFPw=" }, - "dev/equo/ide#solstice/1.7.5": { - "jar": "sha256-BuFLxDrMMx2ra16iAfxnNk7RI/mCyF+lEx8IF+1lrk8=", - "module": "sha256-eYp7cGdyE27iijLt2GOx6fgWE6NJhAXXS+ilyb6/9U8=", - "pom": "sha256-20U7urXn2opDE5sNzTuuZykzIfKcTZH1p5XZ/2xS3d8=" + "dev/equo/ide#solstice/1.8.1": { + "jar": "sha256-bluizOgTvh1xzNwuzz5JJxsU5pG/u7GhFM86MOdzsQ0=", + "module": "sha256-pnYDnqavCPJXtG4Hwr8VcaRqTUtbnMuGw/yY0H+v6hs=", + "pom": "sha256-arSo7K4qu9NrkZ0Lm5+yTBdxSPE+U2TJegxu4Ro/xCY=" }, - "org/apache#apache/29": { - "pom": "sha256-PkkDcXSCC70N9jQgqXclWIY5iVTCoGKR+mH3J6w1s3c=" + "org/apache#apache/31": { + "pom": "sha256-VV0MnqppwEKv+SSSe5OB6PgXQTbTVe6tRFIkRS5ikcw=" }, - "org/apache/commons#commons-parent/58": { - "pom": "sha256-LUsS4YiZBjq9fHUni1+pejcp2Ah4zuy2pA2UbpwNVZA=" + "org/apache/commons#commons-parent/69": { + "pom": "sha256-1Q2pw5vcqCPWGNG0oDtz8ZZJf8uGFv0NpyfIYjWSqbs=" }, - "org/eclipse/jgit#org.eclipse.jgit-parent/6.7.0.202309050840-r": { - "pom": "sha256-u56FQW2Y0HMfx2f41w6EaAQWAdZnKuItsqx5n3qjkR8=" + "org/eclipse/jgit#org.eclipse.jgit-parent/6.10.0.202406032230-r": { + "pom": "sha256-8tNTmgp5Iv15RwgsGQHSCQ2uB0mGsi2r2XO0OYzR6i4=" }, - "org/eclipse/jgit#org.eclipse.jgit/6.7.0.202309050840-r": { - "jar": "sha256-tWRHfQkiQaqrUMhKxd0aw3XAGCBE1+VlnTpgqQ4ugBo=", - "pom": "sha256-BNB83b8ZjfpuRIuan7lA94HAEq2T2eqCBv4KTTplwZI=" + "org/eclipse/jgit#org.eclipse.jgit/6.10.0.202406032230-r": { + "jar": "sha256-Q/kvOttoGl8wBrl56NNBwSqM/YAp8ofEK88KgDd1Za4=", + "pom": "sha256-BVlUQr62ogYQi2c6qcZpLIPkHfGDF33GcROxzD9Sgd0=" }, - "org/eclipse/platform#org.eclipse.osgi/3.18.300": { - "jar": "sha256-urlD5Y7dFzCSOGctunpFrsni2svd24GKjPF3I+oT+iI=", - "pom": "sha256-4nl2N1mZxUJ/y8//PzvCD77a+tiqRRArN59cL5fI/rQ=" + "org/eclipse/platform#org.eclipse.osgi/3.18.500": { + "jar": "sha256-gLJ11YN5cjspHqZQJJzDgJyPELNPeKr5iBMs1tQ0q04=", + "pom": "sha256-4o9b4Azk7Sx+SAnsrQW5UwfzWhflhWAHhri97juk2Wg=" }, "org/jetbrains#annotations/13.0": { "jar": "sha256-rOKhDcji1f00kl7KwD5JiLLA+FFlDJS4zvSbob0RFHg=", @@ -114,9 +114,9 @@ "jar": "sha256-VemJxRK4CQd5n4VDCfO8d4LFs9E5MkQtA3nVxHJxFQQ=", "pom": "sha256-fin79z/fceBnnT3ufmgP1XNGT6AWRKT1irgZ0sCI09I=" }, - "org/junit#junit-bom/5.9.3": { - "module": "sha256-tAH9JZAeWCpSSqU0PEs54ovFbiSWHBBpvytLv87ka5M=", - "pom": "sha256-TQMpzZ5y8kIOXKFXJMv+b/puX9KIg2FRYnEZD9w0Ltc=" + "org/junit#junit-bom/5.10.2": { + "module": "sha256-3iOxFLPkEZqP5usXvtWjhSgWaYus5nBxV51tkn67CAo=", + "pom": "sha256-Fp3ZBKSw9lIM/+ZYzGIpK/6fPBSpifqSEgckzeQ6mWg=" }, "org/slf4j#slf4j-api/1.7.36": { "jar": "sha256-0+9XXj5JeWeNwBvx3M5RAhSTtNEft/G+itmCh3wWocA=", @@ -137,31 +137,31 @@ } }, "https://repo.maven.apache.org/maven2": { - "com/google/code/gson#gson-parent/2.11.0": { - "pom": "sha256-issfO3Km8CaRasBzW62aqwKT1Sftt7NlMn3vE6k2e3o=" + "com/google/code/gson#gson-parent/2.12.1": { + "pom": "sha256-yeewt+Mb574iaEl5wGgAHGUssRPE5u2JTjm2Q97gf8E=" }, - "com/google/code/gson#gson/2.11.0": { - "jar": "sha256-V5KNblpu3rKr03cKj5W6RNzkXzsjt6ncKzCcWBVSp4s=", - "pom": "sha256-wOVHvqmYiI5uJcWIapDnYicryItSdTQ90sBd7Wyi42A=" + "com/google/code/gson#gson/2.12.1": { + "jar": "sha256-6+4T1ft0d81/HMAQ4MNW34yoBwlxUkjal/eeNcy0++w=", + "pom": "sha256-C1c17IX0UoLx4sdpd5gAQnsVCoFj9AUJOpKAtxyrGXg=" }, - "com/google/errorprone#error_prone_annotations/2.27.0": { - "jar": "sha256-JMkjNyxY410LnxagKJKbua7cd1IYZ8J08r0HNd9bofU=", - "pom": "sha256-TKWjXWEjXhZUmsNG0eNFUc3w/ifoSqV+A8vrJV6k5do=" + "com/google/errorprone#error_prone_annotations/2.36.0": { + "jar": "sha256-d0QOJwsLyaJJkDxaB2w2pyLEiGyk9CZ18pA6HFPtYaU=", + "pom": "sha256-15z9N8hfdta3VMdQHuHchEe3smQsI4LXeCUhZr0zHpw=" }, - "com/google/errorprone#error_prone_parent/2.27.0": { - "pom": "sha256-+oGCnQSVWd9pJ/nJpv1rvQn4tQ5tRzaucsgwC2w9dlQ=" + "com/google/errorprone#error_prone_parent/2.36.0": { + "pom": "sha256-Okz8imvtYetI6Wl5b8MeoNJwtj5nBZmUamGIOttwlNw=" }, "commons-io#commons-io/2.18.0": { "jar": "sha256-88oPjWPEDiOlbVQQHGDV7e4Ta0LYS/uFvHljCTEJz4s=", "pom": "sha256-Y9lpQetE35yQ0q2yrYw/aZwuBl5wcEXF2vcT/KUrz8o=" }, - "commons-logging#commons-logging/1.3.3": { - "jar": "sha256-WCj5bAnYhvmxoJk8eASyfPT87IU0UXFk9RN6yLZ+qbk=", - "pom": "sha256-El1hQurD93REC6cCwF8o+eCYxS0QcSrhFJast3EGs7o=" + "commons-logging#commons-logging/1.3.4": { + "jar": "sha256-vC3+MvHvBlCeagZRRMGt97Qg6r8RqH8wvRJ/j6ozIBY=", + "pom": "sha256-1L2jSJKqzL9PrTP8MjqKqQfvnXmYRlnaJJRMCoa/CGU=" }, - "net/java/dev/jna#jna/5.15.0": { - "jar": "sha256-pWQVjSirUSf8apWAKO1UJ5/gmZZixGQltqOwmipSCU0=", - "pom": "sha256-J2YC/zZ6TDkVXa7MHoy1T0eJ5dgN+Qo6i2YD8d61ngU=" + "net/java/dev/jna#jna/5.17.0": { + "jar": "sha256-s6lAjnxR4I7w47/MCPRD9uwPYZG6jNfBjVPSsi5b28A=", + "pom": "sha256-UBoP8F2EpK0Q9t4lvpT0k5i3CjG+jzoO2fTGtE++/uQ=" }, "org/apache#apache/32": { "pom": "sha256-z9hywOwn9Trmj0PbwP7N7YrddzB5pTr705DkB7Qs5y8=" @@ -169,26 +169,26 @@ "org/apache#apache/33": { "pom": "sha256-14vYUkxfg4ChkKZSVoZimpXf5RLfIRETg6bYwJI6RBU=" }, - "org/apache/commons#commons-parent/71": { - "pom": "sha256-lbe+cPMWrkyiL2+90I3iGC6HzYdKZQ3nw9M4anR6gqM=" + "org/apache/commons#commons-parent/72": { + "pom": "sha256-Q0Xev8dnsa6saKvdcvxn0YtSHUs5A3KhG2P/DFhrIyA=" }, "org/apache/commons#commons-parent/78": { "pom": "sha256-Ai0gLmVe3QTyoQ7L5FPZKXeSTTg4Ckyow1nxgXqAMg4=" }, - "org/apache/pdfbox#fontbox/3.0.3": { - "jar": "sha256-ZWkMPzmwShTRLBf0mYwVGGzod9Pi7CIscIV348wCgDA=", - "pom": "sha256-sik+UDqncEMGEVD4hGg9f2FMz1Fjvi0PBSyinzx2d+I=" + "org/apache/pdfbox#fontbox/3.0.4": { + "jar": "sha256-Le7GIy9dbTsxJ2WS0xaArpcir1fSTLD3bacOK6DpnhI=", + "pom": "sha256-8MzDMOmyDYlXigqHArR6OZvNYojCOG10/6uPjjAoROg=" }, - "org/apache/pdfbox#pdfbox-io/3.0.3": { - "jar": "sha256-Ej6jGHtJfFTmYdUMPIZ0eb93Zo/0UOUHEMZY8rtGh7o=", - "pom": "sha256-EXAbuMWnNaSzFA9zroM6KBzqCeO8P583kbmpenRekNQ=" + "org/apache/pdfbox#pdfbox-io/3.0.4": { + "jar": "sha256-ep1HRvLhOh4i9O/kf7r5mXY8rqQXSFNa+ToB7txQ9VQ=", + "pom": "sha256-YLURdX737TBIv4LH2ClvcdC8RwAgEYQzqzs+OEC/jqU=" }, - "org/apache/pdfbox#pdfbox-parent/3.0.3": { - "pom": "sha256-yGXhzv8Jq1Kwh+cmDE8V025bW4vk/+IERvqkCiygvcw=" + "org/apache/pdfbox#pdfbox-parent/3.0.4": { + "pom": "sha256-w5j++zUez/mTEYCNftU0bnHzVrETS9c6JQTLgXB9tFE=" }, - "org/apache/pdfbox#pdfbox/3.0.3": { - "jar": "sha256-W+ONLsgWkbBdU163IN5NxWbF0H5aBHMfoAZo0VOotKY=", - "pom": "sha256-vgiV9rLCDzEdYjXJam/SqsECsxkE0/TDnqUll3WwcAg=" + "org/apache/pdfbox#pdfbox/3.0.4": { + "jar": "sha256-CaD/J9b4Sh3EAGDLCgHezyrU75HDa8kbmDbCVL6KrkU=", + "pom": "sha256-098DLfK90jT+NwMO+sNXa6Yj/Jf1ERNUxMUq13dfkOQ=" }, "org/junit#junit-bom/5.11.0-M2": { "module": "sha256-hkd6vPSQ1soFmqmXPLEI0ipQb0nRpVabsyzGy/Q8LM4=", diff --git a/pkgs/by-name/ci/cie-middleware-linux/package.nix b/pkgs/by-name/ci/cie-middleware-linux/package.nix index 7647f20c69a7..0495b82c0693 100644 --- a/pkgs/by-name/ci/cie-middleware-linux/package.nix +++ b/pkgs/by-name/ci/cie-middleware-linux/package.nix @@ -21,13 +21,13 @@ let pname = "cie-middleware-linux"; - version = "1.5.6"; + version = "1.5.9"; src = fetchFromGitHub { owner = "M0rf30"; repo = "cie-middleware-linux"; rev = version; - sha256 = "sha256-2P/1hQTmeQ6qE7RgAeLOZTszcLcIpa2XX1S2ahXRHcc="; + hash = "sha256-2UMKxanF35oBNBtIqfU46QUYJwXiTU1xCrCMqzqetgI="; }; gradle = gradle_8; @@ -61,8 +61,6 @@ stdenv.mkDerivation { libxml2 ]; - patches = [ ./use-system-podofo.patch ]; - postPatch = '' # substitute the cieid command with this $out/bin/cieid substituteInPlace libs/pkcs11/src/CSP/AbilitaCIE.cpp \ @@ -114,6 +112,7 @@ stdenv.mkDerivation { popd # Install the Java application + ls cie-java/build/libs/CIEID-standalone.jar install -Dm755 cie-java/build/libs/CIEID-standalone.jar \ "$out/share/cieid/cieid.jar" @@ -123,11 +122,11 @@ stdenv.mkDerivation { --add-flags "-Djna.library.path='$out/lib:${libraries}'" \ --add-flags '-Dawt.useSystemAAFontSettings=on' \ --add-flags "-cp $out/share/cieid/cieid.jar" \ - --add-flags "it.ipzs.cieid.MainApplication" + --add-flags "app.m0rf30.cieid.MainApplication" # Install other files - install -Dm644 data/cieid.desktop "$out/share/applications/cieid.desktop" - install -Dm755 data/logo.png "$out/share/pixmaps/cieid.png" + install -Dm644 data/app.m0rf30.cieid.desktop -t "$out/share/applications" + install -Dm755 data/app.m0rf30.cieid.svg -t "$out/share/pixmaps" install -Dm644 LICENSE "$out/share/licenses/cieid/LICENSE" ''; @@ -146,8 +145,6 @@ stdenv.mkDerivation { ''; license = licenses.bsd3; platforms = platforms.unix; - # Note: fails due to a lot of broken type conversions - badPlatforms = platforms.darwin; maintainers = with maintainers; [ rnhmjoj ]; }; } diff --git a/pkgs/by-name/ci/cie-middleware-linux/use-system-podofo.patch b/pkgs/by-name/ci/cie-middleware-linux/use-system-podofo.patch deleted file mode 100644 index 52806d4f96e8..000000000000 --- a/pkgs/by-name/ci/cie-middleware-linux/use-system-podofo.patch +++ /dev/null @@ -1,343 +0,0 @@ -commit c9ac4243a6def08790bbf5552bb31894169596ca -Author: rnhmjoj -Date: Wed Apr 3 12:54:58 2024 +0200 - - use system podofo - -diff --git a/libs/meson.build b/libs/meson.build -index 3ee31c1..5022ba8 100644 ---- a/libs/meson.build -+++ b/libs/meson.build -@@ -16,21 +16,15 @@ curl_dep = dependency('libcurl') - fontconfig_dep = dependency('fontconfig') - freetype_dep = dependency('freetype2') - png_dep = dependency('libpng') --podofo_dep = cpp.find_library('libpodofo', dirs: libdir) -+podofo_dep = dependency('libpodofo') - libxml2_dep = dependency('libxml-2.0', required: false) - xml2_dep = dependency('xml2', required: false) - zlib_dep = dependency('zlib') - - inc_so = include_directories('pkcs11/src/.', 'shared/src/') - --inc_a = include_directories( -- 'sign-sdk/include', -- 'sign-sdk/include/podofo', -- 'sign-sdk/include/podofo/include', -- 'sign-sdk/include/podofo/include/podofo', -- 'sign-sdk/src', -- 'shared/src/', --) -+inc_a = include_directories('sign-sdk/include', 'sign-sdk/src', 'shared/src/') -+ - cie_pkcs11_sources = [ - 'shared/src/Util/log.cpp', - 'shared/src/Util/funccallinfo.cpp', -diff --git a/libs/sign-sdk/include/PdfSignatureGenerator.h b/libs/sign-sdk/include/PdfSignatureGenerator.h -index 93ab445..65d438f 100644 ---- a/libs/sign-sdk/include/PdfSignatureGenerator.h -+++ b/libs/sign-sdk/include/PdfSignatureGenerator.h -@@ -10,9 +10,7 @@ - #ifndef _PDFSIGNATUREGENERATOR_H_ - #define _PDFSIGNATUREGENERATOR_H_ - #include "Util/UUCByteArray.h" --#include "podofo/doc/PdfSignOutputDevice.h" --#include "podofo/doc/PdfSignatureField.h" --#include "podofo/podofo.h" -+#include - - using namespace PoDoFo; - using namespace std; -@@ -60,7 +58,11 @@ class PdfSignatureGenerator { - const double getHeight(int pageIndex); - - private: -- PdfMemDocument* m_pPdfDocument; -+ PdfDocument* m_pPdfDocument; -+ -+ PdfMemDocument* m_pPdfMemDocument; -+ -+ PdfWriter* m_pPdfWriter; - - PdfSignatureField* m_pSignatureField; - -diff --git a/libs/sign-sdk/src/PdfSignatureGenerator.cpp b/libs/sign-sdk/src/PdfSignatureGenerator.cpp -index 44ef54a..e8b8c8e 100644 ---- a/libs/sign-sdk/src/PdfSignatureGenerator.cpp -+++ b/libs/sign-sdk/src/PdfSignatureGenerator.cpp -@@ -27,7 +27,7 @@ int GetNumberOfSignatures(PdfMemDocument* pPdfDocument); - USE_LOG; - - PdfSignatureGenerator::PdfSignatureGenerator() -- : m_pPdfDocument(NULL), -+ : m_pPdfMemDocument(NULL), - m_pSignatureField(NULL), - m_pSignOutputDevice(NULL), - m_pFinalOutDevice(NULL), -@@ -37,7 +37,7 @@ PdfSignatureGenerator::PdfSignatureGenerator() - } - - PdfSignatureGenerator::~PdfSignatureGenerator() { -- if (m_pPdfDocument) delete m_pPdfDocument; -+ if (m_pPdfMemDocument) delete m_pPdfMemDocument; - - if (m_pSignatureField) delete m_pSignatureField; - -@@ -51,21 +51,21 @@ PdfSignatureGenerator::~PdfSignatureGenerator() { - } - - int PdfSignatureGenerator::Load(const char* pdf, int len) { -- if (m_pPdfDocument) delete m_pPdfDocument; -+ if (m_pPdfMemDocument) delete m_pPdfMemDocument; - - try { - printf("PDF LENGTH"); - printf("%i", len); - printf("STOP"); - -- m_pPdfDocument = new PdfMemDocument(); -- m_pPdfDocument->Load(pdf, len); -- printf("OK m_pPdfDocument"); -- int nSigns = PDFVerifier::GetNumberOfSignatures(m_pPdfDocument); -+ m_pPdfMemDocument = new PdfMemDocument(); -+ m_pPdfMemDocument->Load(pdf); -+ printf("OK m_pPdfMemDocument"); -+ int nSigns = PDFVerifier::GetNumberOfSignatures(m_pPdfMemDocument); - printf("OK nSigns: %d", nSigns); - - if (nSigns > 0) { -- m_pPdfDocument->SetIncrementalUpdates(true); -+ m_pPdfWriter->PdfWriter::SetIncrementalUpdate(true); - } - m_actualLen = len; - -@@ -82,14 +82,8 @@ void PdfSignatureGenerator::AddFont(const char* szFontName, - // printf(szFontName); - // printf(szFontPath); - -- m_pPdfDocument->CreateFont( -- szFontName, false, false, -- PdfEncodingFactory::GlobalWinAnsiEncodingInstance(), -- PdfFontCache::eFontCreationFlags_AutoSelectBase14, true, szFontPath); -- m_pPdfDocument->CreateFont( -- szFontName, true, false, -- PdfEncodingFactory::GlobalWinAnsiEncodingInstance(), -- PdfFontCache::eFontCreationFlags_AutoSelectBase14, true, szFontPath); -+ m_pPdfDocument->PoDoFo::PdfDocument::CreateFont( szFontName, false, PdfEncodingFactory::GlobalWinAnsiEncodingInstance(), PdfFontCache::eFontCreationFlags_AutoSelectBase14, true); -+ m_pPdfDocument->PoDoFo::PdfDocument::CreateFont( szFontName, true, PdfEncodingFactory::GlobalWinAnsiEncodingInstance(), PdfFontCache::eFontCreationFlags_AutoSelectBase14, true); - } - - void PdfSignatureGenerator::InitSignature( -@@ -130,7 +124,7 @@ void PdfSignatureGenerator::InitSignature( - - if (m_pSignatureField) delete m_pSignatureField; - -- PdfPage* pPage = m_pPdfDocument->GetPage(pageIndex); -+ PdfPage* pPage = m_pPdfMemDocument->GetPage(pageIndex); - PdfRect cropBox = pPage->GetCropBox(); - - float left0 = left * cropBox.GetWidth(); -@@ -145,15 +139,14 @@ void PdfSignatureGenerator::InitSignature( - - LOG_DBG((0, "InitSignature", "PdfSignatureField")); - -- m_pSignatureField = new PdfSignatureField( -- pPage, rect, m_pPdfDocument, PdfString(szFieldName), szSubFilter); -+ m_pSignatureField = new PdfSignatureField(pPage, rect, m_pPdfMemDocument); - - LOG_DBG((0, "InitSignature", "PdfSignatureField OK")); - - if (szReason && szReason[0]) { - PdfString reason(szReason); - PdfString reasonLabel(szReasonLabel); -- m_pSignatureField->SetSignatureReason(reasonLabel, reason); -+ m_pSignatureField->SetSignatureReason(reason); - } - - LOG_DBG((0, "InitSignature", "szReason OK")); -@@ -161,7 +154,7 @@ void PdfSignatureGenerator::InitSignature( - if (szLocation && szLocation[0]) { - PdfString location(szLocation); - PdfString locationLabel(szLocationLabel); -- m_pSignatureField->SetSignatureLocation(locationLabel, location); -+ m_pSignatureField->SetSignatureLocation(location); - } - - LOG_DBG((0, "InitSignature", "szLocation OK")); -@@ -171,54 +164,42 @@ void PdfSignatureGenerator::InitSignature( - - LOG_DBG((0, "InitSignature", "Date OK")); - -- if (szName && szName[0]) { -- PdfString name(szName); -- PdfString nameLabel(szNameLabel); -- m_pSignatureField->SetSignatureName(nameLabel, name); -- } -- -- LOG_DBG((0, "InitSignature", "szName OK")); -- -- m_pSignatureField->SetSignatureSize(SIGNATURE_SIZE); -+ m_pSignOutputDevice->PdfSignOutputDevice::SetSignatureSize(SIGNATURE_SIZE); - - LOG_DBG((0, "InitSignature", "SIGNATURE_SIZE OK")); - -- // if((szImagePath && szImagePath[0]) || (szDescription && szDescription[0])) -- if (width * height > 0) { -- try { -- // m_pSignatureField->SetFontSize(5); -- m_pSignatureField->SetAppearance(szImagePath, szDescription); -- LOG_DBG((0, "InitSignature", "SetAppearance OK")); -- } catch (PdfError& error) { -- LOG_ERR((0, "InitSignature", "SetAppearance error: %s, %s", -- PdfError::ErrorMessage(error.GetError()), error.what())); -- } catch (PdfError* perror) { -- LOG_ERR((0, "InitSignature", "SetAppearance error2: %s, %s", -- PdfError::ErrorMessage(perror->GetError()), perror->what())); -- } catch (std::exception& ex) { -- LOG_ERR( -- (0, "InitSignature", "SetAppearance std exception, %s", ex.what())); -- } catch (std::exception* pex) { -- LOG_ERR((0, "InitSignature", "SetAppearance std exception2, %s", -- pex->what())); -- } catch (...) { -- LOG_ERR((0, "InitSignature", "SetAppearance unknown error")); -- } -- } -+ // if (width * height > 0) { -+ // try { -+ // m_pSignatureField->SetAppearance(szImagePath, szDescription); -+ // LOG_DBG((0, "InitSignature", "SetAppearance OK")); -+ // } catch (PdfError& error) { -+ // LOG_ERR((0, "InitSignature", "SetAppearance error: %s, %s", -+ // PdfError::ErrorMessage(error.GetError()), error.what())); -+ // } catch (PdfError* perror) { -+ // LOG_ERR((0, "InitSignature", "SetAppearance error2: %s, %s", -+ // PdfError::ErrorMessage(perror->GetError()), perror->what())); -+ // } catch (std::exception& ex) { -+ // LOG_ERR( -+ // (0, "InitSignature", "SetAppearance std exception, %s", -+ // ex.what())); -+ // } catch (std::exception* pex) { -+ // LOG_ERR((0, "InitSignature", "SetAppearance std exception2, %s", -+ // pex->what())); -+ // } catch (...) { -+ // LOG_ERR((0, "InitSignature", "SetAppearance unknown error")); -+ // } -+ // } - -- if (szGraphometricData && szGraphometricData[0]) -- m_pSignatureField->SetGraphometricData( -- PdfString("Aruba_Sign_Biometric_Data"), PdfString(szGraphometricData), -- PdfString(szVersion)); -+ // if (szGraphometricData && szGraphometricData[0]) -+ // m_pSignatureField->SetGraphometricData( -+ // PdfString("Aruba_Sign_Biometric_Data"), -+ // PdfString(szGraphometricData), PdfString(szVersion)); - -- LOG_DBG((0, "InitSignature", "szGraphometricData OK")); -+ // LOG_DBG((0, "InitSignature", "szGraphometricData OK")); - - LOG_DBG((0, "InitSignature", "m_actualLen %d", m_actualLen)); - // crea il nuovo doc con il campo di firma -- int fulllen = m_actualLen * 2 + SIGNATURE_SIZE * 2 + -- (szGraphometricData -- ? (strlen(szGraphometricData) + strlen(szVersion) + 100) -- : 0); -+ int fulllen = m_actualLen * 2 + SIGNATURE_SIZE * 2; - - int mainDoclen = 0; - m_pMainDocbuffer = NULL; -@@ -227,7 +208,7 @@ void PdfSignatureGenerator::InitSignature( - LOG_DBG((0, "InitSignature", "fulllen %d", fulllen)); - m_pMainDocbuffer = new char[fulllen]; - PdfOutputDevice pdfOutDevice(m_pMainDocbuffer, fulllen); -- m_pPdfDocument->Write(&pdfOutDevice); -+ m_pPdfMemDocument->Write(&pdfOutDevice); - mainDoclen = pdfOutDevice.GetLength(); - } catch (::PoDoFo::PdfError err) { - if (m_pMainDocbuffer) { -@@ -301,32 +282,32 @@ void PdfSignatureGenerator::GetSignedPdf(UUCByteArray& signedPdf) { - } - - const double PdfSignatureGenerator::getWidth(int pageIndex) { -- if (m_pPdfDocument) { -- PdfPage* pPage = m_pPdfDocument->GetPage(pageIndex); -+ if (m_pPdfMemDocument) { -+ PdfPage* pPage = m_pPdfMemDocument->GetPage(pageIndex); - return pPage->GetPageSize().GetWidth(); - } - return 0; - } - - const double PdfSignatureGenerator::getHeight(int pageIndex) { -- if (m_pPdfDocument) { -- PdfPage* pPage = m_pPdfDocument->GetPage(pageIndex); -+ if (m_pPdfMemDocument) { -+ PdfPage* pPage = m_pPdfMemDocument->GetPage(pageIndex); - return pPage->GetPageSize().GetHeight(); - } - return 0; - } - - const double PdfSignatureGenerator::lastSignatureY(int left, int bottom) { -- if (!m_pPdfDocument) return -1; -+ if (!m_pPdfMemDocument) return -1; - /// Find the document catalog dictionary -- const PdfObject* const trailer = m_pPdfDocument->GetTrailer(); -+ const PdfObject* const trailer = m_pPdfMemDocument->GetTrailer(); - if (!trailer->IsDictionary()) return -1; - const PdfObject* const catalogRef = - trailer->GetDictionary().GetKey(PdfName("Root")); - if (catalogRef == 0 || !catalogRef->IsReference()) - return -2; // throw std::invalid_argument("Invalid /Root entry"); - const PdfObject* const catalog = -- m_pPdfDocument->GetObjects().GetObject(catalogRef->GetReference()); -+ m_pPdfMemDocument->GetObjects().GetObject(catalogRef->GetReference()); - if (catalog == 0 || !catalog->IsDictionary()) - return -3; // throw std::invalid_argument("Invalid or non-dictionary - // referenced by /Root entry"); -@@ -336,8 +317,8 @@ const double PdfSignatureGenerator::lastSignatureY(int left, int bottom) { - catalog->GetDictionary().GetKey(PdfName("AcroForm")); - if (acroFormValue == 0) return bottom; - if (acroFormValue->IsReference()) -- acroFormValue = -- m_pPdfDocument->GetObjects().GetObject(acroFormValue->GetReference()); -+ acroFormValue = m_pPdfMemDocument->GetObjects().GetObject( -+ acroFormValue->GetReference()); - - if (!acroFormValue->IsDictionary()) return bottom; - -@@ -346,8 +327,8 @@ const double PdfSignatureGenerator::lastSignatureY(int left, int bottom) { - if (fieldsValue == 0) return bottom; - - if (fieldsValue->IsReference()) -- fieldsValue = -- m_pPdfDocument->GetObjects().GetObject(acroFormValue->GetReference()); -+ fieldsValue = m_pPdfMemDocument->GetObjects().GetObject( -+ acroFormValue->GetReference()); - - if (!fieldsValue->IsArray()) return bottom; - -@@ -360,8 +341,8 @@ const double PdfSignatureGenerator::lastSignatureY(int left, int bottom) { - - for (unsigned int i = 0; i < array.size(); i++) { - const PdfObject* pObj = -- m_pPdfDocument->GetObjects().GetObject(array[i].GetReference()); -- if (IsSignatureField(m_pPdfDocument, pObj)) { -+ m_pPdfMemDocument->GetObjects().GetObject(array[i].GetReference()); -+ if (IsSignatureField(m_pPdfMemDocument, pObj)) { - const PdfObject* const keyRect = - pObj->GetDictionary().GetKey(PdfName("Rect")); - if (keyRect == 0) { -diff --git a/libs/sign-sdk/src/disigonsdk.cpp b/libs/sign-sdk/src/disigonsdk.cpp -index 250c93f..84e1b0b 100644 ---- a/libs/sign-sdk/src/disigonsdk.cpp -+++ b/libs/sign-sdk/src/disigonsdk.cpp -@@ -5,6 +5,7 @@ - - #include - #include -+#include - #include - #include - diff --git a/pkgs/by-name/do/dooit/package.nix b/pkgs/by-name/do/dooit/package.nix index 361729775ae1..df0cfae3fc9d 100644 --- a/pkgs/by-name/do/dooit/package.nix +++ b/pkgs/by-name/do/dooit/package.nix @@ -9,14 +9,14 @@ }: python3.pkgs.buildPythonApplication rec { pname = "dooit"; - version = "3.2.2"; + version = "3.2.3"; pyproject = true; src = fetchFromGitHub { owner = "dooit-org"; repo = "dooit"; tag = "v${version}"; - hash = "sha256-2W3iO4AOuDdDKJDMMY8YKXlI+dQKRI3PQtkdi9J3wZo="; + hash = "sha256-bI9X+2tTLnQwxfsnBmy2vBI3lJ4UX418zOy3oniVKWc="; }; build-system = with python3.pkgs; [ poetry-core ]; diff --git a/pkgs/by-name/el/element-desktop/element-desktop-pin.nix b/pkgs/by-name/el/element-desktop/element-desktop-pin.nix index 1a72d1488ab6..178e8c7b5ce6 100644 --- a/pkgs/by-name/el/element-desktop/element-desktop-pin.nix +++ b/pkgs/by-name/el/element-desktop/element-desktop-pin.nix @@ -1,7 +1,7 @@ { - "version" = "1.11.100"; + "version" = "1.11.102"; "hashes" = { - "desktopSrcHash" = "sha256-qlKZkBPWcD1eyEetCrIKsSXmodg6DYCmENfY+UT7Khc="; - "desktopYarnHash" = "sha256-wuRAeb4IpA2Ihr3ohaMPvFsaMod4Bg8o9lm8yzStwmk="; + "desktopSrcHash" = "sha256-wefoN8Nk31lwJFYbBRoKfy+0n69yVg6jskqP6aTHApE="; + "desktopYarnHash" = "sha256-/Gy/sYk8EBWU07zXwPl0zsDW5ADRq1j5PH4lPFe8dxk="; }; } diff --git a/pkgs/by-name/el/element-desktop/package.nix b/pkgs/by-name/el/element-desktop/package.nix index 3e8ae7ee091f..a3683e80d6b7 100644 --- a/pkgs/by-name/el/element-desktop/package.nix +++ b/pkgs/by-name/el/element-desktop/package.nix @@ -4,15 +4,15 @@ fetchFromGitHub, makeWrapper, makeDesktopItem, - yarnConfigHook, + yarn, nodejs, - fetchYarnDeps, jq, - electron_35, + electron_36, element-web, sqlcipher, callPackage, desktopToDarwinBundle, + typescript, useKeytar ? true, # command line arguments which are always set commandLineArgs ? "", @@ -22,7 +22,7 @@ let pinData = import ./element-desktop-pin.nix; inherit (pinData.hashes) desktopSrcHash desktopYarnHash; executableName = "element-desktop"; - electron = electron_35; + electron = electron_36; keytar = callPackage ./keytar { inherit electron; }; @@ -41,16 +41,22 @@ stdenv.mkDerivation ( hash = desktopSrcHash; }; - offlineCache = fetchYarnDeps { - yarnLock = finalAttrs.src + "/yarn.lock"; - sha256 = desktopYarnHash; + # TODO: fetchYarnDeps currently does not deal properly with a dependency + # declared as a pin to a commit in a specific git repository. + # While it does download everything correctly, `yarn install --offline` + # always wants to `git ls-remote` to the repository, ignoring the local + # cached tarball. + offlineCache = callPackage ./yarn.nix { + inherit (finalAttrs) version src; + hash = desktopYarnHash; }; nativeBuildInputs = [ - yarnConfigHook nodejs makeWrapper jq + yarn + typescript ] ++ lib.optionals stdenv.hostPlatform.isDarwin [ desktopToDarwinBundle ]; inherit seshat; @@ -60,13 +66,21 @@ stdenv.mkDerivation ( # this shouldn't be in the closure just for unused scripts. dontPatchShebangs = true; + configurePhase = '' + mkdir -p node_modules/ + cp -r $offlineCache/node_modules/* node_modules/ + substituteInPlace package.json --replace-fail "tsx " "node node_modules/tsx/dist/cli.mjs " + ''; + buildPhase = '' runHook preBuild yarn --offline run build:ts - yarn --offline run i18n + node node_modules/matrix-web-i18n/scripts/gen-i18n.js + yarn --offline run i18n:sort yarn --offline run build:res + chmod -R a+w node_modules/keytar-forked rm -rf node_modules/matrix-seshat node_modules/keytar-forked ${lib.optionalString useKeytar "ln -s ${keytar} node_modules/keytar-forked"} ln -s $seshat node_modules/matrix-seshat @@ -82,6 +96,7 @@ stdenv.mkDerivation ( ln -s '${element-web}' "$out/share/element/webapp" cp -r '.' "$out/share/element/electron" cp -r './res/img' "$out/share/element" + chmod -R "a+w" "$out/share/element/electron/node_modules" rm -rf "$out/share/element/electron/node_modules" cp -r './node_modules' "$out/share/element/electron" cp $out/share/element/electron/lib/i18n/strings/en_EN.json $out/share/element/electron/lib/i18n/strings/en-us.json diff --git a/pkgs/by-name/el/element-desktop/yarn.nix b/pkgs/by-name/el/element-desktop/yarn.nix new file mode 100644 index 000000000000..5a1274349537 --- /dev/null +++ b/pkgs/by-name/el/element-desktop/yarn.nix @@ -0,0 +1,38 @@ +{ + stdenvNoCC, + yarn, + cacert, + git, + version, + src, + hash, +}: +stdenvNoCC.mkDerivation { + pname = "element-desktop-yarn-deps"; + inherit version src; + + nativeBuildInputs = [ + cacert + yarn + git + ]; + + dontInstall = true; + + NODE_EXTRA_CA_CERTS = "${cacert}/etc/ssl/certs/ca-bundle.crt"; + + buildPhase = '' + export HOME=$(mktemp -d) + export YARN_ENABLE_TELEMETRY=0 + + yarn install --frozen-lockfile --ignore-platform --skip-integrity-check --ignore-scripts --no-progress --non-interactive + + mkdir -p $out/node_modules + cp -r node_modules/* $out/node_modules/ + ''; + + dontPatchShebangs = true; + + outputHash = hash; + outputHashMode = "recursive"; +} diff --git a/pkgs/by-name/el/element-web-unwrapped/element-web-pin.nix b/pkgs/by-name/el/element-web-unwrapped/element-web-pin.nix index 2fb0517243dc..bf3c641d42d4 100644 --- a/pkgs/by-name/el/element-web-unwrapped/element-web-pin.nix +++ b/pkgs/by-name/el/element-web-unwrapped/element-web-pin.nix @@ -1,7 +1,7 @@ { - "version" = "1.11.100"; + "version" = "1.11.102"; "hashes" = { - "webSrcHash" = "sha256-FiYjWOJ50Vhbs9vgEqK64HTVtwSuy4/BZAkPK4c6DXQ="; - "webYarnHash" = "sha256-C1yVJHU9ClTJHQfMLkdZEeRWVVu68eJp2kxnIlLinY8="; + "webSrcHash" = "sha256-B21fBRcksX8MoyWiqf1sa9yowJ/Z/wcM3vEqslunv7A="; + "webYarnHash" = "sha256-NXoGMEJM4uxFMCb5YX0ue9hgxe22hzG0MTeAOaBrJO4="; }; } diff --git a/pkgs/by-name/fr/frankenphp/package.nix b/pkgs/by-name/fr/frankenphp/package.nix index 71dd976cbff0..075fd4bfa35e 100644 --- a/pkgs/by-name/fr/frankenphp/package.nix +++ b/pkgs/by-name/fr/frankenphp/package.nix @@ -31,13 +31,13 @@ let in buildGoModule rec { pname = "frankenphp"; - version = "1.6.2"; + version = "1.7.0"; src = fetchFromGitHub { owner = "dunglas"; repo = "frankenphp"; tag = "v${version}"; - hash = "sha256-YYEipvr0qqudKuLrINt5htl2oCp6wXWLpyvCPuwR9e8="; + hash = "sha256-FukxXuZwF7P5tUao0nTT7bELGIYivtoOBQQkHA7ZE3s="; }; sourceRoot = "${src.name}/caddy"; @@ -45,7 +45,7 @@ buildGoModule rec { # frankenphp requires C code that would be removed with `go mod tidy` # https://github.com/golang/go/issues/26366 proxyVendor = true; - vendorHash = "sha256-M1z9Yv+WomKA+UACOUT8xcWdw+SD1MY2jfaDO4zYUQg="; + vendorHash = "sha256-likIETO/eq1kATNbbYHxXxvIPH7q5tp3WHjz+zvNOws="; buildInputs = [ phpUnwrapped diff --git a/pkgs/by-name/li/lillydap/package.nix b/pkgs/by-name/li/lillydap/package.nix index d095f3a4a8ca..115673095020 100644 --- a/pkgs/by-name/li/lillydap/package.nix +++ b/pkgs/by-name/li/lillydap/package.nix @@ -6,7 +6,7 @@ gperf, arpa2cm, quickder, - nix-update-script, + gitUpdater, }: stdenv.mkDerivation (finalAttrs: { @@ -32,7 +32,7 @@ stdenv.mkDerivation (finalAttrs: { quickder ]; - passthru.updateScript = nix-update-script { }; + passthru.updateScript = gitUpdater { rev-prefix = "v"; }; meta = { description = "Little LDAP: Event-driven, lock-free kernel for dynamic data servers, clients, filters"; diff --git a/pkgs/by-name/mp/mpls/package.nix b/pkgs/by-name/mp/mpls/package.nix index 0e9abac8f657..baca6ca2a8d9 100644 --- a/pkgs/by-name/mp/mpls/package.nix +++ b/pkgs/by-name/mp/mpls/package.nix @@ -7,13 +7,13 @@ }: buildGoModule rec { pname = "mpls"; - version = "0.15.2"; + version = "0.15.3"; src = fetchFromGitHub { owner = "mhersson"; repo = "mpls"; tag = "v${version}"; - hash = "sha256-UQIGg31OJ8vTqlj5JLYxOxg9oS0+PXPcdocAJbUgpzY="; + hash = "sha256-MGrvJOnjNNXU8Z9rqDIacb5awKxf50xYeNkY06U4cUk="; }; vendorHash = "sha256-n3DG3sR7HOQPQJW1t1qC94EKkDBgXpdmjUWtLzAE7kY="; diff --git a/pkgs/by-name/sh/shottr/package.nix b/pkgs/by-name/sh/shottr/package.nix new file mode 100644 index 000000000000..4de63b3f11a3 --- /dev/null +++ b/pkgs/by-name/sh/shottr/package.nix @@ -0,0 +1,65 @@ +{ + lib, + stdenvNoCC, + fetchurl, + writeShellApplication, + cacert, + curl, + common-updater-scripts, + pup, + undmg, +}: + +stdenvNoCC.mkDerivation (finalAttrs: { + pname = "shottr"; + version = "1.8.1"; + + src = fetchurl { + url = "https://shottr.cc/dl/Shottr-${finalAttrs.version}.dmg"; + hash = "sha256-I3LNLuhIRdjKDn79HWRK2B/tVsV+1aGt/aY442y3r2I="; + }; + + nativeBuildInputs = [ undmg ]; + + sourceRoot = "."; + + installPhase = '' + runHook preInstall + + mkdir -p "$out/Applications" + cp -R Shottr.app "$out/Applications" + + mkdir -p "$out/bin" + ln -s "$out/Applications/Shottr.app/Contents/MacOS/Shottr" "$out/bin/shottr" + + runHook postInstall + ''; + + passthru.updateScript = lib.getExe (writeShellApplication { + name = "shottr-update-script"; + runtimeInputs = [ + cacert + common-updater-scripts + curl + pup + ]; + text = '' + version="$(curl -s https://shottr.cc/newversion.html \ + | pup 'a[href*="Shottr-"] attr{href}' \ + | sed -E 's|/dl/Shottr-||' \ + | sed -E 's|\.dmg||')" + update-source-version shottr "$version" + ''; + }); + + meta = { + changelog = "https://shottr.cc/newversion.html"; + description = "MacOS screenshot app with scrolling screenshots, OCR, annotation and measurement instruments"; + homepage = "https://shottr.cc/"; + license = lib.licenses.unfree; + mainProgram = "shottr"; + maintainers = with lib.maintainers; [ donteatoreo ]; + platforms = lib.platforms.darwin; + sourceProvenance = with lib.sourceTypes; [ binaryNativeCode ]; + }; +}) diff --git a/pkgs/by-name/st/stockfish/package.nix b/pkgs/by-name/st/stockfish/package.nix index 50216db2b95e..02cfd355f012 100644 --- a/pkgs/by-name/st/stockfish/package.nix +++ b/pkgs/by-name/st/stockfish/package.nix @@ -3,7 +3,12 @@ stdenv, fetchurl, fetchFromGitHub, - apple-sdk, + versionCheckHook, + _experimental-update-script-combinators, + nix-update-script, + writeShellApplication, + nix, + gnugrep, }: let @@ -24,29 +29,31 @@ let "unknown"; # These files can be found in src/evaluate.h - nnueBigFile = "nn-1111cefa1111.nnue"; + nnueBigFile = "nn-1c0000000000.nnue"; + nnueBigHash = "sha256-HAAAAAAApn1imZnZMtDDc/dFDOQ80S0FYoaPTq+a4q0="; nnueBig = fetchurl { name = nnueBigFile; url = "https://tests.stockfishchess.org/api/nn/${nnueBigFile}"; - sha256 = "sha256-ERHO+hERa3cWG9SxTatMUPJuWSDHVvSGFZK+Pc1t4XQ="; + hash = nnueBigHash; }; nnueSmallFile = "nn-37f18f62d772.nnue"; + nnueSmallHash = "sha256-N/GPYtdy8xB+HWqso4mMEww8hvKrY+ZVX7vKIGNaiZ0="; nnueSmall = fetchurl { name = nnueSmallFile; url = "https://tests.stockfishchess.org/api/nn/${nnueSmallFile}"; - sha256 = "sha256-N/GPYtdy8xB+HWqso4mMEww8hvKrY+ZVX7vKIGNaiZ0="; + hash = nnueSmallHash; }; in stdenv.mkDerivation rec { pname = "stockfish"; - version = "17"; + version = "17.1"; src = fetchFromGitHub { owner = "official-stockfish"; repo = "Stockfish"; - rev = "sf_${version}"; - sha256 = "sha256-oXvLaC5TEUPlHjhm7tOxpNPY88QxYHFw+Cev3Q8NEeQ="; + tag = "sf_${version}"; + hash = "sha256-c8o1d7/yPnF3Eo7M/MSzYuYQr2qt2tIwyu7WfuKMAzg="; }; postUnpack = '' @@ -64,6 +71,37 @@ stdenv.mkDerivation rec { enableParallelBuilding = true; + nativeInstallCheckInputs = [ + versionCheckHook + ]; + doInstallCheck = true; + versionCheckProgram = "${placeholder "out"}/bin/stockfish"; + versionCheckProgramArg = "--help"; + + passthru = { + updateScript = _experimental-update-script-combinators.sequence [ + (nix-update-script { + extraArgs = [ "--version-regex=^sf_([\\d.]+)$" ]; + }) + (lib.getExe (writeShellApplication { + name = "${pname}-nnue-updater"; + runtimeInputs = [ + nix + gnugrep + ]; + runtimeEnv = { + PNAME = pname; + PKG_FILE = builtins.toString ./package.nix; + NNUE_BIG_FILE = nnueBigFile; + NNUE_BIG_HASH = nnueBigHash; + NNUE_SMALL_FILE = nnueSmallFile; + NNUE_SMALL_HASH = nnueSmallHash; + }; + text = builtins.readFile ./update.bash; + })) + ]; + }; + meta = with lib; { homepage = "https://stockfishchess.org/"; description = "Strong open source chess engine"; diff --git a/pkgs/by-name/st/stockfish/update.bash b/pkgs/by-name/st/stockfish/update.bash new file mode 100644 index 000000000000..aaf294fd027d --- /dev/null +++ b/pkgs/by-name/st/stockfish/update.bash @@ -0,0 +1,20 @@ +new_src="$(nix-build --attr "pkgs.$PNAME.src" --no-out-link)" +new_nnue_big_file="$(grep --perl-regexp --only-matching 'EvalFileDefaultNameBig "\Knn-(\w+).nnue' "$new_src/src/evaluate.h")" +new_nnue_small_file="$(grep --perl-regexp --only-matching 'EvalFileDefaultNameSmall "\Knn-(\w+).nnue' "$new_src/src/evaluate.h")" +new_nnue_big_hash="$( + nix hash convert --hash-algo sha256 "$( + nix-prefetch-url --type sha256 "https://tests.stockfishchess.org/api/nn/$new_nnue_big_file" + )" +)" +new_nnue_small_hash="$( + nix hash convert --hash-algo sha256 "$( + nix-prefetch-url --type sha256 "https://tests.stockfishchess.org/api/nn/$new_nnue_small_file" + )" +)" + +pkg_body="$(<"$PKG_FILE")" +pkg_body="${pkg_body//"$NNUE_BIG_FILE"/"$new_nnue_big_file"}" +pkg_body="${pkg_body//"$NNUE_BIG_HASH"/"$new_nnue_big_hash"}" +pkg_body="${pkg_body//"$NNUE_SMALL_FILE"/"$new_nnue_small_file"}" +pkg_body="${pkg_body//"$NNUE_SMALL_HASH"/"$new_nnue_small_hash"}" +echo "$pkg_body" >"$PKG_FILE" diff --git a/pkgs/by-name/st/style50/package.nix b/pkgs/by-name/st/style50/package.nix new file mode 100644 index 000000000000..034f7428aa4e --- /dev/null +++ b/pkgs/by-name/st/style50/package.nix @@ -0,0 +1,68 @@ +{ + lib, + python3Packages, + fetchFromGitHub, + libclang, + versionCheckHook, +}: + +python3Packages.buildPythonApplication rec { + pname = "style50"; + version = "2.10.4"; + pyproject = true; + + src = fetchFromGitHub { + owner = "cs50"; + repo = "style50"; + tag = "v${version}"; + hash = "sha256-59V3QZMYH5edBXv1GNdoaQxerDfKmLKUZ7VL3cvDvuE="; + }; + + build-system = [ + python3Packages.setuptools + ]; + + dependencies = with python3Packages; [ + autopep8 + icdiff + jinja2 + jsbeautifier + pycodestyle + python-magic + termcolor + ]; + + preFixup = '' + makeWrapperArgs+=(--prefix PATH : ${ + lib.makeBinPath [ + libclang # clang-format + ] + }) + ''; + + pythonRelaxDeps = [ + "pycodestyle" + ]; + + pythonRemoveDeps = [ + "clang-format" + ]; + + pythonImportsCheck = [ "style50" ]; + + nativeCheckInputs = [ versionCheckHook ]; + versionCheckProgramArg = "--version"; + + # no python tests + + meta = { + description = "Tool for checking code against the CS50 style guide"; + homepage = "https://cs50.readthedocs.io/style50/"; + downloadPage = "https://github.com/cs50/style50"; + changelog = "https://github.com/cs50/style50/releases/tag/v${version}"; + license = lib.licenses.gpl3Only; + platforms = lib.platforms.unix; + maintainers = with lib.maintainers; [ ethancedwards8 ]; + mainProgram = "style50"; + }; +} diff --git a/pkgs/desktops/xfce/core/libxfce4windowing/default.nix b/pkgs/desktops/xfce/core/libxfce4windowing/default.nix index a79d48d52927..0112316d4c83 100644 --- a/pkgs/desktops/xfce/core/libxfce4windowing/default.nix +++ b/pkgs/desktops/xfce/core/libxfce4windowing/default.nix @@ -2,6 +2,7 @@ stdenv, lib, mkXfceDerivation, + python3, wayland-scanner, glib, gtk3, @@ -22,12 +23,13 @@ mkXfceDerivation { category = "xfce"; pname = "libxfce4windowing"; - version = "4.20.2"; + version = "4.20.3"; - sha256 = "sha256-Xw1hs854K5dZCAYoBMoqJzdSxPRFUYqEpWxg4DLSK5Q="; + sha256 = "sha256-l58cTz28UPSVfoIpjBCoSwcqdUJfG9e4UlhVYPyEeAs="; nativeBuildInputs = [ + python3 wayland-scanner ] ++ lib.optionals withIntrospection [ @@ -46,6 +48,10 @@ mkXfceDerivation { wlr-protocols ]; + postPatch = '' + patchShebangs xdt-gen-visibility + ''; + meta = { description = "Windowing concept abstraction library for X11 and Wayland"; license = lib.licenses.lgpl21Plus; diff --git a/pkgs/development/interpreters/python/cpython/3.14/CVE-2025-4517.patch b/pkgs/development/interpreters/python/cpython/3.14/CVE-2025-4517.patch new file mode 100644 index 000000000000..5ad45fdde9fa --- /dev/null +++ b/pkgs/development/interpreters/python/cpython/3.14/CVE-2025-4517.patch @@ -0,0 +1,2055 @@ +From 9e0ac76d96cf80b49055f6d6b9a6763fb9215c2a Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?=C5=81ukasz=20Langa?= +Date: Tue, 3 Jun 2025 14:05:00 +0200 +Subject: [PATCH] [3.14] gh-135034: Normalize link targets in tarfile, add + `os.path.realpath(strict='allow_missing')` (gh-135037) (gh-135065) +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Addresses CVEs 2024-12718, 2025-4138, 2025-4330, and 2025-4517. + +(cherry picked from commit 3612d8f51741b11f36f8fb0494d79086bac9390a) + +Signed-off-by: Ɓukasz Langa +Co-authored-by: Petr Viktorin +Co-authored-by: Seth Michael Larson +Co-authored-by: Adam Turner <9087854+AA-Turner@users.noreply.github.com> +Co-authored-by: Serhiy Storchaka +--- + Doc/library/os.path.rst | 32 +- + Doc/library/tarfile.rst | 20 ++ + Doc/whatsnew/3.14.rst | 32 ++ + Lib/genericpath.py | 11 +- + Lib/ntpath.py | 38 ++- + Lib/posixpath.py | 57 ++-- + Lib/tarfile.py | 163 +++++++-- + Lib/test/test_ntpath.py | 216 ++++++++++-- + Lib/test/test_posixpath.py | 252 +++++++++++--- + Lib/test/test_tarfile.py | 310 +++++++++++++++++- + ...-06-02-11-32-23.gh-issue-135034.RLGjbp.rst | 6 + + 11 files changed, 967 insertions(+), 170 deletions(-) + create mode 100644 Misc/NEWS.d/next/Security/2025-06-02-11-32-23.gh-issue-135034.RLGjbp.rst + +diff --git a/Doc/library/os.path.rst b/Doc/library/os.path.rst +index ecbbc1d7605f9f..f72aee19d8f332 100644 +--- a/Doc/library/os.path.rst ++++ b/Doc/library/os.path.rst +@@ -408,9 +408,26 @@ the :mod:`glob` module.) + system). On Windows, this function will also resolve MS-DOS (also called 8.3) + style names such as ``C:\\PROGRA~1`` to ``C:\\Program Files``. + +- If a path doesn't exist or a symlink loop is encountered, and *strict* is +- ``True``, :exc:`OSError` is raised. If *strict* is ``False`` these errors +- are ignored, and so the result might be missing or otherwise inaccessible. ++ By default, the path is evaluated up to the first component that does not ++ exist, is a symlink loop, or whose evaluation raises :exc:`OSError`. ++ All such components are appended unchanged to the existing part of the path. ++ ++ Some errors that are handled this way include "access denied", "not a ++ directory", or "bad argument to internal function". Thus, the ++ resulting path may be missing or inaccessible, may still contain ++ links or loops, and may traverse non-directories. ++ ++ This behavior can be modified by keyword arguments: ++ ++ If *strict* is ``True``, the first error encountered when evaluating the path is ++ re-raised. ++ In particular, :exc:`FileNotFoundError` is raised if *path* does not exist, ++ or another :exc:`OSError` if it is otherwise inaccessible. ++ ++ If *strict* is :py:data:`os.path.ALLOW_MISSING`, errors other than ++ :exc:`FileNotFoundError` are re-raised (as with ``strict=True``). ++ Thus, the returned path will not contain any symbolic links, but the named ++ file and some of its parent directories may be missing. + + .. note:: + This function emulates the operating system's procedure for making a path +@@ -429,6 +446,15 @@ the :mod:`glob` module.) + .. versionchanged:: 3.10 + The *strict* parameter was added. + ++ .. versionchanged:: next ++ The :py:data:`~os.path.ALLOW_MISSING` value for the *strict* parameter ++ was added. ++ ++.. data:: ALLOW_MISSING ++ ++ Special value used for the *strict* argument in :func:`realpath`. ++ ++ .. versionadded:: next + + .. function:: relpath(path, start=os.curdir) + +diff --git a/Doc/library/tarfile.rst b/Doc/library/tarfile.rst +index f9cb5495e60cd2..7cec108a5bd41d 100644 +--- a/Doc/library/tarfile.rst ++++ b/Doc/library/tarfile.rst +@@ -255,6 +255,15 @@ The :mod:`tarfile` module defines the following exceptions: + Raised to refuse extracting a symbolic link pointing outside the destination + directory. + ++.. exception:: LinkFallbackError ++ ++ Raised to refuse emulating a link (hard or symbolic) by extracting another ++ archive member, when that member would be rejected by the filter location. ++ The exception that was raised to reject the replacement member is available ++ as :attr:`!BaseException.__context__`. ++ ++ .. versionadded:: next ++ + + The following constants are available at the module level: + +@@ -1068,6 +1077,12 @@ reused in custom filters: + Implements the ``'data'`` filter. + In addition to what ``tar_filter`` does: + ++ - Normalize link targets (:attr:`TarInfo.linkname`) using ++ :func:`os.path.normpath`. ++ Note that this removes internal ``..`` components, which may change the ++ meaning of the link if the path in :attr:`!TarInfo.linkname` traverses ++ symbolic links. ++ + - :ref:`Refuse ` to extract links (hard or soft) + that link to absolute paths, or ones that link outside the destination. + +@@ -1099,6 +1114,10 @@ reused in custom filters: + Note that this filter does not block *all* dangerous archive features. + See :ref:`tarfile-further-verification` for details. + ++ .. versionchanged:: next ++ ++ Link targets are now normalized. ++ + + .. _tarfile-extraction-refuse: + +@@ -1127,6 +1146,7 @@ Here is an incomplete list of things to consider: + * Extract to a :func:`new temporary directory ` + to prevent e.g. exploiting pre-existing links, and to make it easier to + clean up after a failed extraction. ++* Disallow symbolic links if you do not need the functionality. + * When working with untrusted data, use external (e.g. OS-level) limits on + disk, memory and CPU usage. + * Check filenames against an allow-list of characters +diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst +index 561d1a8914b50c..1e6a3615963ff4 100644 +--- a/Doc/whatsnew/3.14.rst ++++ b/Doc/whatsnew/3.14.rst +@@ -1608,6 +1608,16 @@ os + (Contributed by Cody Maloney in :gh:`129205`.) + + ++os.path ++------- ++ ++* The *strict* parameter to :func:`os.path.realpath` accepts a new value, ++ :data:`os.path.ALLOW_MISSING`. ++ If used, errors other than :exc:`FileNotFoundError` will be re-raised; ++ the resulting path can be missing but it will be free of symlinks. ++ (Contributed by Petr Viktorin for :cve:`2025-4517`.) ++ ++ + pathlib + ------- + +@@ -1796,6 +1806,28 @@ sysconfig + (Contributed by Xuehai Pan in :gh:`131799`.) + + ++tarfile ++------- ++ ++* :func:`~tarfile.data_filter` now normalizes symbolic link targets in order to ++ avoid path traversal attacks. ++ (Contributed by Petr Viktorin in :gh:`127987` and :cve:`2025-4138`.) ++* :func:`~tarfile.TarFile.extractall` now skips fixing up directory attributes ++ when a directory was removed or replaced by another kind of file. ++ (Contributed by Petr Viktorin in :gh:`127987` and :cve:`2024-12718`.) ++* :func:`~tarfile.TarFile.extract` and :func:`~tarfile.TarFile.extractall` ++ now (re-)apply the extraction filter when substituting a link (hard or ++ symbolic) with a copy of another archive member, and when fixing up ++ directory attributes. ++ The former raises a new exception, :exc:`~tarfile.LinkFallbackError`. ++ (Contributed by Petr Viktorin for :cve:`2025-4330` and :cve:`2024-12718`.) ++* :func:`~tarfile.TarFile.extract` and :func:`~tarfile.TarFile.extractall` ++ no longer extract rejected members when ++ :func:`~tarfile.TarFile.errorlevel` is zero. ++ (Contributed by Matt Prodani and Petr Viktorin in :gh:`112887` ++ and :cve:`2025-4435`.) ++ ++ + threading + --------- + +diff --git a/Lib/genericpath.py b/Lib/genericpath.py +index ba7b0a13c7f81d..9363f564aab7a6 100644 +--- a/Lib/genericpath.py ++++ b/Lib/genericpath.py +@@ -8,7 +8,7 @@ + + __all__ = ['commonprefix', 'exists', 'getatime', 'getctime', 'getmtime', + 'getsize', 'isdevdrive', 'isdir', 'isfile', 'isjunction', 'islink', +- 'lexists', 'samefile', 'sameopenfile', 'samestat'] ++ 'lexists', 'samefile', 'sameopenfile', 'samestat', 'ALLOW_MISSING'] + + + # Does a path exist? +@@ -189,3 +189,12 @@ def _check_arg_types(funcname, *args): + f'os.PathLike object, not {s.__class__.__name__!r}') from None + if hasstr and hasbytes: + raise TypeError("Can't mix strings and bytes in path components") from None ++ ++# A singleton with a true boolean value. ++@object.__new__ ++class ALLOW_MISSING: ++ """Special value for use in realpath().""" ++ def __repr__(self): ++ return 'os.path.ALLOW_MISSING' ++ def __reduce__(self): ++ return self.__class__.__name__ +diff --git a/Lib/ntpath.py b/Lib/ntpath.py +index 52ff2af743af6c..9cdc16480f9afe 100644 +--- a/Lib/ntpath.py ++++ b/Lib/ntpath.py +@@ -29,7 +29,7 @@ + "abspath","curdir","pardir","sep","pathsep","defpath","altsep", + "extsep","devnull","realpath","supports_unicode_filenames","relpath", + "samefile", "sameopenfile", "samestat", "commonpath", "isjunction", +- "isdevdrive"] ++ "isdevdrive", "ALLOW_MISSING"] + + def _get_bothseps(path): + if isinstance(path, bytes): +@@ -601,9 +601,10 @@ def abspath(path): + from nt import _findfirstfile, _getfinalpathname, readlink as _nt_readlink + except ImportError: + # realpath is a no-op on systems without _getfinalpathname support. +- realpath = abspath ++ def realpath(path, *, strict=False): ++ return abspath(path) + else: +- def _readlink_deep(path): ++ def _readlink_deep(path, ignored_error=OSError): + # These error codes indicate that we should stop reading links and + # return the path we currently have. + # 1: ERROR_INVALID_FUNCTION +@@ -636,7 +637,7 @@ def _readlink_deep(path): + path = old_path + break + path = normpath(join(dirname(old_path), path)) +- except OSError as ex: ++ except ignored_error as ex: + if ex.winerror in allowed_winerror: + break + raise +@@ -645,7 +646,7 @@ def _readlink_deep(path): + break + return path + +- def _getfinalpathname_nonstrict(path): ++ def _getfinalpathname_nonstrict(path, ignored_error=OSError): + # These error codes indicate that we should stop resolving the path + # and return the value we currently have. + # 1: ERROR_INVALID_FUNCTION +@@ -673,17 +674,18 @@ def _getfinalpathname_nonstrict(path): + try: + path = _getfinalpathname(path) + return join(path, tail) if tail else path +- except OSError as ex: ++ except ignored_error as ex: + if ex.winerror not in allowed_winerror: + raise + try: + # The OS could not resolve this path fully, so we attempt + # to follow the link ourselves. If we succeed, join the tail + # and return. +- new_path = _readlink_deep(path) ++ new_path = _readlink_deep(path, ++ ignored_error=ignored_error) + if new_path != path: + return join(new_path, tail) if tail else new_path +- except OSError: ++ except ignored_error: + # If we fail to readlink(), let's keep traversing + pass + # If we get these errors, try to get the real name of the file without accessing it. +@@ -691,7 +693,7 @@ def _getfinalpathname_nonstrict(path): + try: + name = _findfirstfile(path) + path, _ = split(path) +- except OSError: ++ except ignored_error: + path, name = split(path) + else: + path, name = split(path) +@@ -721,6 +723,15 @@ def realpath(path, *, strict=False): + if normcase(path) == devnull: + return '\\\\.\\NUL' + had_prefix = path.startswith(prefix) ++ ++ if strict is ALLOW_MISSING: ++ ignored_error = FileNotFoundError ++ strict = True ++ elif strict: ++ ignored_error = () ++ else: ++ ignored_error = OSError ++ + if not had_prefix and not isabs(path): + path = join(cwd, path) + try: +@@ -728,17 +739,16 @@ def realpath(path, *, strict=False): + initial_winerror = 0 + except ValueError as ex: + # gh-106242: Raised for embedded null characters +- # In strict mode, we convert into an OSError. ++ # In strict modes, we convert into an OSError. + # Non-strict mode returns the path as-is, since we've already + # made it absolute. + if strict: + raise OSError(str(ex)) from None + path = normpath(path) +- except OSError as ex: +- if strict: +- raise ++ except ignored_error as ex: + initial_winerror = ex.winerror +- path = _getfinalpathname_nonstrict(path) ++ path = _getfinalpathname_nonstrict(path, ++ ignored_error=ignored_error) + # The path returned by _getfinalpathname will always start with \\?\ - + # strip off that prefix unless it was already provided on the original + # path. +diff --git a/Lib/posixpath.py b/Lib/posixpath.py +index db72ded8826056..d38f3bd5872bcd 100644 +--- a/Lib/posixpath.py ++++ b/Lib/posixpath.py +@@ -36,7 +36,7 @@ + "samefile","sameopenfile","samestat", + "curdir","pardir","sep","pathsep","defpath","altsep","extsep", + "devnull","realpath","supports_unicode_filenames","relpath", +- "commonpath", "isjunction","isdevdrive"] ++ "commonpath", "isjunction","isdevdrive","ALLOW_MISSING"] + + + def _get_sep(path): +@@ -402,10 +402,18 @@ def realpath(filename, *, strict=False): + curdir = '.' + pardir = '..' + getcwd = os.getcwd +- return _realpath(filename, strict, sep, curdir, pardir, getcwd) ++ if strict is ALLOW_MISSING: ++ ignored_error = FileNotFoundError ++ strict = True ++ elif strict: ++ ignored_error = () ++ else: ++ ignored_error = OSError ++ ++ lstat = os.lstat ++ readlink = os.readlink ++ maxlinks = None + +-def _realpath(filename, strict=False, sep=sep, curdir=curdir, pardir=pardir, +- getcwd=os.getcwd, lstat=os.lstat, readlink=os.readlink, maxlinks=None): + # The stack of unresolved path parts. When popped, a special value of None + # indicates that a symlink target has been resolved, and that the original + # symlink path can be retrieved by popping again. The [::-1] slice is a +@@ -477,27 +485,28 @@ def _realpath(filename, strict=False, sep=sep, curdir=curdir, pardir=pardir, + path = newpath + continue + target = readlink(newpath) +- except OSError: +- if strict: +- raise +- path = newpath ++ except ignored_error: ++ pass ++ else: ++ # Resolve the symbolic link ++ if target.startswith(sep): ++ # Symlink target is absolute; reset resolved path. ++ path = sep ++ if maxlinks is None: ++ # Mark this symlink as seen but not fully resolved. ++ seen[newpath] = None ++ # Push the symlink path onto the stack, and signal its specialness ++ # by also pushing None. When these entries are popped, we'll ++ # record the fully-resolved symlink target in the 'seen' mapping. ++ rest.append(newpath) ++ rest.append(None) ++ # Push the unresolved symlink target parts onto the stack. ++ target_parts = target.split(sep)[::-1] ++ rest.extend(target_parts) ++ part_count += len(target_parts) + continue +- # Resolve the symbolic link +- if target.startswith(sep): +- # Symlink target is absolute; reset resolved path. +- path = sep +- if maxlinks is None: +- # Mark this symlink as seen but not fully resolved. +- seen[newpath] = None +- # Push the symlink path onto the stack, and signal its specialness +- # by also pushing None. When these entries are popped, we'll +- # record the fully-resolved symlink target in the 'seen' mapping. +- rest.append(newpath) +- rest.append(None) +- # Push the unresolved symlink target parts onto the stack. +- target_parts = target.split(sep)[::-1] +- rest.extend(target_parts) +- part_count += len(target_parts) ++ # An error occurred and was ignored. ++ path = newpath + + return path + +diff --git a/Lib/tarfile.py b/Lib/tarfile.py +index 212b71f6509740..068aa13ed70356 100644 +--- a/Lib/tarfile.py ++++ b/Lib/tarfile.py +@@ -67,7 +67,7 @@ + "DEFAULT_FORMAT", "open","fully_trusted_filter", "data_filter", + "tar_filter", "FilterError", "AbsoluteLinkError", + "OutsideDestinationError", "SpecialFileError", "AbsolutePathError", +- "LinkOutsideDestinationError"] ++ "LinkOutsideDestinationError", "LinkFallbackError"] + + + #--------------------------------------------------------- +@@ -766,10 +766,22 @@ def __init__(self, tarinfo, path): + super().__init__(f'{tarinfo.name!r} would link to {path!r}, ' + + 'which is outside the destination') + ++class LinkFallbackError(FilterError): ++ def __init__(self, tarinfo, path): ++ self.tarinfo = tarinfo ++ self._path = path ++ super().__init__(f'link {tarinfo.name!r} would be extracted as a ' ++ + f'copy of {path!r}, which was rejected') ++ ++# Errors caused by filters -- both "fatal" and "non-fatal" -- that ++# we consider to be issues with the argument, rather than a bug in the ++# filter function ++_FILTER_ERRORS = (FilterError, OSError, ExtractError) ++ + def _get_filtered_attrs(member, dest_path, for_data=True): + new_attrs = {} + name = member.name +- dest_path = os.path.realpath(dest_path) ++ dest_path = os.path.realpath(dest_path, strict=os.path.ALLOW_MISSING) + # Strip leading / (tar's directory separator) from filenames. + # Include os.sep (target OS directory separator) as well. + if name.startswith(('/', os.sep)): +@@ -779,7 +791,8 @@ def _get_filtered_attrs(member, dest_path, for_data=True): + # For example, 'C:/foo' on Windows. + raise AbsolutePathError(member) + # Ensure we stay in the destination +- target_path = os.path.realpath(os.path.join(dest_path, name)) ++ target_path = os.path.realpath(os.path.join(dest_path, name), ++ strict=os.path.ALLOW_MISSING) + if os.path.commonpath([target_path, dest_path]) != dest_path: + raise OutsideDestinationError(member, target_path) + # Limit permissions (no high bits, and go-w) +@@ -817,6 +830,9 @@ def _get_filtered_attrs(member, dest_path, for_data=True): + if member.islnk() or member.issym(): + if os.path.isabs(member.linkname): + raise AbsoluteLinkError(member) ++ normalized = os.path.normpath(member.linkname) ++ if normalized != member.linkname: ++ new_attrs['linkname'] = normalized + if member.issym(): + target_path = os.path.join(dest_path, + os.path.dirname(name), +@@ -824,7 +840,8 @@ def _get_filtered_attrs(member, dest_path, for_data=True): + else: + target_path = os.path.join(dest_path, + member.linkname) +- target_path = os.path.realpath(target_path) ++ target_path = os.path.realpath(target_path, ++ strict=os.path.ALLOW_MISSING) + if os.path.commonpath([target_path, dest_path]) != dest_path: + raise LinkOutsideDestinationError(member, target_path) + return new_attrs +@@ -2386,30 +2403,58 @@ def extractall(self, path=".", members=None, *, numeric_owner=False, + members = self + + for member in members: +- tarinfo = self._get_extract_tarinfo(member, filter_function, path) ++ tarinfo, unfiltered = self._get_extract_tarinfo( ++ member, filter_function, path) + if tarinfo is None: + continue + if tarinfo.isdir(): + # For directories, delay setting attributes until later, + # since permissions can interfere with extraction and + # extracting contents can reset mtime. +- directories.append(tarinfo) ++ directories.append(unfiltered) + self._extract_one(tarinfo, path, set_attrs=not tarinfo.isdir(), +- numeric_owner=numeric_owner) ++ numeric_owner=numeric_owner, ++ filter_function=filter_function) + + # Reverse sort directories. + directories.sort(key=lambda a: a.name, reverse=True) + ++ + # Set correct owner, mtime and filemode on directories. +- for tarinfo in directories: +- dirpath = os.path.join(path, tarinfo.name) ++ for unfiltered in directories: + try: ++ # Need to re-apply any filter, to take the *current* filesystem ++ # state into account. ++ try: ++ tarinfo = filter_function(unfiltered, path) ++ except _FILTER_ERRORS as exc: ++ self._log_no_directory_fixup(unfiltered, repr(exc)) ++ continue ++ if tarinfo is None: ++ self._log_no_directory_fixup(unfiltered, ++ 'excluded by filter') ++ continue ++ dirpath = os.path.join(path, tarinfo.name) ++ try: ++ lstat = os.lstat(dirpath) ++ except FileNotFoundError: ++ self._log_no_directory_fixup(tarinfo, 'missing') ++ continue ++ if not stat.S_ISDIR(lstat.st_mode): ++ # This is no longer a directory; presumably a later ++ # member overwrote the entry. ++ self._log_no_directory_fixup(tarinfo, 'not a directory') ++ continue + self.chown(tarinfo, dirpath, numeric_owner=numeric_owner) + self.utime(tarinfo, dirpath) + self.chmod(tarinfo, dirpath) + except ExtractError as e: + self._handle_nonfatal_error(e) + ++ def _log_no_directory_fixup(self, member, reason): ++ self._dbg(2, "tarfile: Not fixing up directory %r (%s)" % ++ (member.name, reason)) ++ + def extract(self, member, path="", set_attrs=True, *, numeric_owner=False, + filter=None): + """Extract a member from the archive to the current working directory, +@@ -2425,41 +2470,56 @@ def extract(self, member, path="", set_attrs=True, *, numeric_owner=False, + String names of common filters are accepted. + """ + filter_function = self._get_filter_function(filter) +- tarinfo = self._get_extract_tarinfo(member, filter_function, path) ++ tarinfo, unfiltered = self._get_extract_tarinfo( ++ member, filter_function, path) + if tarinfo is not None: + self._extract_one(tarinfo, path, set_attrs, numeric_owner) + + def _get_extract_tarinfo(self, member, filter_function, path): +- """Get filtered TarInfo (or None) from member, which might be a str""" ++ """Get (filtered, unfiltered) TarInfos from *member* ++ ++ *member* might be a string. ++ ++ Return (None, None) if not found. ++ """ ++ + if isinstance(member, str): +- tarinfo = self.getmember(member) ++ unfiltered = self.getmember(member) + else: +- tarinfo = member ++ unfiltered = member + +- unfiltered = tarinfo ++ filtered = None + try: +- tarinfo = filter_function(tarinfo, path) ++ filtered = filter_function(unfiltered, path) + except (OSError, UnicodeEncodeError, FilterError) as e: + self._handle_fatal_error(e) + except ExtractError as e: + self._handle_nonfatal_error(e) +- if tarinfo is None: ++ if filtered is None: + self._dbg(2, "tarfile: Excluded %r" % unfiltered.name) +- return None ++ return None, None ++ + # Prepare the link target for makelink(). +- if tarinfo.islnk(): +- tarinfo = copy.copy(tarinfo) +- tarinfo._link_target = os.path.join(path, tarinfo.linkname) +- return tarinfo ++ if filtered.islnk(): ++ filtered = copy.copy(filtered) ++ filtered._link_target = os.path.join(path, filtered.linkname) ++ return filtered, unfiltered ++ ++ def _extract_one(self, tarinfo, path, set_attrs, numeric_owner, ++ filter_function=None): ++ """Extract from filtered tarinfo to disk. + +- def _extract_one(self, tarinfo, path, set_attrs, numeric_owner): +- """Extract from filtered tarinfo to disk""" ++ filter_function is only used when extracting a *different* ++ member (e.g. as fallback to creating a symlink) ++ """ + self._check("r") + + try: + self._extract_member(tarinfo, os.path.join(path, tarinfo.name), + set_attrs=set_attrs, +- numeric_owner=numeric_owner) ++ numeric_owner=numeric_owner, ++ filter_function=filter_function, ++ extraction_root=path) + except (OSError, UnicodeEncodeError) as e: + self._handle_fatal_error(e) + except ExtractError as e: +@@ -2517,9 +2577,13 @@ def extractfile(self, member): + return None + + def _extract_member(self, tarinfo, targetpath, set_attrs=True, +- numeric_owner=False): +- """Extract the TarInfo object tarinfo to a physical ++ numeric_owner=False, *, filter_function=None, ++ extraction_root=None): ++ """Extract the filtered TarInfo object tarinfo to a physical + file called targetpath. ++ ++ filter_function is only used when extracting a *different* ++ member (e.g. as fallback to creating a symlink) + """ + # Fetch the TarInfo object for the given name + # and build the destination pathname, replacing +@@ -2548,7 +2612,10 @@ def _extract_member(self, tarinfo, targetpath, set_attrs=True, + elif tarinfo.ischr() or tarinfo.isblk(): + self.makedev(tarinfo, targetpath) + elif tarinfo.islnk() or tarinfo.issym(): +- self.makelink(tarinfo, targetpath) ++ self.makelink_with_filter( ++ tarinfo, targetpath, ++ filter_function=filter_function, ++ extraction_root=extraction_root) + elif tarinfo.type not in SUPPORTED_TYPES: + self.makeunknown(tarinfo, targetpath) + else: +@@ -2631,10 +2698,18 @@ def makedev(self, tarinfo, targetpath): + os.makedev(tarinfo.devmajor, tarinfo.devminor)) + + def makelink(self, tarinfo, targetpath): ++ return self.makelink_with_filter(tarinfo, targetpath, None, None) ++ ++ def makelink_with_filter(self, tarinfo, targetpath, ++ filter_function, extraction_root): + """Make a (symbolic) link called targetpath. If it cannot be created + (platform limitation), we try to make a copy of the referenced file + instead of a link. ++ ++ filter_function is only used when extracting a *different* ++ member (e.g. as fallback to creating a link). + """ ++ keyerror_to_extracterror = False + try: + # For systems that support symbolic and hard links. + if tarinfo.issym(): +@@ -2642,18 +2717,38 @@ def makelink(self, tarinfo, targetpath): + # Avoid FileExistsError on following os.symlink. + os.unlink(targetpath) + os.symlink(tarinfo.linkname, targetpath) ++ return + else: + if os.path.exists(tarinfo._link_target): + os.link(tarinfo._link_target, targetpath) +- else: +- self._extract_member(self._find_link_target(tarinfo), +- targetpath) ++ return + except symlink_exception: ++ keyerror_to_extracterror = True ++ ++ try: ++ unfiltered = self._find_link_target(tarinfo) ++ except KeyError: ++ if keyerror_to_extracterror: ++ raise ExtractError( ++ "unable to resolve link inside archive") from None ++ else: ++ raise ++ ++ if filter_function is None: ++ filtered = unfiltered ++ else: ++ if extraction_root is None: ++ raise ExtractError( ++ "makelink_with_filter: if filter_function is not None, " ++ + "extraction_root must also not be None") + try: +- self._extract_member(self._find_link_target(tarinfo), +- targetpath) +- except KeyError: +- raise ExtractError("unable to resolve link inside archive") from None ++ filtered = filter_function(unfiltered, extraction_root) ++ except _FILTER_ERRORS as cause: ++ raise LinkFallbackError(tarinfo, unfiltered.name) from cause ++ if filtered is not None: ++ self._extract_member(filtered, targetpath, ++ filter_function=filter_function, ++ extraction_root=extraction_root) + + def chown(self, tarinfo, targetpath, numeric_owner): + """Set owner of targetpath according to tarinfo. If numeric_owner +diff --git a/Lib/test/test_ntpath.py b/Lib/test/test_ntpath.py +index f83ef225a6e48e..927a0befcc8c13 100644 +--- a/Lib/test/test_ntpath.py ++++ b/Lib/test/test_ntpath.py +@@ -7,7 +7,8 @@ + import unittest + import warnings + from test.support import cpython_only, os_helper +-from test.support import TestFailed, is_emscripten ++from test.support import TestFailed ++from ntpath import ALLOW_MISSING + from test.support.os_helper import FakePath + from test import test_genericpath + from tempfile import TemporaryFile +@@ -77,6 +78,27 @@ def tester(fn, wantResult): + %(str(fn), str(wantResult), repr(gotResult))) + + ++def _parameterize(*parameters): ++ """Simplistic decorator to parametrize a test ++ ++ Runs the decorated test multiple times in subTest, with a value from ++ 'parameters' passed as an extra positional argument. ++ Calls doCleanups() after each run. ++ ++ Not for general use. Intended to avoid indenting for easier backports. ++ ++ See https://discuss.python.org/t/91827 for discussing generalizations. ++ """ ++ def _parametrize_decorator(func): ++ def _parameterized(self, *args, **kwargs): ++ for parameter in parameters: ++ with self.subTest(parameter): ++ func(self, *args, parameter, **kwargs) ++ self.doCleanups() ++ return _parameterized ++ return _parametrize_decorator ++ ++ + class NtpathTestCase(unittest.TestCase): + def assertPathEqual(self, path1, path2): + if path1 == path2 or _norm(path1) == _norm(path2): +@@ -475,6 +497,27 @@ def test_realpath_curdir(self): + tester("ntpath.realpath('.\\.')", expected) + tester("ntpath.realpath('\\'.join(['.'] * 100))", expected) + ++ def test_realpath_curdir_strict(self): ++ expected = ntpath.normpath(os.getcwd()) ++ tester("ntpath.realpath('.', strict=True)", expected) ++ tester("ntpath.realpath('./.', strict=True)", expected) ++ tester("ntpath.realpath('/'.join(['.'] * 100), strict=True)", expected) ++ tester("ntpath.realpath('.\\.', strict=True)", expected) ++ tester("ntpath.realpath('\\'.join(['.'] * 100), strict=True)", expected) ++ ++ def test_realpath_curdir_missing_ok(self): ++ expected = ntpath.normpath(os.getcwd()) ++ tester("ntpath.realpath('.', strict=ALLOW_MISSING)", ++ expected) ++ tester("ntpath.realpath('./.', strict=ALLOW_MISSING)", ++ expected) ++ tester("ntpath.realpath('/'.join(['.'] * 100), strict=ALLOW_MISSING)", ++ expected) ++ tester("ntpath.realpath('.\\.', strict=ALLOW_MISSING)", ++ expected) ++ tester("ntpath.realpath('\\'.join(['.'] * 100), strict=ALLOW_MISSING)", ++ expected) ++ + def test_realpath_pardir(self): + expected = ntpath.normpath(os.getcwd()) + tester("ntpath.realpath('..')", ntpath.dirname(expected)) +@@ -487,24 +530,59 @@ def test_realpath_pardir(self): + tester("ntpath.realpath('\\'.join(['..'] * 50))", + ntpath.splitdrive(expected)[0] + '\\') + ++ def test_realpath_pardir_strict(self): ++ expected = ntpath.normpath(os.getcwd()) ++ tester("ntpath.realpath('..', strict=True)", ntpath.dirname(expected)) ++ tester("ntpath.realpath('../..', strict=True)", ++ ntpath.dirname(ntpath.dirname(expected))) ++ tester("ntpath.realpath('/'.join(['..'] * 50), strict=True)", ++ ntpath.splitdrive(expected)[0] + '\\') ++ tester("ntpath.realpath('..\\..', strict=True)", ++ ntpath.dirname(ntpath.dirname(expected))) ++ tester("ntpath.realpath('\\'.join(['..'] * 50), strict=True)", ++ ntpath.splitdrive(expected)[0] + '\\') ++ ++ def test_realpath_pardir_missing_ok(self): ++ expected = ntpath.normpath(os.getcwd()) ++ tester("ntpath.realpath('..', strict=ALLOW_MISSING)", ++ ntpath.dirname(expected)) ++ tester("ntpath.realpath('../..', strict=ALLOW_MISSING)", ++ ntpath.dirname(ntpath.dirname(expected))) ++ tester("ntpath.realpath('/'.join(['..'] * 50), strict=ALLOW_MISSING)", ++ ntpath.splitdrive(expected)[0] + '\\') ++ tester("ntpath.realpath('..\\..', strict=ALLOW_MISSING)", ++ ntpath.dirname(ntpath.dirname(expected))) ++ tester("ntpath.realpath('\\'.join(['..'] * 50), strict=ALLOW_MISSING)", ++ ntpath.splitdrive(expected)[0] + '\\') ++ + @os_helper.skip_unless_symlink + @unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname') +- def test_realpath_basic(self): ++ @_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING}) ++ def test_realpath_basic(self, kwargs): + ABSTFN = ntpath.abspath(os_helper.TESTFN) + open(ABSTFN, "wb").close() + self.addCleanup(os_helper.unlink, ABSTFN) + self.addCleanup(os_helper.unlink, ABSTFN + "1") + + os.symlink(ABSTFN, ABSTFN + "1") +- self.assertPathEqual(ntpath.realpath(ABSTFN + "1"), ABSTFN) +- self.assertPathEqual(ntpath.realpath(os.fsencode(ABSTFN + "1")), ++ self.assertPathEqual(ntpath.realpath(ABSTFN + "1", **kwargs), ABSTFN) ++ self.assertPathEqual(ntpath.realpath(os.fsencode(ABSTFN + "1"), **kwargs), + os.fsencode(ABSTFN)) + + # gh-88013: call ntpath.realpath with binary drive name may raise a + # TypeError. The drive should not exist to reproduce the bug. + drives = {f"{c}:\\" for c in string.ascii_uppercase} - set(os.listdrives()) + d = drives.pop().encode() +- self.assertEqual(ntpath.realpath(d), d) ++ self.assertEqual(ntpath.realpath(d, strict=False), d) ++ ++ # gh-106242: Embedded nulls and non-strict fallback to abspath ++ if kwargs: ++ with self.assertRaises(OSError): ++ ntpath.realpath(os_helper.TESTFN + "\0spam", ++ **kwargs) ++ else: ++ self.assertEqual(ABSTFN + "\0spam", ++ ntpath.realpath(os_helper.TESTFN + "\0spam", **kwargs)) + + @os_helper.skip_unless_symlink + @unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname') +@@ -527,51 +605,66 @@ def test_realpath_invalid_paths(self): + self.assertEqual(realpath(path, strict=False), path) + # gh-106242: Embedded nulls should raise OSError (not ValueError) + self.assertRaises(OSError, realpath, path, strict=True) ++ self.assertRaises(OSError, realpath, path, strict=ALLOW_MISSING) + path = ABSTFNb + b'\x00' + self.assertEqual(realpath(path, strict=False), path) + self.assertRaises(OSError, realpath, path, strict=True) ++ self.assertRaises(OSError, realpath, path, strict=ALLOW_MISSING) + path = ABSTFN + '\\nonexistent\\x\x00' + self.assertEqual(realpath(path, strict=False), path) + self.assertRaises(OSError, realpath, path, strict=True) ++ self.assertRaises(OSError, realpath, path, strict=ALLOW_MISSING) + path = ABSTFNb + b'\\nonexistent\\x\x00' + self.assertEqual(realpath(path, strict=False), path) + self.assertRaises(OSError, realpath, path, strict=True) ++ self.assertRaises(OSError, realpath, path, strict=ALLOW_MISSING) + path = ABSTFN + '\x00\\..' + self.assertEqual(realpath(path, strict=False), os.getcwd()) + self.assertEqual(realpath(path, strict=True), os.getcwd()) ++ self.assertEqual(realpath(path, strict=ALLOW_MISSING), os.getcwd()) + path = ABSTFNb + b'\x00\\..' + self.assertEqual(realpath(path, strict=False), os.getcwdb()) + self.assertEqual(realpath(path, strict=True), os.getcwdb()) ++ self.assertEqual(realpath(path, strict=ALLOW_MISSING), os.getcwdb()) + path = ABSTFN + '\\nonexistent\\x\x00\\..' + self.assertEqual(realpath(path, strict=False), ABSTFN + '\\nonexistent') + self.assertRaises(OSError, realpath, path, strict=True) ++ self.assertEqual(realpath(path, strict=ALLOW_MISSING), ABSTFN + '\\nonexistent') + path = ABSTFNb + b'\\nonexistent\\x\x00\\..' + self.assertEqual(realpath(path, strict=False), ABSTFNb + b'\\nonexistent') + self.assertRaises(OSError, realpath, path, strict=True) ++ self.assertEqual(realpath(path, strict=ALLOW_MISSING), ABSTFNb + b'\\nonexistent') + ++ @unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname') ++ @_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING}) ++ def test_realpath_invalid_unicode_paths(self, kwargs): ++ realpath = ntpath.realpath ++ ABSTFN = ntpath.abspath(os_helper.TESTFN) ++ ABSTFNb = os.fsencode(ABSTFN) + path = ABSTFNb + b'\xff' +- self.assertRaises(UnicodeDecodeError, realpath, path, strict=False) +- self.assertRaises(UnicodeDecodeError, realpath, path, strict=True) ++ self.assertRaises(UnicodeDecodeError, realpath, path, **kwargs) ++ self.assertRaises(UnicodeDecodeError, realpath, path, **kwargs) + path = ABSTFNb + b'\\nonexistent\\\xff' +- self.assertRaises(UnicodeDecodeError, realpath, path, strict=False) +- self.assertRaises(UnicodeDecodeError, realpath, path, strict=True) ++ self.assertRaises(UnicodeDecodeError, realpath, path, **kwargs) ++ self.assertRaises(UnicodeDecodeError, realpath, path, **kwargs) + path = ABSTFNb + b'\xff\\..' +- self.assertRaises(UnicodeDecodeError, realpath, path, strict=False) +- self.assertRaises(UnicodeDecodeError, realpath, path, strict=True) ++ self.assertRaises(UnicodeDecodeError, realpath, path, **kwargs) ++ self.assertRaises(UnicodeDecodeError, realpath, path, **kwargs) + path = ABSTFNb + b'\\nonexistent\\\xff\\..' +- self.assertRaises(UnicodeDecodeError, realpath, path, strict=False) +- self.assertRaises(UnicodeDecodeError, realpath, path, strict=True) ++ self.assertRaises(UnicodeDecodeError, realpath, path, **kwargs) ++ self.assertRaises(UnicodeDecodeError, realpath, path, **kwargs) + + @os_helper.skip_unless_symlink + @unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname') +- def test_realpath_relative(self): ++ @_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING}) ++ def test_realpath_relative(self, kwargs): + ABSTFN = ntpath.abspath(os_helper.TESTFN) + open(ABSTFN, "wb").close() + self.addCleanup(os_helper.unlink, ABSTFN) + self.addCleanup(os_helper.unlink, ABSTFN + "1") + + os.symlink(ABSTFN, ntpath.relpath(ABSTFN + "1")) +- self.assertPathEqual(ntpath.realpath(ABSTFN + "1"), ABSTFN) ++ self.assertPathEqual(ntpath.realpath(ABSTFN + "1", **kwargs), ABSTFN) + + @os_helper.skip_unless_symlink + @unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname') +@@ -723,7 +816,62 @@ def test_realpath_symlink_loops_strict(self): + + @os_helper.skip_unless_symlink + @unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname') +- def test_realpath_symlink_prefix(self): ++ def test_realpath_symlink_loops_raise(self): ++ # Symlink loops raise OSError in ALLOW_MISSING mode ++ ABSTFN = ntpath.abspath(os_helper.TESTFN) ++ self.addCleanup(os_helper.unlink, ABSTFN) ++ self.addCleanup(os_helper.unlink, ABSTFN + "1") ++ self.addCleanup(os_helper.unlink, ABSTFN + "2") ++ self.addCleanup(os_helper.unlink, ABSTFN + "y") ++ self.addCleanup(os_helper.unlink, ABSTFN + "c") ++ self.addCleanup(os_helper.unlink, ABSTFN + "a") ++ self.addCleanup(os_helper.unlink, ABSTFN + "x") ++ ++ os.symlink(ABSTFN, ABSTFN) ++ self.assertRaises(OSError, ntpath.realpath, ABSTFN, strict=ALLOW_MISSING) ++ ++ os.symlink(ABSTFN + "1", ABSTFN + "2") ++ os.symlink(ABSTFN + "2", ABSTFN + "1") ++ self.assertRaises(OSError, ntpath.realpath, ABSTFN + "1", ++ strict=ALLOW_MISSING) ++ self.assertRaises(OSError, ntpath.realpath, ABSTFN + "2", ++ strict=ALLOW_MISSING) ++ self.assertRaises(OSError, ntpath.realpath, ABSTFN + "1\\x", ++ strict=ALLOW_MISSING) ++ ++ # Windows eliminates '..' components before resolving links; ++ # realpath is not expected to raise if this removes the loop. ++ self.assertPathEqual(ntpath.realpath(ABSTFN + "1\\.."), ++ ntpath.dirname(ABSTFN)) ++ self.assertPathEqual(ntpath.realpath(ABSTFN + "1\\..\\x"), ++ ntpath.dirname(ABSTFN) + "\\x") ++ ++ os.symlink(ABSTFN + "x", ABSTFN + "y") ++ self.assertPathEqual(ntpath.realpath(ABSTFN + "1\\..\\" ++ + ntpath.basename(ABSTFN) + "y"), ++ ABSTFN + "x") ++ self.assertRaises( ++ OSError, ntpath.realpath, ++ ABSTFN + "1\\..\\" + ntpath.basename(ABSTFN) + "1", ++ strict=ALLOW_MISSING) ++ ++ os.symlink(ntpath.basename(ABSTFN) + "a\\b", ABSTFN + "a") ++ self.assertRaises(OSError, ntpath.realpath, ABSTFN + "a", ++ strict=ALLOW_MISSING) ++ ++ os.symlink("..\\" + ntpath.basename(ntpath.dirname(ABSTFN)) ++ + "\\" + ntpath.basename(ABSTFN) + "c", ABSTFN + "c") ++ self.assertRaises(OSError, ntpath.realpath, ABSTFN + "c", ++ strict=ALLOW_MISSING) ++ ++ # Test using relative path as well. ++ self.assertRaises(OSError, ntpath.realpath, ntpath.basename(ABSTFN), ++ strict=ALLOW_MISSING) ++ ++ @os_helper.skip_unless_symlink ++ @unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname') ++ @_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING}) ++ def test_realpath_symlink_prefix(self, kwargs): + ABSTFN = ntpath.abspath(os_helper.TESTFN) + self.addCleanup(os_helper.unlink, ABSTFN + "3") + self.addCleanup(os_helper.unlink, "\\\\?\\" + ABSTFN + "3.") +@@ -738,9 +886,9 @@ def test_realpath_symlink_prefix(self): + f.write(b'1') + os.symlink("\\\\?\\" + ABSTFN + "3.", ABSTFN + "3.link") + +- self.assertPathEqual(ntpath.realpath(ABSTFN + "3link"), ++ self.assertPathEqual(ntpath.realpath(ABSTFN + "3link", **kwargs), + ABSTFN + "3") +- self.assertPathEqual(ntpath.realpath(ABSTFN + "3.link"), ++ self.assertPathEqual(ntpath.realpath(ABSTFN + "3.link", **kwargs), + "\\\\?\\" + ABSTFN + "3.") + + # Resolved paths should be usable to open target files +@@ -750,14 +898,17 @@ def test_realpath_symlink_prefix(self): + self.assertEqual(f.read(), b'1') + + # When the prefix is included, it is not stripped +- self.assertPathEqual(ntpath.realpath("\\\\?\\" + ABSTFN + "3link"), ++ self.assertPathEqual(ntpath.realpath("\\\\?\\" + ABSTFN + "3link", **kwargs), + "\\\\?\\" + ABSTFN + "3") +- self.assertPathEqual(ntpath.realpath("\\\\?\\" + ABSTFN + "3.link"), ++ self.assertPathEqual(ntpath.realpath("\\\\?\\" + ABSTFN + "3.link", **kwargs), + "\\\\?\\" + ABSTFN + "3.") + + @unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname') + def test_realpath_nul(self): + tester("ntpath.realpath('NUL')", r'\\.\NUL') ++ tester("ntpath.realpath('NUL', strict=False)", r'\\.\NUL') ++ tester("ntpath.realpath('NUL', strict=True)", r'\\.\NUL') ++ tester("ntpath.realpath('NUL', strict=ALLOW_MISSING)", r'\\.\NUL') + + @unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname') + @unittest.skipUnless(HAVE_GETSHORTPATHNAME, 'need _getshortpathname') +@@ -781,12 +932,20 @@ def test_realpath_cwd(self): + + self.assertPathEqual(test_file_long, ntpath.realpath(test_file_short)) + +- with os_helper.change_cwd(test_dir_long): +- self.assertPathEqual(test_file_long, ntpath.realpath("file.txt")) +- with os_helper.change_cwd(test_dir_long.lower()): +- self.assertPathEqual(test_file_long, ntpath.realpath("file.txt")) +- with os_helper.change_cwd(test_dir_short): +- self.assertPathEqual(test_file_long, ntpath.realpath("file.txt")) ++ for kwargs in {}, {'strict': True}, {'strict': ALLOW_MISSING}: ++ with self.subTest(**kwargs): ++ with os_helper.change_cwd(test_dir_long): ++ self.assertPathEqual( ++ test_file_long, ++ ntpath.realpath("file.txt", **kwargs)) ++ with os_helper.change_cwd(test_dir_long.lower()): ++ self.assertPathEqual( ++ test_file_long, ++ ntpath.realpath("file.txt", **kwargs)) ++ with os_helper.change_cwd(test_dir_short): ++ self.assertPathEqual( ++ test_file_long, ++ ntpath.realpath("file.txt", **kwargs)) + + @unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname') + def test_realpath_permission(self): +@@ -807,12 +966,15 @@ def test_realpath_permission(self): + # Automatic generation of short names may be disabled on + # NTFS volumes for the sake of performance. + # They're not supported at all on ReFS and exFAT. +- subprocess.run( ++ p = subprocess.run( + # Try to set the short name manually. + ['fsutil.exe', 'file', 'setShortName', test_file, 'LONGFI~1.TXT'], + creationflags=subprocess.DETACHED_PROCESS + ) + ++ if p.returncode: ++ raise unittest.SkipTest('failed to set short name') ++ + try: + self.assertPathEqual(test_file, ntpath.realpath(test_file_short)) + except AssertionError: +diff --git a/Lib/test/test_posixpath.py b/Lib/test/test_posixpath.py +index f3f9895f529470..c45ce6d3ef7820 100644 +--- a/Lib/test/test_posixpath.py ++++ b/Lib/test/test_posixpath.py +@@ -4,7 +4,8 @@ + import random + import sys + import unittest +-from posixpath import realpath, abspath, dirname, basename ++from functools import partial ++from posixpath import realpath, abspath, dirname, basename, ALLOW_MISSING + from test import support + from test import test_genericpath + from test.support import import_helper +@@ -33,6 +34,27 @@ def skip_if_ABSTFN_contains_backslash(test): + msg = "ABSTFN is not a posix path - tests fail" + return [test, unittest.skip(msg)(test)][found_backslash] + ++ ++def _parameterize(*parameters): ++ """Simplistic decorator to parametrize a test ++ ++ Runs the decorated test multiple times in subTest, with a value from ++ 'parameters' passed as an extra positional argument. ++ Does *not* call doCleanups() after each run. ++ ++ Not for general use. Intended to avoid indenting for easier backports. ++ ++ See https://discuss.python.org/t/91827 for discussing generalizations. ++ """ ++ def _parametrize_decorator(func): ++ def _parameterized(self, *args, **kwargs): ++ for parameter in parameters: ++ with self.subTest(parameter): ++ func(self, *args, parameter, **kwargs) ++ return _parameterized ++ return _parametrize_decorator ++ ++ + class PosixPathTest(unittest.TestCase): + + def setUp(self): +@@ -442,32 +464,35 @@ def test_normpath(self): + self.assertEqual(result, expected) + + @skip_if_ABSTFN_contains_backslash +- def test_realpath_curdir(self): +- self.assertEqual(realpath('.'), os.getcwd()) +- self.assertEqual(realpath('./.'), os.getcwd()) +- self.assertEqual(realpath('/'.join(['.'] * 100)), os.getcwd()) ++ @_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING}) ++ def test_realpath_curdir(self, kwargs): ++ self.assertEqual(realpath('.', **kwargs), os.getcwd()) ++ self.assertEqual(realpath('./.', **kwargs), os.getcwd()) ++ self.assertEqual(realpath('/'.join(['.'] * 100), **kwargs), os.getcwd()) + +- self.assertEqual(realpath(b'.'), os.getcwdb()) +- self.assertEqual(realpath(b'./.'), os.getcwdb()) +- self.assertEqual(realpath(b'/'.join([b'.'] * 100)), os.getcwdb()) ++ self.assertEqual(realpath(b'.', **kwargs), os.getcwdb()) ++ self.assertEqual(realpath(b'./.', **kwargs), os.getcwdb()) ++ self.assertEqual(realpath(b'/'.join([b'.'] * 100), **kwargs), os.getcwdb()) + + @skip_if_ABSTFN_contains_backslash +- def test_realpath_pardir(self): +- self.assertEqual(realpath('..'), dirname(os.getcwd())) +- self.assertEqual(realpath('../..'), dirname(dirname(os.getcwd()))) +- self.assertEqual(realpath('/'.join(['..'] * 100)), '/') ++ @_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING}) ++ def test_realpath_pardir(self, kwargs): ++ self.assertEqual(realpath('..', **kwargs), dirname(os.getcwd())) ++ self.assertEqual(realpath('../..', **kwargs), dirname(dirname(os.getcwd()))) ++ self.assertEqual(realpath('/'.join(['..'] * 100), **kwargs), '/') + +- self.assertEqual(realpath(b'..'), dirname(os.getcwdb())) +- self.assertEqual(realpath(b'../..'), dirname(dirname(os.getcwdb()))) +- self.assertEqual(realpath(b'/'.join([b'..'] * 100)), b'/') ++ self.assertEqual(realpath(b'..', **kwargs), dirname(os.getcwdb())) ++ self.assertEqual(realpath(b'../..', **kwargs), dirname(dirname(os.getcwdb()))) ++ self.assertEqual(realpath(b'/'.join([b'..'] * 100), **kwargs), b'/') + + @os_helper.skip_unless_symlink + @skip_if_ABSTFN_contains_backslash +- def test_realpath_basic(self): ++ @_parameterize({}, {'strict': ALLOW_MISSING}) ++ def test_realpath_basic(self, kwargs): + # Basic operation. + try: + os.symlink(ABSTFN+"1", ABSTFN) +- self.assertEqual(realpath(ABSTFN), ABSTFN+"1") ++ self.assertEqual(realpath(ABSTFN, **kwargs), ABSTFN+"1") + finally: + os_helper.unlink(ABSTFN) + +@@ -487,90 +512,115 @@ def test_realpath_invalid_paths(self): + path = '/\x00' + self.assertRaises(ValueError, realpath, path, strict=False) + self.assertRaises(ValueError, realpath, path, strict=True) ++ self.assertRaises(ValueError, realpath, path, strict=ALLOW_MISSING) + path = b'/\x00' + self.assertRaises(ValueError, realpath, path, strict=False) + self.assertRaises(ValueError, realpath, path, strict=True) ++ self.assertRaises(ValueError, realpath, path, strict=ALLOW_MISSING) + path = '/nonexistent/x\x00' + self.assertRaises(ValueError, realpath, path, strict=False) + self.assertRaises(FileNotFoundError, realpath, path, strict=True) ++ self.assertRaises(ValueError, realpath, path, strict=ALLOW_MISSING) + path = b'/nonexistent/x\x00' + self.assertRaises(ValueError, realpath, path, strict=False) + self.assertRaises(FileNotFoundError, realpath, path, strict=True) ++ self.assertRaises(ValueError, realpath, path, strict=ALLOW_MISSING) + path = '/\x00/..' + self.assertRaises(ValueError, realpath, path, strict=False) + self.assertRaises(ValueError, realpath, path, strict=True) ++ self.assertRaises(ValueError, realpath, path, strict=ALLOW_MISSING) + path = b'/\x00/..' + self.assertRaises(ValueError, realpath, path, strict=False) + self.assertRaises(ValueError, realpath, path, strict=True) ++ self.assertRaises(ValueError, realpath, path, strict=ALLOW_MISSING) ++ + path = '/nonexistent/x\x00/..' + self.assertRaises(ValueError, realpath, path, strict=False) + self.assertRaises(FileNotFoundError, realpath, path, strict=True) ++ self.assertRaises(ValueError, realpath, path, strict=ALLOW_MISSING) + path = b'/nonexistent/x\x00/..' + self.assertRaises(ValueError, realpath, path, strict=False) + self.assertRaises(FileNotFoundError, realpath, path, strict=True) ++ self.assertRaises(ValueError, realpath, path, strict=ALLOW_MISSING) + + path = '/\udfff' + if sys.platform == 'win32': + self.assertEqual(realpath(path, strict=False), path) + self.assertRaises(FileNotFoundError, realpath, path, strict=True) ++ self.assertEqual(realpath(path, strict=ALLOW_MISSING), path) + else: + self.assertRaises(UnicodeEncodeError, realpath, path, strict=False) + self.assertRaises(UnicodeEncodeError, realpath, path, strict=True) ++ self.assertRaises(UnicodeEncodeError, realpath, path, strict=ALLOW_MISSING) + path = '/nonexistent/\udfff' + if sys.platform == 'win32': + self.assertEqual(realpath(path, strict=False), path) ++ self.assertEqual(realpath(path, strict=ALLOW_MISSING), path) + else: + self.assertRaises(UnicodeEncodeError, realpath, path, strict=False) ++ self.assertRaises(UnicodeEncodeError, realpath, path, strict=ALLOW_MISSING) + self.assertRaises(FileNotFoundError, realpath, path, strict=True) + path = '/\udfff/..' + if sys.platform == 'win32': + self.assertEqual(realpath(path, strict=False), '/') + self.assertRaises(FileNotFoundError, realpath, path, strict=True) ++ self.assertEqual(realpath(path, strict=ALLOW_MISSING), '/') + else: + self.assertRaises(UnicodeEncodeError, realpath, path, strict=False) + self.assertRaises(UnicodeEncodeError, realpath, path, strict=True) ++ self.assertRaises(UnicodeEncodeError, realpath, path, strict=ALLOW_MISSING) + path = '/nonexistent/\udfff/..' + if sys.platform == 'win32': + self.assertEqual(realpath(path, strict=False), '/nonexistent') ++ self.assertEqual(realpath(path, strict=ALLOW_MISSING), '/nonexistent') + else: + self.assertRaises(UnicodeEncodeError, realpath, path, strict=False) ++ self.assertRaises(UnicodeEncodeError, realpath, path, strict=ALLOW_MISSING) + self.assertRaises(FileNotFoundError, realpath, path, strict=True) + + path = b'/\xff' + if sys.platform == 'win32': + self.assertRaises(UnicodeDecodeError, realpath, path, strict=False) + self.assertRaises(UnicodeDecodeError, realpath, path, strict=True) ++ self.assertRaises(UnicodeDecodeError, realpath, path, strict=ALLOW_MISSING) + else: + self.assertEqual(realpath(path, strict=False), path) + if support.is_wasi: + self.assertRaises(OSError, realpath, path, strict=True) ++ self.assertRaises(OSError, realpath, path, strict=ALLOW_MISSING) + else: + self.assertRaises(FileNotFoundError, realpath, path, strict=True) ++ self.assertEqual(realpath(path, strict=ALLOW_MISSING), path) + path = b'/nonexistent/\xff' + if sys.platform == 'win32': + self.assertRaises(UnicodeDecodeError, realpath, path, strict=False) ++ self.assertRaises(UnicodeDecodeError, realpath, path, strict=ALLOW_MISSING) + else: + self.assertEqual(realpath(path, strict=False), path) + if support.is_wasi: + self.assertRaises(OSError, realpath, path, strict=True) ++ self.assertRaises(OSError, realpath, path, strict=ALLOW_MISSING) + else: + self.assertRaises(FileNotFoundError, realpath, path, strict=True) + + @os_helper.skip_unless_symlink + @skip_if_ABSTFN_contains_backslash +- def test_realpath_relative(self): ++ @_parameterize({}, {'strict': ALLOW_MISSING}) ++ def test_realpath_relative(self, kwargs): + try: + os.symlink(posixpath.relpath(ABSTFN+"1"), ABSTFN) +- self.assertEqual(realpath(ABSTFN), ABSTFN+"1") ++ self.assertEqual(realpath(ABSTFN, **kwargs), ABSTFN+"1") + finally: + os_helper.unlink(ABSTFN) + + @os_helper.skip_unless_symlink + @skip_if_ABSTFN_contains_backslash +- def test_realpath_missing_pardir(self): ++ @_parameterize({}, {'strict': ALLOW_MISSING}) ++ def test_realpath_missing_pardir(self, kwargs): + try: + os.symlink(TESTFN + "1", TESTFN) +- self.assertEqual(realpath("nonexistent/../" + TESTFN), ABSTFN + "1") ++ self.assertEqual( ++ realpath("nonexistent/../" + TESTFN, **kwargs), ABSTFN + "1") + finally: + os_helper.unlink(TESTFN) + +@@ -617,37 +667,38 @@ def test_realpath_symlink_loops(self): + + @os_helper.skip_unless_symlink + @skip_if_ABSTFN_contains_backslash +- def test_realpath_symlink_loops_strict(self): ++ @_parameterize({'strict': True}, {'strict': ALLOW_MISSING}) ++ def test_realpath_symlink_loops_strict(self, kwargs): + # Bug #43757, raise OSError if we get into an infinite symlink loop in +- # strict mode. ++ # the strict modes. + try: + os.symlink(ABSTFN, ABSTFN) +- self.assertRaises(OSError, realpath, ABSTFN, strict=True) ++ self.assertRaises(OSError, realpath, ABSTFN, **kwargs) + + os.symlink(ABSTFN+"1", ABSTFN+"2") + os.symlink(ABSTFN+"2", ABSTFN+"1") +- self.assertRaises(OSError, realpath, ABSTFN+"1", strict=True) +- self.assertRaises(OSError, realpath, ABSTFN+"2", strict=True) ++ self.assertRaises(OSError, realpath, ABSTFN+"1", **kwargs) ++ self.assertRaises(OSError, realpath, ABSTFN+"2", **kwargs) + +- self.assertRaises(OSError, realpath, ABSTFN+"1/x", strict=True) +- self.assertRaises(OSError, realpath, ABSTFN+"1/..", strict=True) +- self.assertRaises(OSError, realpath, ABSTFN+"1/../x", strict=True) ++ self.assertRaises(OSError, realpath, ABSTFN+"1/x", **kwargs) ++ self.assertRaises(OSError, realpath, ABSTFN+"1/..", **kwargs) ++ self.assertRaises(OSError, realpath, ABSTFN+"1/../x", **kwargs) + os.symlink(ABSTFN+"x", ABSTFN+"y") + self.assertRaises(OSError, realpath, +- ABSTFN+"1/../" + basename(ABSTFN) + "y", strict=True) ++ ABSTFN+"1/../" + basename(ABSTFN) + "y", **kwargs) + self.assertRaises(OSError, realpath, +- ABSTFN+"1/../" + basename(ABSTFN) + "1", strict=True) ++ ABSTFN+"1/../" + basename(ABSTFN) + "1", **kwargs) + + os.symlink(basename(ABSTFN) + "a/b", ABSTFN+"a") +- self.assertRaises(OSError, realpath, ABSTFN+"a", strict=True) ++ self.assertRaises(OSError, realpath, ABSTFN+"a", **kwargs) + + os.symlink("../" + basename(dirname(ABSTFN)) + "/" + + basename(ABSTFN) + "c", ABSTFN+"c") +- self.assertRaises(OSError, realpath, ABSTFN+"c", strict=True) ++ self.assertRaises(OSError, realpath, ABSTFN+"c", **kwargs) + + # Test using relative path as well. + with os_helper.change_cwd(dirname(ABSTFN)): +- self.assertRaises(OSError, realpath, basename(ABSTFN), strict=True) ++ self.assertRaises(OSError, realpath, basename(ABSTFN), **kwargs) + finally: + os_helper.unlink(ABSTFN) + os_helper.unlink(ABSTFN+"1") +@@ -658,13 +709,14 @@ def test_realpath_symlink_loops_strict(self): + + @os_helper.skip_unless_symlink + @skip_if_ABSTFN_contains_backslash +- def test_realpath_repeated_indirect_symlinks(self): ++ @_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING}) ++ def test_realpath_repeated_indirect_symlinks(self, kwargs): + # Issue #6975. + try: + os.mkdir(ABSTFN) + os.symlink('../' + basename(ABSTFN), ABSTFN + '/self') + os.symlink('self/self/self', ABSTFN + '/link') +- self.assertEqual(realpath(ABSTFN + '/link'), ABSTFN) ++ self.assertEqual(realpath(ABSTFN + '/link', **kwargs), ABSTFN) + finally: + os_helper.unlink(ABSTFN + '/self') + os_helper.unlink(ABSTFN + '/link') +@@ -672,14 +724,15 @@ def test_realpath_repeated_indirect_symlinks(self): + + @os_helper.skip_unless_symlink + @skip_if_ABSTFN_contains_backslash +- def test_realpath_deep_recursion(self): ++ @_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING}) ++ def test_realpath_deep_recursion(self, kwargs): + depth = 10 + try: + os.mkdir(ABSTFN) + for i in range(depth): + os.symlink('/'.join(['%d' % i] * 10), ABSTFN + '/%d' % (i + 1)) + os.symlink('.', ABSTFN + '/0') +- self.assertEqual(realpath(ABSTFN + '/%d' % depth), ABSTFN) ++ self.assertEqual(realpath(ABSTFN + '/%d' % depth, **kwargs), ABSTFN) + + # Test using relative path as well. + with os_helper.change_cwd(ABSTFN): +@@ -691,7 +744,8 @@ def test_realpath_deep_recursion(self): + + @os_helper.skip_unless_symlink + @skip_if_ABSTFN_contains_backslash +- def test_realpath_resolve_parents(self): ++ @_parameterize({}, {'strict': ALLOW_MISSING}) ++ def test_realpath_resolve_parents(self, kwargs): + # We also need to resolve any symlinks in the parents of a relative + # path passed to realpath. E.g.: current working directory is + # /usr/doc with 'doc' being a symlink to /usr/share/doc. We call +@@ -702,7 +756,8 @@ def test_realpath_resolve_parents(self): + os.symlink(ABSTFN + "/y", ABSTFN + "/k") + + with os_helper.change_cwd(ABSTFN + "/k"): +- self.assertEqual(realpath("a"), ABSTFN + "/y/a") ++ self.assertEqual(realpath("a", **kwargs), ++ ABSTFN + "/y/a") + finally: + os_helper.unlink(ABSTFN + "/k") + os_helper.rmdir(ABSTFN + "/y") +@@ -710,7 +765,8 @@ def test_realpath_resolve_parents(self): + + @os_helper.skip_unless_symlink + @skip_if_ABSTFN_contains_backslash +- def test_realpath_resolve_before_normalizing(self): ++ @_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING}) ++ def test_realpath_resolve_before_normalizing(self, kwargs): + # Bug #990669: Symbolic links should be resolved before we + # normalize the path. E.g.: if we have directories 'a', 'k' and 'y' + # in the following hierarchy: +@@ -725,10 +781,10 @@ def test_realpath_resolve_before_normalizing(self): + os.symlink(ABSTFN + "/k/y", ABSTFN + "/link-y") + + # Absolute path. +- self.assertEqual(realpath(ABSTFN + "/link-y/.."), ABSTFN + "/k") ++ self.assertEqual(realpath(ABSTFN + "/link-y/..", **kwargs), ABSTFN + "/k") + # Relative path. + with os_helper.change_cwd(dirname(ABSTFN)): +- self.assertEqual(realpath(basename(ABSTFN) + "/link-y/.."), ++ self.assertEqual(realpath(basename(ABSTFN) + "/link-y/..", **kwargs), + ABSTFN + "/k") + finally: + os_helper.unlink(ABSTFN + "/link-y") +@@ -738,7 +794,8 @@ def test_realpath_resolve_before_normalizing(self): + + @os_helper.skip_unless_symlink + @skip_if_ABSTFN_contains_backslash +- def test_realpath_resolve_first(self): ++ @_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING}) ++ def test_realpath_resolve_first(self, kwargs): + # Bug #1213894: The first component of the path, if not absolute, + # must be resolved too. + +@@ -748,8 +805,8 @@ def test_realpath_resolve_first(self): + os.symlink(ABSTFN, ABSTFN + "link") + with os_helper.change_cwd(dirname(ABSTFN)): + base = basename(ABSTFN) +- self.assertEqual(realpath(base + "link"), ABSTFN) +- self.assertEqual(realpath(base + "link/k"), ABSTFN + "/k") ++ self.assertEqual(realpath(base + "link", **kwargs), ABSTFN) ++ self.assertEqual(realpath(base + "link/k", **kwargs), ABSTFN + "/k") + finally: + os_helper.unlink(ABSTFN + "link") + os_helper.rmdir(ABSTFN + "/k") +@@ -767,12 +824,67 @@ def test_realpath_unreadable_symlink(self): + self.assertEqual(realpath(ABSTFN + '/foo'), ABSTFN + '/foo') + self.assertEqual(realpath(ABSTFN + '/../foo'), dirname(ABSTFN) + '/foo') + self.assertEqual(realpath(ABSTFN + '/foo/..'), ABSTFN) +- with self.assertRaises(PermissionError): +- realpath(ABSTFN, strict=True) + finally: + os.chmod(ABSTFN, 0o755, follow_symlinks=False) + os_helper.unlink(ABSTFN) + ++ @os_helper.skip_unless_symlink ++ @skip_if_ABSTFN_contains_backslash ++ @unittest.skipIf(os.chmod not in os.supports_follow_symlinks, "Can't set symlink permissions") ++ @unittest.skipIf(sys.platform != "darwin", "only macOS requires read permission to readlink()") ++ @_parameterize({'strict': True}, {'strict': ALLOW_MISSING}) ++ def test_realpath_unreadable_symlink_strict(self, kwargs): ++ try: ++ os.symlink(ABSTFN+"1", ABSTFN) ++ os.chmod(ABSTFN, 0o000, follow_symlinks=False) ++ with self.assertRaises(PermissionError): ++ realpath(ABSTFN, **kwargs) ++ with self.assertRaises(PermissionError): ++ realpath(ABSTFN + '/foo', **kwargs), ++ with self.assertRaises(PermissionError): ++ realpath(ABSTFN + '/../foo', **kwargs) ++ with self.assertRaises(PermissionError): ++ realpath(ABSTFN + '/foo/..', **kwargs) ++ finally: ++ os.chmod(ABSTFN, 0o755, follow_symlinks=False) ++ os.unlink(ABSTFN) ++ ++ @skip_if_ABSTFN_contains_backslash ++ @os_helper.skip_unless_symlink ++ def test_realpath_unreadable_directory(self): ++ try: ++ os.mkdir(ABSTFN) ++ os.mkdir(ABSTFN + '/k') ++ os.chmod(ABSTFN, 0o000) ++ self.assertEqual(realpath(ABSTFN, strict=False), ABSTFN) ++ self.assertEqual(realpath(ABSTFN, strict=True), ABSTFN) ++ self.assertEqual(realpath(ABSTFN, strict=ALLOW_MISSING), ABSTFN) ++ ++ try: ++ os.stat(ABSTFN) ++ except PermissionError: ++ pass ++ else: ++ self.skipTest('Cannot block permissions') ++ ++ self.assertEqual(realpath(ABSTFN + '/k', strict=False), ++ ABSTFN + '/k') ++ self.assertRaises(PermissionError, realpath, ABSTFN + '/k', ++ strict=True) ++ self.assertRaises(PermissionError, realpath, ABSTFN + '/k', ++ strict=ALLOW_MISSING) ++ ++ self.assertEqual(realpath(ABSTFN + '/missing', strict=False), ++ ABSTFN + '/missing') ++ self.assertRaises(PermissionError, realpath, ABSTFN + '/missing', ++ strict=True) ++ self.assertRaises(PermissionError, realpath, ABSTFN + '/missing', ++ strict=ALLOW_MISSING) ++ finally: ++ os.chmod(ABSTFN, 0o755) ++ os_helper.rmdir(ABSTFN + '/k') ++ os_helper.rmdir(ABSTFN) ++ + @skip_if_ABSTFN_contains_backslash + def test_realpath_nonterminal_file(self): + try: +@@ -780,14 +892,27 @@ def test_realpath_nonterminal_file(self): + f.write('test_posixpath wuz ere') + self.assertEqual(realpath(ABSTFN, strict=False), ABSTFN) + self.assertEqual(realpath(ABSTFN, strict=True), ABSTFN) ++ self.assertEqual(realpath(ABSTFN, strict=ALLOW_MISSING), ABSTFN) ++ + self.assertEqual(realpath(ABSTFN + "/", strict=False), ABSTFN) + self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/", strict=True) ++ self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/", ++ strict=ALLOW_MISSING) ++ + self.assertEqual(realpath(ABSTFN + "/.", strict=False), ABSTFN) + self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/.", strict=True) ++ self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/.", ++ strict=ALLOW_MISSING) ++ + self.assertEqual(realpath(ABSTFN + "/..", strict=False), dirname(ABSTFN)) + self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/..", strict=True) ++ self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/..", ++ strict=ALLOW_MISSING) ++ + self.assertEqual(realpath(ABSTFN + "/subdir", strict=False), ABSTFN + "/subdir") + self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/subdir", strict=True) ++ self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/subdir", ++ strict=ALLOW_MISSING) + finally: + os_helper.unlink(ABSTFN) + +@@ -800,14 +925,27 @@ def test_realpath_nonterminal_symlink_to_file(self): + os.symlink(ABSTFN + "1", ABSTFN) + self.assertEqual(realpath(ABSTFN, strict=False), ABSTFN + "1") + self.assertEqual(realpath(ABSTFN, strict=True), ABSTFN + "1") ++ self.assertEqual(realpath(ABSTFN, strict=ALLOW_MISSING), ABSTFN + "1") ++ + self.assertEqual(realpath(ABSTFN + "/", strict=False), ABSTFN + "1") + self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/", strict=True) ++ self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/", ++ strict=ALLOW_MISSING) ++ + self.assertEqual(realpath(ABSTFN + "/.", strict=False), ABSTFN + "1") + self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/.", strict=True) ++ self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/.", ++ strict=ALLOW_MISSING) ++ + self.assertEqual(realpath(ABSTFN + "/..", strict=False), dirname(ABSTFN)) + self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/..", strict=True) ++ self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/..", ++ strict=ALLOW_MISSING) ++ + self.assertEqual(realpath(ABSTFN + "/subdir", strict=False), ABSTFN + "1/subdir") + self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/subdir", strict=True) ++ self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/subdir", ++ strict=ALLOW_MISSING) + finally: + os_helper.unlink(ABSTFN) + os_helper.unlink(ABSTFN + "1") +@@ -822,14 +960,27 @@ def test_realpath_nonterminal_symlink_to_symlinks_to_file(self): + os.symlink(ABSTFN + "1", ABSTFN) + self.assertEqual(realpath(ABSTFN, strict=False), ABSTFN + "2") + self.assertEqual(realpath(ABSTFN, strict=True), ABSTFN + "2") ++ self.assertEqual(realpath(ABSTFN, strict=True), ABSTFN + "2") ++ + self.assertEqual(realpath(ABSTFN + "/", strict=False), ABSTFN + "2") + self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/", strict=True) ++ self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/", ++ strict=ALLOW_MISSING) ++ + self.assertEqual(realpath(ABSTFN + "/.", strict=False), ABSTFN + "2") + self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/.", strict=True) ++ self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/.", ++ strict=ALLOW_MISSING) ++ + self.assertEqual(realpath(ABSTFN + "/..", strict=False), dirname(ABSTFN)) + self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/..", strict=True) ++ self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/..", ++ strict=ALLOW_MISSING) ++ + self.assertEqual(realpath(ABSTFN + "/subdir", strict=False), ABSTFN + "2/subdir") + self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/subdir", strict=True) ++ self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/subdir", ++ strict=ALLOW_MISSING) + finally: + os_helper.unlink(ABSTFN) + os_helper.unlink(ABSTFN + "1") +@@ -1017,9 +1168,12 @@ def test_path_normpath(self): + def test_path_abspath(self): + self.assertPathEqual(self.path.abspath) + +- def test_path_realpath(self): ++ @_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING}) ++ def test_path_realpath(self, kwargs): + self.assertPathEqual(self.path.realpath) + ++ self.assertPathEqual(partial(self.path.realpath, **kwargs)) ++ + def test_path_relpath(self): + self.assertPathEqual(self.path.relpath) + +diff --git a/Lib/test/test_tarfile.py b/Lib/test/test_tarfile.py +index cf218a2bf14369..7055e1ed147a9e 100644 +--- a/Lib/test/test_tarfile.py ++++ b/Lib/test/test_tarfile.py +@@ -2715,6 +2715,31 @@ def test_useful_error_message_when_modules_missing(self): + str(excinfo.exception), + ) + ++ @unittest.skipUnless(os_helper.can_symlink(), 'requires symlink support') ++ @unittest.skipUnless(hasattr(os, 'chmod'), "missing os.chmod") ++ @unittest.mock.patch('os.chmod') ++ def test_deferred_directory_attributes_update(self, mock_chmod): ++ # Regression test for gh-127987: setting attributes on arbitrary files ++ tempdir = os.path.join(TEMPDIR, 'test127987') ++ def mock_chmod_side_effect(path, mode, **kwargs): ++ target_path = os.path.realpath(path) ++ if os.path.commonpath([target_path, tempdir]) != tempdir: ++ raise Exception("should not try to chmod anything outside the destination", target_path) ++ mock_chmod.side_effect = mock_chmod_side_effect ++ ++ outside_tree_dir = os.path.join(TEMPDIR, 'outside_tree_dir') ++ with ArchiveMaker() as arc: ++ arc.add('x', symlink_to='.') ++ arc.add('x', type=tarfile.DIRTYPE, mode='?rwsrwsrwt') ++ arc.add('x', symlink_to=outside_tree_dir) ++ ++ os.makedirs(outside_tree_dir) ++ try: ++ arc.open().extractall(path=tempdir, filter='tar') ++ finally: ++ os_helper.rmtree(outside_tree_dir) ++ os_helper.rmtree(tempdir) ++ + + class CommandLineTest(unittest.TestCase): + +@@ -3275,6 +3300,10 @@ def check_files_present(self, directory): + got_paths = set( + p.relative_to(directory) + for p in pathlib.Path(directory).glob('**/*')) ++ if self.extraction_filter in (None, 'data'): ++ # The 'data' filter is expected to reject special files ++ for path in 'ustar/fifotype', 'ustar/blktype', 'ustar/chrtype': ++ got_paths.discard(pathlib.Path(path)) + self.assertEqual(self.control_paths, got_paths) + + @contextmanager +@@ -3504,12 +3533,28 @@ def __exit__(self, *exc): + self.bio = None + + def add(self, name, *, type=None, symlink_to=None, hardlink_to=None, +- mode=None, size=None, **kwargs): +- """Add a member to the test archive. Call within `with`.""" ++ mode=None, size=None, content=None, **kwargs): ++ """Add a member to the test archive. Call within `with`. ++ ++ Provides many shortcuts: ++ - default `type` is based on symlink_to, hardlink_to, and trailing `/` ++ in name (which is stripped) ++ - size & content defaults are based on each other ++ - content can be str or bytes ++ - mode should be textual ('-rwxrwxrwx') ++ ++ (add more! this is unstable internal test-only API) ++ """ + name = str(name) + tarinfo = tarfile.TarInfo(name).replace(**kwargs) ++ if content is not None: ++ if isinstance(content, str): ++ content = content.encode() ++ size = len(content) + if size is not None: + tarinfo.size = size ++ if content is None: ++ content = bytes(tarinfo.size) + if mode: + tarinfo.mode = _filemode_to_int(mode) + if symlink_to is not None: +@@ -3523,7 +3568,7 @@ def add(self, name, *, type=None, symlink_to=None, hardlink_to=None, + if type is not None: + tarinfo.type = type + if tarinfo.isreg(): +- fileobj = io.BytesIO(bytes(tarinfo.size)) ++ fileobj = io.BytesIO(content) + else: + fileobj = None + self.tar_w.addfile(tarinfo, fileobj) +@@ -3557,7 +3602,7 @@ class TestExtractionFilters(unittest.TestCase): + destdir = outerdir / 'dest' + + @contextmanager +- def check_context(self, tar, filter): ++ def check_context(self, tar, filter, *, check_flag=True): + """Extracts `tar` to `self.destdir` and allows checking the result + + If an error occurs, it must be checked using `expect_exception` +@@ -3566,27 +3611,40 @@ def check_context(self, tar, filter): + except the destination directory itself and parent directories of + other files. + When checking directories, do so before their contents. ++ ++ A file called 'flag' is made in outerdir (i.e. outside destdir) ++ before extraction; it should not be altered nor should its contents ++ be read/copied. + """ + with os_helper.temp_dir(self.outerdir): ++ flag_path = self.outerdir / 'flag' ++ flag_path.write_text('capture me') + try: + tar.extractall(self.destdir, filter=filter) + except Exception as exc: + self.raised_exception = exc ++ self.reraise_exception = True + self.expected_paths = set() + else: + self.raised_exception = None ++ self.reraise_exception = False + self.expected_paths = set(self.outerdir.glob('**/*')) + self.expected_paths.discard(self.destdir) ++ self.expected_paths.discard(flag_path) + try: +- yield ++ yield self + finally: + tar.close() +- if self.raised_exception: ++ if self.reraise_exception: + raise self.raised_exception + self.assertEqual(self.expected_paths, set()) ++ if check_flag: ++ self.assertEqual(flag_path.read_text(), 'capture me') ++ else: ++ assert filter == 'fully_trusted' + + def expect_file(self, name, type=None, symlink_to=None, mode=None, +- size=None): ++ size=None, content=None): + """Check a single file. See check_context.""" + if self.raised_exception: + raise self.raised_exception +@@ -3605,26 +3663,45 @@ def expect_file(self, name, type=None, symlink_to=None, mode=None, + # The symlink might be the same (textually) as what we expect, + # but some systems change the link to an equivalent path, so + # we fall back to samefile(). +- if expected != got: +- self.assertTrue(got.samefile(expected)) ++ try: ++ if expected != got: ++ self.assertTrue(got.samefile(expected)) ++ except Exception as e: ++ # attach a note, so it's shown even if `samefile` fails ++ e.add_note(f'{expected=}, {got=}') ++ raise + elif type == tarfile.REGTYPE or type is None: + self.assertTrue(path.is_file()) + elif type == tarfile.DIRTYPE: + self.assertTrue(path.is_dir()) + elif type == tarfile.FIFOTYPE: + self.assertTrue(path.is_fifo()) ++ elif type == tarfile.SYMTYPE: ++ self.assertTrue(path.is_symlink()) + else: + raise NotImplementedError(type) + if size is not None: + self.assertEqual(path.stat().st_size, size) ++ if content is not None: ++ self.assertEqual(path.read_text(), content) + for parent in path.parents: + self.expected_paths.discard(parent) + ++ def expect_any_tree(self, name): ++ """Check a directory; forget about its contents.""" ++ tree_path = (self.destdir / name).resolve() ++ self.expect_file(tree_path, type=tarfile.DIRTYPE) ++ self.expected_paths = { ++ p for p in self.expected_paths ++ if tree_path not in p.parents ++ } ++ + def expect_exception(self, exc_type, message_re='.'): + with self.assertRaisesRegex(exc_type, message_re): + if self.raised_exception is not None: + raise self.raised_exception +- self.raised_exception = None ++ self.reraise_exception = False ++ return self.raised_exception + + def test_benign_file(self): + with ArchiveMaker() as arc: +@@ -3709,6 +3786,80 @@ def test_parent_symlink(self): + with self.check_context(arc.open(), 'data'): + self.expect_file('parent/evil') + ++ @symlink_test ++ @os_helper.skip_unless_symlink ++ def test_realpath_limit_attack(self): ++ # (CVE-2025-4517) ++ ++ with ArchiveMaker() as arc: ++ # populate the symlinks and dirs that expand in os.path.realpath() ++ # The component length is chosen so that in common cases, the unexpanded ++ # path fits in PATH_MAX, but it overflows when the final symlink ++ # is expanded ++ steps = "abcdefghijklmnop" ++ if sys.platform == 'win32': ++ component = 'd' * 25 ++ elif 'PC_PATH_MAX' in os.pathconf_names: ++ max_path_len = os.pathconf(self.outerdir.parent, "PC_PATH_MAX") ++ path_sep_len = 1 ++ dest_len = len(str(self.destdir)) + path_sep_len ++ component_len = (max_path_len - dest_len) // (len(steps) + path_sep_len) ++ component = 'd' * component_len ++ else: ++ raise NotImplementedError("Need to guess component length for {sys.platform}") ++ path = "" ++ step_path = "" ++ for i in steps: ++ arc.add(os.path.join(path, component), type=tarfile.DIRTYPE, ++ mode='drwxrwxrwx') ++ arc.add(os.path.join(path, i), symlink_to=component) ++ path = os.path.join(path, component) ++ step_path = os.path.join(step_path, i) ++ # create the final symlink that exceeds PATH_MAX and simply points ++ # to the top dir. ++ # this link will never be expanded by ++ # os.path.realpath(strict=False), nor anything after it. ++ linkpath = os.path.join(*steps, "l"*254) ++ parent_segments = [".."] * len(steps) ++ arc.add(linkpath, symlink_to=os.path.join(*parent_segments)) ++ # make a symlink outside to keep the tar command happy ++ arc.add("escape", symlink_to=os.path.join(linkpath, "..")) ++ # use the symlinks above, that are not checked, to create a hardlink ++ # to a file outside of the destination path ++ arc.add("flaglink", hardlink_to=os.path.join("escape", "flag")) ++ # now that we have the hardlink we can overwrite the file ++ arc.add("flaglink", content='overwrite') ++ # we can also create new files as well! ++ arc.add("escape/newfile", content='new') ++ ++ with (self.subTest('fully_trusted'), ++ self.check_context(arc.open(), filter='fully_trusted', ++ check_flag=False)): ++ if sys.platform == 'win32': ++ self.expect_exception((FileNotFoundError, FileExistsError)) ++ elif self.raised_exception: ++ # Cannot symlink/hardlink: tarfile falls back to getmember() ++ self.expect_exception(KeyError) ++ # Otherwise, this block should never enter. ++ else: ++ self.expect_any_tree(component) ++ self.expect_file('flaglink', content='overwrite') ++ self.expect_file('../newfile', content='new') ++ self.expect_file('escape', type=tarfile.SYMTYPE) ++ self.expect_file('a', symlink_to=component) ++ ++ for filter in 'tar', 'data': ++ with self.subTest(filter), self.check_context(arc.open(), filter=filter): ++ exc = self.expect_exception((OSError, KeyError)) ++ if isinstance(exc, OSError): ++ if sys.platform == 'win32': ++ # 3: ERROR_PATH_NOT_FOUND ++ # 5: ERROR_ACCESS_DENIED ++ # 206: ERROR_FILENAME_EXCED_RANGE ++ self.assertIn(exc.winerror, (3, 5, 206)) ++ else: ++ self.assertEqual(exc.errno, errno.ENAMETOOLONG) ++ + @symlink_test + def test_parent_symlink2(self): + # Test interplaying symlinks +@@ -3931,8 +4082,8 @@ def test_chains(self): + arc.add('symlink2', symlink_to=os.path.join( + 'linkdir', 'hardlink2')) + arc.add('targetdir/target', size=3) +- arc.add('linkdir/hardlink', hardlink_to='targetdir/target') +- arc.add('linkdir/hardlink2', hardlink_to='linkdir/symlink') ++ arc.add('linkdir/hardlink', hardlink_to=os.path.join('targetdir', 'target')) ++ arc.add('linkdir/hardlink2', hardlink_to=os.path.join('linkdir', 'symlink')) + + for filter in 'tar', 'data', 'fully_trusted': + with self.check_context(arc.open(), filter): +@@ -3948,6 +4099,129 @@ def test_chains(self): + self.expect_file('linkdir/symlink', size=3) + self.expect_file('symlink2', size=3) + ++ @symlink_test ++ def test_sneaky_hardlink_fallback(self): ++ # (CVE-2025-4330) ++ # Test that when hardlink extraction falls back to extracting members ++ # from the archive, the extracted member is (re-)filtered. ++ with ArchiveMaker() as arc: ++ # Create a directory structure so the c/escape symlink stays ++ # inside the path ++ arc.add("a/t/dummy") ++ # Create b/ directory ++ arc.add("b/") ++ # Point "c" to the bottom of the tree in "a" ++ arc.add("c", symlink_to=os.path.join("a", "t")) ++ # link to non-existant location under "a" ++ arc.add("c/escape", symlink_to=os.path.join("..", "..", ++ "link_here")) ++ # Move "c" to point to "b" ("c/escape" no longer exists) ++ arc.add("c", symlink_to="b") ++ # Attempt to create a hard link to "c/escape". Since it doesn't ++ # exist it will attempt to extract "cescape" but at "boom". ++ arc.add("boom", hardlink_to=os.path.join("c", "escape")) ++ ++ with self.check_context(arc.open(), 'data'): ++ if not os_helper.can_symlink(): ++ # When 'c/escape' is extracted, 'c' is a regular ++ # directory, and 'c/escape' *would* point outside ++ # the destination if symlinks were allowed. ++ self.expect_exception( ++ tarfile.LinkOutsideDestinationError) ++ elif sys.platform == "win32": ++ # On Windows, 'c/escape' points outside the destination ++ self.expect_exception(tarfile.LinkOutsideDestinationError) ++ else: ++ e = self.expect_exception( ++ tarfile.LinkFallbackError, ++ "link 'boom' would be extracted as a copy of " ++ + "'c/escape', which was rejected") ++ self.assertIsInstance(e.__cause__, ++ tarfile.LinkOutsideDestinationError) ++ for filter in 'tar', 'fully_trusted': ++ with self.subTest(filter), self.check_context(arc.open(), filter): ++ if not os_helper.can_symlink(): ++ self.expect_file("a/t/dummy") ++ self.expect_file("b/") ++ self.expect_file("c/") ++ else: ++ self.expect_file("a/t/dummy") ++ self.expect_file("b/") ++ self.expect_file("a/t/escape", symlink_to='../../link_here') ++ self.expect_file("boom", symlink_to='../../link_here') ++ self.expect_file("c", symlink_to='b') ++ ++ @symlink_test ++ def test_exfiltration_via_symlink(self): ++ # (CVE-2025-4138) ++ # Test changing symlinks that result in a symlink pointing outside ++ # the extraction directory, unless prevented by 'data' filter's ++ # normalization. ++ with ArchiveMaker() as arc: ++ arc.add("escape", symlink_to=os.path.join('link', 'link', '..', '..', 'link-here')) ++ arc.add("link", symlink_to='./') ++ ++ for filter in 'tar', 'data', 'fully_trusted': ++ with self.check_context(arc.open(), filter): ++ if os_helper.can_symlink(): ++ self.expect_file("link", symlink_to='./') ++ if filter == 'data': ++ self.expect_file("escape", symlink_to='link-here') ++ else: ++ self.expect_file("escape", ++ symlink_to='link/link/../../link-here') ++ else: ++ # Nothing is extracted. ++ pass ++ ++ @symlink_test ++ def test_chmod_outside_dir(self): ++ # (CVE-2024-12718) ++ # Test that members used for delayed updates of directory metadata ++ # are (re-)filtered. ++ with ArchiveMaker() as arc: ++ # "pwn" is a veeeery innocent symlink: ++ arc.add("a/pwn", symlink_to='.') ++ # But now "pwn" is also a directory, so it's scheduled to have its ++ # metadata updated later: ++ arc.add("a/pwn/", mode='drwxrwxrwx') ++ # Oops, "pwn" is not so innocent any more: ++ arc.add("a/pwn", symlink_to='x/../') ++ # Newly created symlink points to the dest dir, ++ # so it's OK for the "data" filter. ++ arc.add('a/x', symlink_to=('../')) ++ # But now "pwn" points outside the dest dir ++ ++ for filter in 'tar', 'data', 'fully_trusted': ++ with self.check_context(arc.open(), filter) as cc: ++ if not os_helper.can_symlink(): ++ self.expect_file("a/pwn/") ++ elif filter == 'data': ++ self.expect_file("a/x", symlink_to='../') ++ self.expect_file("a/pwn", symlink_to='.') ++ else: ++ self.expect_file("a/x", symlink_to='../') ++ self.expect_file("a/pwn", symlink_to='x/../') ++ if sys.platform != "win32": ++ st_mode = cc.outerdir.stat().st_mode ++ self.assertNotEqual(st_mode & 0o777, 0o777) ++ ++ def test_link_fallback_normalizes(self): ++ # Make sure hardlink fallbacks work for non-normalized paths for all ++ # filters ++ with ArchiveMaker() as arc: ++ arc.add("dir/") ++ arc.add("dir/../afile") ++ arc.add("link1", hardlink_to='dir/../afile') ++ arc.add("link2", hardlink_to='dir/../dir/../afile') ++ ++ for filter in 'tar', 'data', 'fully_trusted': ++ with self.check_context(arc.open(), filter) as cc: ++ self.expect_file("dir/") ++ self.expect_file("afile") ++ self.expect_file("link1") ++ self.expect_file("link2") ++ + def test_modes(self): + # Test how file modes are extracted + # (Note that the modes are ignored on platforms without working chmod) +@@ -4072,7 +4346,7 @@ def test_tar_filter(self): + # The 'tar' filter returns TarInfo objects with the same name/type. + # (It can also fail for particularly "evil" input, but we don't have + # that in the test archive.) +- with tarfile.TarFile.open(tarname) as tar: ++ with tarfile.TarFile.open(tarname, encoding="iso8859-1") as tar: + for tarinfo in tar.getmembers(): + try: + filtered = tarfile.tar_filter(tarinfo, '') +@@ -4084,7 +4358,7 @@ def test_tar_filter(self): + def test_data_filter(self): + # The 'data' filter either raises, or returns TarInfo with the same + # name/type. +- with tarfile.TarFile.open(tarname) as tar: ++ with tarfile.TarFile.open(tarname, encoding="iso8859-1") as tar: + for tarinfo in tar.getmembers(): + try: + filtered = tarfile.data_filter(tarinfo, '') +@@ -4242,13 +4516,13 @@ def valueerror_filter(tarinfo, path): + # If errorlevel is 0, errors affected by errorlevel are ignored + + with self.check_context(arc.open(errorlevel=0), extracterror_filter): +- self.expect_file('file') ++ pass + + with self.check_context(arc.open(errorlevel=0), filtererror_filter): +- self.expect_file('file') ++ pass + + with self.check_context(arc.open(errorlevel=0), oserror_filter): +- self.expect_file('file') ++ pass + + with self.check_context(arc.open(errorlevel=0), tarerror_filter): + self.expect_exception(tarfile.TarError) +@@ -4259,7 +4533,7 @@ def valueerror_filter(tarinfo, path): + # If 1, all fatal errors are raised + + with self.check_context(arc.open(errorlevel=1), extracterror_filter): +- self.expect_file('file') ++ pass + + with self.check_context(arc.open(errorlevel=1), filtererror_filter): + self.expect_exception(tarfile.FilterError) +diff --git a/Misc/NEWS.d/next/Security/2025-06-02-11-32-23.gh-issue-135034.RLGjbp.rst b/Misc/NEWS.d/next/Security/2025-06-02-11-32-23.gh-issue-135034.RLGjbp.rst +new file mode 100644 +index 00000000000000..08a0087e203671 +--- /dev/null ++++ b/Misc/NEWS.d/next/Security/2025-06-02-11-32-23.gh-issue-135034.RLGjbp.rst +@@ -0,0 +1,6 @@ ++Fixes multiple issues that allowed ``tarfile`` extraction filters ++(``filter="data"`` and ``filter="tar"``) to be bypassed using crafted ++symlinks and hard links. ++ ++Addresses :cve:`2024-12718`, :cve:`2025-4138`, :cve:`2025-4330`, and :cve:`2025-4517`. ++ diff --git a/pkgs/development/interpreters/python/cpython/default.nix b/pkgs/development/interpreters/python/cpython/default.nix index 03d0383402fd..4cf35d59ad74 100644 --- a/pkgs/development/interpreters/python/cpython/default.nix +++ b/pkgs/development/interpreters/python/cpython/default.nix @@ -349,6 +349,9 @@ stdenv.mkDerivation (finalAttrs: { ++ optionals (pythonAtLeast "3.13") [ ./3.13/virtualenv-permissions.patch ] + ++ optionals (pythonAtLeast "3.14") [ + ./3.14/CVE-2025-4517.patch + ] ++ optionals mimetypesSupport [ # Make the mimetypes module refer to the right file ./mimetypes.patch diff --git a/pkgs/development/python-modules/pyngrok/default.nix b/pkgs/development/python-modules/pyngrok/default.nix index 619d98e60636..5a6f3d32b490 100644 --- a/pkgs/development/python-modules/pyngrok/default.nix +++ b/pkgs/development/python-modules/pyngrok/default.nix @@ -9,14 +9,14 @@ buildPythonPackage rec { pname = "pyngrok"; - version = "7.2.9"; + version = "7.2.11"; pyproject = true; disabled = pythonOlder "3.8"; src = fetchPypi { inherit pname version; - hash = "sha256-Ixt/1Kel6CZajWFQ07o8gtnf/UCe2u23LE5isy3kT5E="; + hash = "sha256-n4iQOJvZWrwA7KE4Rsj0haDbZmfcBZh+uY/eNDrfslo="; }; build-system = [ diff --git a/pkgs/os-specific/linux/prl-tools/default.nix b/pkgs/os-specific/linux/prl-tools/default.nix index 4e7b391f840a..ba2fe72e1277 100644 --- a/pkgs/os-specific/linux/prl-tools/default.nix +++ b/pkgs/os-specific/linux/prl-tools/default.nix @@ -43,13 +43,13 @@ let in stdenv.mkDerivation (finalAttrs: { pname = "prl-tools"; - version = "20.3.1-55959"; + version = "20.3.2-55975"; # We download the full distribution to extract prl-tools-lin.iso from # => ${dmg}/Parallels\ Desktop.app/Contents/Resources/Tools/prl-tools-lin.iso src = fetchurl { url = "https://download.parallels.com/desktop/v${lib.versions.major finalAttrs.version}/${finalAttrs.version}/ParallelsDesktop-${finalAttrs.version}.dmg"; - hash = "sha256-/J6NouI2htptG06Undeg1rUfbWUu6q/nV2P9D+sS7OA="; + hash = "sha256-eazDR+eSUcp81XdRfYRHIt7E4FNCEjsh0M0wYQQYmMQ="; }; hardeningDisable = [ diff --git a/pkgs/servers/sql/postgresql/ext/omnigres.nix b/pkgs/servers/sql/postgresql/ext/omnigres.nix index c7e967882413..c422de9becd9 100644 --- a/pkgs/servers/sql/postgresql/ext/omnigres.nix +++ b/pkgs/servers/sql/postgresql/ext/omnigres.nix @@ -12,6 +12,7 @@ postgresqlBuildExtension, postgresqlTestExtension, python3, + unstableGitUpdater, }: let @@ -19,13 +20,13 @@ let in postgresqlBuildExtension (finalAttrs: { pname = "omnigres"; - version = "0-unstable-2025-05-16"; + version = "0-unstable-2025-06-03"; src = fetchFromGitHub { owner = "omnigres"; repo = "omnigres"; - rev = "84f14792d80fb6fd60b680b7825245a8e7c5583e"; - hash = "sha256-jOlHXl7ANhMwOPizd5KH+wYZmBNNkkIa9jbXZR8Xu28="; + rev = "d347be5ae1d79645ac277d19080eacba7b229cf8"; + hash = "sha256-LKsH+aeLg7v2RfK80D3mgXdPB8jMIv5uFdf+3c5Z0vA="; }; strictDeps = true; @@ -75,6 +76,10 @@ postgresqlBuildExtension (finalAttrs: { ''; }; + passthru.updateScript = unstableGitUpdater { + hardcodeZeroVersion = true; + }; + meta = { description = "Postgres as a Business Operating System"; homepage = "https://docs.omnigres.org";