Compare commits

...
Author SHA1 Message Date
devydave 0835c4ea22 fix: reproducable ssh-add
continuous-integration/drone/push Build is passing
2026-10-30 00:10:09 +01:00
devydave c77e0f1923 docs: adjust get started
continuous-integration/drone/push Build is passing
2026-10-29 23:55:36 +01:00
devydave 1ce4cd38d4 refactor: move ssh-key add to nix specific run
continuous-integration/drone/push Build is passing
2026-10-29 23:43:26 +01:00
devydave d024d21766 refactor: move integration testing script to nix
continuous-integration/drone/push Build is passing
2026-10-29 23:27:57 +01:00
devydave b34d9c6373 Merge branch 'main' into local-integration-testing
continuous-integration/drone/push Build is passing
2026-10-29 22:05:53 +01:00
renovate-bot 1bffd1831d chore(deps): update nginx docker tag to v1.31.8
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2026-10-21 23:01:23 +00:00
renovate-bot eb62135e3b chore(deps): update otel/weaver docker tag to v0.28.0
continuous-integration/drone/push Build is passing
2026-10-15 03:01:18 +00:00
Weblate e8a494d9f4 chore: update translation files
continuous-integration/drone/push Build is passing
Updated by "Update PO files to match POT (msgmerge)" add-on in Weblate.

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/
2026-10-15 02:55:54 +00:00
3wordchant 35c29580bb fix: Run make i18n
continuous-integration/drone/push Build is passing
2026-10-15 02:55:38 +00:00
renovate-bot e458c531f7 chore(deps): update nginx docker tag to v1.31.7
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2026-10-14 20:01:20 +00:00
renovate-bot d0d156dc30 chore(deps): update golang docker tag
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2026-10-14 16:01:23 +00:00
renovate-bot 14f05407f3 chore(deps): update module github.com/leonelquinteros/gotext to v1.8.0
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2026-10-14 15:02:32 +00:00
jade e611a3be38 Sort invalid versions to the bottom rather than panicking
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2026-10-14 17:13:01 +11:00
renovate-bot b5f7d26c9d chore(deps): update tonistiigi/xx docker tag to v1.10.0
continuous-integration/drone/push Build is passing
2026-10-12 11:01:13 +00:00
renovate-bot 5b42b3d306 chore(deps): update golang docker tag
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2026-10-09 17:01:20 +00:00
renovate-bot d99e86fb0e chore(deps): update module golang.org/x/term to v0.47.0
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2026-10-09 16:01:52 +00:00
renovate-bot ade646abbc chore(deps): update golang docker tag
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2026-10-09 14:01:30 +00:00
renovate-bot 80b3b90905 chore(deps): update module golang.org/x/sys to v0.49.0
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2026-10-09 13:01:52 +00:00
devydave a2647c3331 refactor: add option to run specific test files, file mode ssh key and ignore changes to path_to_file
continuous-integration/drone/push Build is passing
2026-08-31 10:59:02 +02:00
devydave d510e13461 docs: explains -t option
continuous-integration/drone/push Build is passing
2026-08-24 23:31:12 +02:00
devydave 470a34fdb1 test: adds automation script and ignore path_to_repo changes 2026-08-24 23:29:30 +02:00
devydave a28cc7afce docs: more formatting and typo
continuous-integration/drone/push Build is passing
2026-08-13 20:41:04 +02:00
devydave 21dccc895d docs: formatting
continuous-integration/drone/push Build is passing
2026-08-13 20:38:20 +02:00
devydave bb3c7576f8 docs: add setup documentation
continuous-integration/drone/push Build is passing
2026-08-13 20:34:14 +02:00
devydave 02cbf7c014 refactor: make path editable without nix and impure 2026-08-13 19:57:51 +02:00
devydave a8c8df8604 fix: remove personal terminal choice
continuous-integration/drone/push Build is passing
2026-08-12 23:01:12 +02:00
devydave 5766a25144 feat: adds host module
continuous-integration/drone/push Build is passing
2026-08-12 22:43:16 +02:00
devydave 1ccbb31b70 refactor: simplify config and remove hom-manager dependency
continuous-integration/drone/push Build is passing
2026-08-07 15:15:04 +02:00
devydave 5e0dac6508 refactor: adjusting hosts generation
continuous-integration/drone/push Build is passing
2026-08-07 14:11:00 +02:00
devydave 38d13eae73 refactor: better variable name 2026-08-07 13:42:22 +02:00
devydave b5e22790be refactor: better name for inspect function 2026-08-07 13:36:25 +02:00
devydave beee7bd3f2 refactor: only export ssh auth socket when not existing 2026-08-07 11:09:30 +02:00
devydave 0c0e16914a refactor: remove traefik deployment because it is not needed 2026-08-07 10:36:23 +02:00
devydave 9bd8f4962a feat: adds running integrations test suite
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2026-08-06 22:00:29 +02:00
devydave 4e42d4fb9e feat: adds local vm setup that can reach each other
continuous-integration/drone/push Build is passing
2026-07-30 16:53:34 +02:00
130 changed files with 5677 additions and 1537 deletions
+1
View File
@@ -7,3 +7,4 @@
/bin
dist/
tests/integration/.bats
tests/resources/path_to_repo
+9
View File
@@ -84,3 +84,12 @@ build-mo:
release:
@goreleaser release --clean
# TEST_INTEGRATION_FILES overrides the default of running all test files
ifdef TEST_INTEGRATION_FILES
test_files := $(TEST_INTEGRATION_FILES);
else
test_files := *;
endif
test-integration: build
@bats -Tp --verbose-run tests/integration/$(test_files)
Generated
+38
View File
@@ -18,6 +18,27 @@
"type": "github"
}
},
"microvm": {
"inputs": {
"nixpkgs": [
"nixpkgs"
],
"spectrum": "spectrum"
},
"locked": {
"lastModified": 1784666190,
"narHash": "sha256-xgfS6slV7J3baMooNN1UuBi51RIgg9y0DbCxfSA0668=",
"owner": "astro",
"repo": "microvm.nix",
"rev": "fa5340ac684cdce8a22b6d4a0bcebb0cc999275e",
"type": "github"
},
"original": {
"owner": "astro",
"repo": "microvm.nix",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1778443072,
@@ -37,9 +58,26 @@
"root": {
"inputs": {
"flake-utils": "flake-utils",
"microvm": "microvm",
"nixpkgs": "nixpkgs"
}
},
"spectrum": {
"flake": false,
"locked": {
"lastModified": 1783694892,
"narHash": "sha256-xO8f7Qng+18FK2UlB9vcrkxCaQMCt5WjCH24aW/11eg=",
"ref": "refs/heads/main",
"rev": "24c4346e30fdea8d8e80f34aec3554a15a667d24",
"revCount": 1410,
"type": "git",
"url": "https://spectrum-os.org/git/spectrum"
},
"original": {
"type": "git",
"url": "https://spectrum-os.org/git/spectrum"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
+87 -18
View File
@@ -4,34 +4,103 @@
inputs = {
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
microvm = {
url = "github:astro/microvm.nix";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs =
{
self,
nixpkgs,
microvm,
flake-utils,
}:
flake-utils.lib.eachDefaultSystem (
system:
let
pkgs = nixpkgs.legacyPackages.${system};
in
{
packages = rec {
abra = pkgs.callPackage ./package.nix { };
default = abra;
let
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
publicKey = builtins.readFile ./tests/resources/local_integration_ssh.pub;
privateKey = builtins.readFile ./tests/resources/local_integration_ssh;
pathToRepo = builtins.readFile ./tests/resources/path_to_repo;
# local DNS aliases for the server host
extraHosts = builtins.readFile ./tests/resources/extra_hosts;
username = "abra";
password = "abra";
defaultDNS = [
# Quad9.net
"9.9.9.9"
"149.112.112.112"
"2620:fe::fe"
"2620:fe::9"
];
in
{
nixosConfigurations = {
abra-client = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
microvm.nixosModules.microvm
./nix/hosts/client/configuration.nix
];
specialArgs = {
inherit
publicKey
privateKey
username
password
defaultDNS
extraHosts
pathToRepo
;
};
};
apps = rec {
abra = flake-utils.lib.mkApp { drv = self.packages.${system}.abra; };
default = abra;
abra-server = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
microvm.nixosModules.microvm
./nix/hosts/server/configuration.nix
];
specialArgs = {
inherit
publicKey
privateKey
username
password
defaultDNS
;
};
};
devShells.default = pkgs.mkShell {
packages = with pkgs; [
go_1_26
gnumake
};
nixosModules = rec {
host = {
imports = [
microvm.nixosModules.host
./nix/modules/host.nix
];
};
}
);
default = host;
};
packages = rec {
abra = pkgs.callPackage ./nix/package.nix { };
default = abra;
};
apps = rec {
abra = flake-utils.lib.mkApp { drv = self.packages.${system}.abra; };
default = abra;
};
devShells.${system}.default = pkgs.mkShell {
# testing env variables
BATS_LIB_PATH = "~/.local/share/bats/";
TEST_SERVER = "abra.local";
ABRA_DIR = "$HOME/.abra_test";
packages = with pkgs; [
go_1_26
gnumake
gopls
];
};
};
}
+21 -24
View File
@@ -1,6 +1,6 @@
module coopcloud.tech/abra
go 1.26.0
go 1.27
require (
coopcloud.tech/tagcmp v0.0.0-20260515102403-c26951b55977
@@ -9,18 +9,24 @@ require (
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/lipgloss v1.1.0
github.com/charmbracelet/log v1.0.0
github.com/containers/image v3.0.2+incompatible
github.com/decentral1se/passgen v1.0.1
github.com/distribution/reference v0.6.0
github.com/docker/cli v28.4.0+incompatible
github.com/docker/docker v28.5.2+incompatible
github.com/docker/go-units v0.5.0
github.com/go-git/go-git/v5 v5.19.3
github.com/google/go-cmp v0.7.0
github.com/leonelquinteros/gotext v1.7.2
github.com/hashicorp/go-retryablehttp v0.7.8
github.com/leonelquinteros/gotext v1.8.0
github.com/moby/sys/signal v0.7.1
github.com/moby/term v0.5.2
github.com/pkg/errors v0.9.1
github.com/schollz/progressbar/v3 v3.19.1
golang.org/x/term v0.46.0
github.com/spf13/cobra v1.10.1
github.com/stretchr/testify v1.12.1
golang.org/x/sys v0.49.0
golang.org/x/term v0.47.0
gopkg.in/yaml.v3 v3.0.1
gotest.tools/v3 v3.5.2
)
@@ -48,15 +54,18 @@ require (
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/platforms v0.2.1 // indirect
github.com/containers/storage v1.38.2 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
github.com/docker/distribution v2.8.3+incompatible // indirect
github.com/docker/docker-credential-helpers v0.9.5 // indirect
github.com/docker/go-connections v0.6.0 // indirect
github.com/docker/go-metrics v0.0.1 // indirect
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fvbommel/sortorder v1.1.0 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.9.2 // indirect
@@ -65,7 +74,9 @@ require (
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
@@ -83,7 +94,9 @@ require (
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/go-archive v0.1.0 // indirect
github.com/moby/patternmatcher v0.6.1 // indirect
github.com/moby/sys/atomicwriter v0.1.0 // indirect
github.com/moby/sys/sequential v0.6.0 // indirect
github.com/moby/sys/user v0.4.0 // indirect
github.com/moby/sys/userns v0.1.0 // indirect
github.com/morikuni/aec v1.1.0 // indirect
@@ -92,18 +105,23 @@ require (
github.com/muesli/termenv v0.16.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/opencontainers/runc v1.1.13 // indirect
github.com/opencontainers/runtime-spec v1.1.0 // indirect
github.com/pjbgf/sha1cd v0.7.0 // indirect
github.com/prometheus/client_golang v1.23.2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.20.1 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sergi/go-diff v1.4.0 // indirect
github.com/sirupsen/logrus v1.9.4 // indirect
github.com/skeema/knownhosts v1.3.2 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/theupdateframework/notary v0.7.0 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
@@ -134,27 +152,6 @@ require (
gopkg.in/yaml.v2 v2.4.0 // indirect
)
require (
github.com/containers/image v3.0.2+incompatible
github.com/containers/storage v1.38.2 // indirect
github.com/decentral1se/passgen v1.0.1
github.com/docker/docker-credential-helpers v0.9.5 // indirect
github.com/fvbommel/sortorder v1.1.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/hashicorp/go-retryablehttp v0.7.8
github.com/moby/patternmatcher v0.6.1 // indirect
github.com/moby/sys/sequential v0.6.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/prometheus/client_golang v1.23.2 // indirect
github.com/sergi/go-diff v1.4.0 // indirect
github.com/spf13/cobra v1.10.1
github.com/stretchr/testify v1.12.1
github.com/theupdateframework/notary v0.7.0 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
golang.org/x/sys v0.48.0
)
replace github.com/docker/cli v28.4.0+incompatible => git.coopcloud.tech/toolshed/docker-cli v28.5.3-0.20260202112816-30df2d0b3a00+incompatible
replace github.com/spf13/cobra => github.com/decentral1se/cobra v1.10.2
+6 -6
View File
@@ -602,8 +602,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/leonelquinteros/gotext v1.7.2 h1:bDPndU8nt+/kRo1m4l/1OXiiy2v7Z7dfPQ9+YP7G1Mc=
github.com/leonelquinteros/gotext v1.7.2/go.mod h1:9/haCkm5P7Jay1sxKDGJ5WIg4zkz8oZKw4ekNpALob8=
github.com/leonelquinteros/gotext v1.8.0 h1:wwpxINWmWq3JN3WFI9BoSupBFrvAqxZTtf3qppijYkU=
github.com/leonelquinteros/gotext v1.8.0/go.mod h1:Wqke380+8unZq2F4oTmmqmbUY9zWQMzwImdC34XOAo8=
github.com/lib/pq v0.0.0-20150723085316-0dad96c0b94f/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/linuxkit/virtsock v0.0.0-20201010232012-f8cee7dfc7a3/go.mod h1:3r6x7q95whyfWQpmGZTu3gk3v2YkMi05HEzl7Tf7YEo=
github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
@@ -1136,13 +1136,13 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA=
golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE=
golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc=
golang.org/x/term v0.47.0 h1:cpyOwQkr2Nzxe2p6KnrTG7Ei2wtzjsuSjhpXXepZ/BM=
golang.org/x/term v0.47.0/go.mod h1:efAfAuG6RKABvg1ArGebRkvX6LXul14SM67o2e4mUCY=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+43
View File
@@ -0,0 +1,43 @@
GIT_ROOT := $$(git rev-parse --show-toplevel)
test-integration:
@$(GIT_ROOT)/nix/test_integration.sh
test-integration-hosts:
$(GIT_ROOT)/scripts/tests/extra_hosts
CLIENT_VM := abra-client
vm-client-status:
sudo systemctl status microvm@$(CLIENT_VM).service
vm-client-create:test-integration-hosts
cd $(GIT_ROOT) && sudo microvm -f git+file://$(GIT_ROOT) -c $(CLIENT_VM)
vm-client-start:
sudo systemctl start microvm@$(CLIENT_VM).service
vm-client-update:test-integration-hosts
cd $(GIT_ROOT) && sudo microvm -R -f git+file://$(GIT_ROOT) -u $(CLIENT_VM)
vm-client-run:
sudo microvm -f git+file://$(GIT_ROOT) -r $(CLIENT_VM)
vm-client-stop:
sudo systemctl stop microvm@$(CLIENT_VM)
vm-client-delete: vm-client-stop
sudo rm -rf /var/lib/microvms/$(CLIENT_VM)
vm-client-connect:
ssh abra@10.0.0.2 -i $(GIT_ROOT)/tests/resources/local_integration_ssh
SERVER_VM := abra-server
vm-server-status:
sudo systemctl status microvm@$(SERVER_VM).service
vm-server-create:
cd $(GIT_ROOT) && sudo microvm -f git+file://$(GIT_ROOT) -c $(SERVER_VM)
vm-server-start:
sudo systemctl start microvm@$(SERVER_VM).service
vm-server-update:
cd $(GIT_ROOT) && sudo microvm -R -f git+file://$(GIT_ROOT) -u $(SERVER_VM)
vm-server-run:
cd $(GIT_ROOT) && sudo microvm -f git+file://$(GIT_ROOT) -r $(SERVER_VM)
vm-server-stop:
sudo systemctl stop microvm@$(SERVER_VM)
vm-server-delete: vm-server-stop
sudo rm -rf /var/lib/microvms/$(SERVER_VM)
vm-server-connect:
ssh abra@10.0.0.3 -i $(GIT_ROOT)/tests/resources/local_integration_ssh
+48
View File
@@ -0,0 +1,48 @@
# Hosts
These hosts are VMs used for running the integration tests on your local machine.
The hosts contain a client where abra with the tests will be run and a server
to simulate a remote machine.
## Prepare the host
For this setup to work you need a machine with NixOS and flakes enabled.
1. Import the abra flake
The install example is based on the [using nix flakes wiki page](https://nixos.wiki/wiki/flakes#Using_nix_flakes_with_NixOS).
```nix
inputs = {
abra = {
url = "git+https://git.coopcloud.tech/toolshed/abra.git";
};
};
```
2. Add the host module to your configuration
Now add the host module to your configuration. At the toplevel of a flake it could look like this:
```nix
desktop = inputs.nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
abra.nixosModules.host
./configuration.nix
];
specialArgs = inputs;
};
```
3. Adjust the required config
```nix
abra.testing = {
enable = true; # loads the required config, when set true
externalInterface = "eth1"; # adjust this to your network interface that has access to the internet
};
```
You can look up the interface with `ip link`. After that rebuild your system and you are ready to go.
## Get started
After preparing the host run:
```
make test-integration
```
This runs the automatic setup of the VMs and network. You will be prompted if you want to run all
or specific files.
+145
View File
@@ -0,0 +1,145 @@
{
lib,
pkgs,
publicKey,
username,
pathToRepo,
defaultDNS,
extraHosts,
...
}:
let
index = 2;
mac = "00:00:00:00:00:01";
serverIp = "10.0.0.3";
in
{
imports = [
../../modules/ssh.nix
../../modules/user.nix
../../modules/docker.nix
];
microvm = {
vcpu = 4;
mem = 2049; # see issue related in microvm repo with 2048, so add 1MB
interfaces = [
{
id = "vm${toString index}";
type = "tap";
inherit mac;
}
];
shares = [
{
proto = "virtiofs";
tag = "repo";
# Source path can be absolute or relative
# to /var/lib/microvms/$hostName
source = "${lib.trim pathToRepo}";
mountPoint = "/home/${username}/abra";
}
];
};
boot.tmp = {
useTmpfs = true;
tmpfsSize = "2G";
};
networking = {
hostName = "abra-client";
useNetworkd = true;
};
systemd.network.networks."10-eth" = {
matchConfig.MACAddress = mac;
# Static IP configuration
address = [
"10.0.0.${toString index}/32"
"fec0::${lib.toHexString index}/128"
];
routes = [
{
# A route to the host
Destination = "10.0.0.0/32";
GatewayOnLink = true;
}
{
# Route to server
Destination = "${serverIp}/32";
Gateway = "10.0.0.0";
GatewayOnLink = true;
}
{
# Default route
Destination = "0.0.0.0/0";
Gateway = "10.0.0.0";
GatewayOnLink = true;
}
{
# Default route
Destination = "::/0";
Gateway = "fec0::";
GatewayOnLink = true;
}
];
networkConfig = {
# DNS servers no longer come from DHCP nor Router
# Advertisements. Perhaps you want to change the defaults:
DNS = defaultDNS;
};
};
# open the ports to allow ssh access from the host
networking.firewall.allowedTCPPorts = [ 22 ];
networking.firewall.allowedUDPPorts = [ 22 ];
networking.extraHosts = extraHosts;
environment.variables = {
CGO_ENABLED = 0;
TEST_SERVER = "abra.local";
ABRA_DIR = "$HOME/.abra_test";
};
programs.ssh = {
startAgent = true;
knownHostsFiles = [
(pkgs.writeText "local.keys" ''
abra.local ${publicKey}
'')
];
extraConfig = ''
Host abra.local
HostName abra.local
Port 22
User ${username}
'';
};
programs.git = {
enable = true;
# many recipe commands need a preset git user
config = [
{
user = {
name = "abra dev";
email = "helo@coopcloud.tech";
};
}
];
};
environment.systemPackages = with pkgs; [
# build dependencies, copied from flake.nix
go_1_26
gnumake
# testing dependencies
(bats.withLibraries (p: [
p.bats-assert
p.bats-file
p.bats-support
]))
jq
wget
];
}
+42
View File
@@ -0,0 +1,42 @@
{
serverIp,
username,
...
}:
{
# Home Manager needs a bit of information about you and the
# paths it should manage.
home.username = username;
home.homeDirectory = "/home/${username}";
# This value determines the Home Manager release that your
# configuration is compatible with. This helps avoid breakage
# when a new Home Manager release introduces backwards
# incompatible changes.
#
# You can update Home Manager without changing this value. See
# the Home Manager release notes for a list of state version
# changes in each release.
home.stateVersion = "26.05";
# Let Home Manager install and manage itself.
programs.home-manager.enable = true;
programs.ssh = {
enable = true;
enableDefaultConfig = false;
settings = {
"abra.local" = {
HostName = serverIp;
User = username;
Port = 22;
IdentityFile = "/home/${username}/abra/tests/resources/local_integration_ssh";
};
"*.abra.local" = {
HostName = serverIp;
User = username;
Port = 22;
IdentityFile = "/home/${username}/abra/tests/resources/local_integration_sshn";
};
};
};
}
+91
View File
@@ -0,0 +1,91 @@
{
lib,
defaultDNS,
...
}:
let
index = 3;
mac = "00:00:00:00:00:02";
clientIp = "10.0.0.2";
in
{
imports = [
../../modules/ssh.nix
../../modules/user.nix
../../modules/docker.nix
];
microvm = {
vcpu = 4;
mem = 8097; # see issue related in microvm repo with 2048, so add 1MB
interfaces = [
{
id = "vm${toString index}";
type = "tap";
inherit mac;
}
];
};
boot.tmp = {
useTmpfs = true;
tmpfsSize = "8G";
};
networking = {
hostName = "abra-server";
useNetworkd = true;
};
systemd.network.networks."11-eth" = {
matchConfig.MACAddress = mac;
# Static IP configuration
address = [
"10.0.0.${toString index}/32"
"fec0::${lib.toHexString index}/128"
];
routes = [
{
# A route to the host
Destination = "10.0.0.0/32";
GatewayOnLink = true;
}
{
# Route to server
Destination = "${clientIp}/32";
Gateway = "10.0.0.0";
GatewayOnLink = true;
}
{
# Default route
Destination = "0.0.0.0/0";
Gateway = "10.0.0.0";
GatewayOnLink = true;
}
{
# Default route
Destination = "::/0";
Gateway = "fec0::";
GatewayOnLink = true;
}
];
networkConfig = {
# DNS servers no longer come from DHCP nor Router
# Advertisements. Perhaps you want to change the defaults:
DNS = defaultDNS;
};
};
# open the ports to allow ssh access from the host
networking.firewall.allowedTCPPorts = [
22
80
443
1312 # deploy with udp and tcp on same port test
];
networking.firewall.allowedUDPPorts = [
22
80
443
1312 # deploy with udp and tcp on same port test
];
}
+7
View File
@@ -0,0 +1,7 @@
{ ... }:
{
virtualisation.docker = {
enable = true;
liveRestore = false;
};
}
+59
View File
@@ -0,0 +1,59 @@
{
lib,
config,
...
}:
let
maxVMs = 3;
in
{
options = {
abra.testing.enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Enable the abra integration testing framework";
};
abra.testing.externalInterface = lib.mkOption {
type = lib.types.str;
default = null;
description = "Change this to the interface with upstream Internet access";
};
};
config = lib.mkIf config.abra.testing.enable {
networking.useNetworkd = true;
systemd.network.wait-online.enable = false;
systemd.network.networks = builtins.listToAttrs (
map (index: {
name = "30-vm${toString index}";
value = {
matchConfig.Name = "vm${toString index}";
# Host's addresses
address = [
"10.0.0.0/32"
"fec0::/128"
];
# Setup routes to the VM
routes = [
{
Destination = "10.0.0.${toString index}/32";
}
{
Destination = "fec0::${lib.toHexString index}/128";
}
];
# Enable routing
networkConfig = {
IPv4Forwarding = true;
IPv6Forwarding = true;
};
};
}) (lib.genList (i: i + 1) maxVMs)
);
networking.nat = {
enable = true;
internalIPs = [ "10.0.0.0/24" ];
externalInterface = config.abra.testing.externalInterface;
};
};
}
+33
View File
@@ -0,0 +1,33 @@
{
privateKey,
publicKey,
username,
...
}:
{
# default host key location
environment.etc."ssh/ssh_host_ed25519_key" = {
text = privateKey;
mode = "0600";
};
environment.etc."ssh/ssh_host_ed25519_key.pub" = {
text = publicKey;
mode = "0644";
};
services.openssh = {
enable = true;
settings = {
PasswordAuthentication = false;
PermitRootLogin = "no";
AllowUsers = [ "${username}" ];
};
generateHostKeys = false;
hostKeys = [
{
path = "/etc/ssh/ssh_host_ed25519_key";
type = "ed25519";
}
];
};
}
+22
View File
@@ -0,0 +1,22 @@
{
username,
password,
publicKey,
...
}:
{
users.users."${username}" = {
isNormalUser = true;
password = password;
home = "/home/${username}";
description = "Abra user";
extraGroups = [
"wheel"
"networkmanager"
"docker"
];
openssh.authorizedKeys.keys = [
publicKey
];
};
}
View File
+39
View File
@@ -0,0 +1,39 @@
#!/usr/bin/env bash
GIT_ROOT=$(git rev-parse --show-toplevel)
# write path to repo
echo "$GIT_ROOT" > "$GIT_ROOT/tests/resources/path_to_repo"
# create or update a VM, start it and wait for successful ping
start_vm () {
if [ -d "/var/lib/microvms/abra-$1" ]; then
make "vm-$1-update"
else
make "vm-$1-create"
fi
make "vm-$1-start"
host_available=0
while [ $host_available == 0 ]; do
if ping -c 1 "$2" &> /dev/null
then
host_available=1
else
echo "could not ping $1, waiting for 5 seconds..."
sleep 5
fi
done
}
start_vm "client" "10.0.0.2"
start_vm "server" "10.0.0.3"
# revert dynamic content of file to prevent commiting changes
git restore -- "$GIT_ROOT/tests/resources/path_to_repo"
read -rp "Press enter to run all tests or specify test files: " TEST_INTEGRATION_FILES
# -t option is needed for stdin output that is checked in integration tests
ssh -t abra@10.0.0.2 -i "$GIT_ROOT/tests/resources/local_integration_ssh" "cd abra; chmod 600 ./tests/resources/local_integration_ssh; ssh-add ./tests/resources/local_integration_ssh; make test-integration TEST_INTEGRATION_FILES=$TEST_INTEGRATION_FILES"
+2 -2
View File
@@ -7,7 +7,7 @@
msgid ""
msgstr "Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-08-31 19:21+0000\n"
"POT-Creation-Date: 2026-10-14 10:32-0400\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -4475,7 +4475,7 @@ msgstr ""
msgid "retrieving docker auth token: failed create docker cli: %s"
msgstr ""
#: ./pkg/recipe/recipe.go:659
#: ./pkg/recipe/recipe.go:656
msgid "retrieving recipes"
msgstr ""
Binary file not shown.
+2 -2
View File
@@ -2,7 +2,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-08-31 19:21+0000\n"
"POT-Creation-Date: 2026-10-14 10:32-0400\n"
"PO-Revision-Date: 2026-09-01 22:35+0000\n"
"Last-Translator: chasqui <chasqui@cryptolab.net>\n"
"Language-Team: Spanish <https://translate.coopcloud.tech/projects/co-op-cloud/abra/es/>\n"
@@ -5125,7 +5125,7 @@ msgstr "versiones recuperadas del repositorio local de recetas"
msgid "retrieving docker auth token: failed create docker cli: %s"
msgstr "recuperando token de autenticación de Docker: error al crear docker cli: %s"
#: pkg/recipe/recipe.go:659
#: pkg/recipe/recipe.go:656
msgid "retrieving recipes"
msgstr "obteniendo 📜 recetas"
+15 -18
View File
@@ -592,15 +592,7 @@ func ReadReposMetadata(debug bool) (RepoCatalogue, error) {
// sortRecipeVersions sorts the recipe semver versions
func sortRecipeVersions(versions RecipeVersions) {
sort.Slice(versions, func(i, j int) bool {
version1, err := tagcmp.Parse(getVersionString(versions[i]))
if err != nil {
panic(err)
}
version2, err := tagcmp.Parse(getVersionString(versions[j]))
if err != nil {
panic(err)
}
return version1.IsLessThan(version2)
return safeCompareVersions(getVersionString(versions[i]), getVersionString(versions[j]))
})
}
@@ -616,18 +608,23 @@ func getVersionString(versionMap map[string]map[string]ServiceMeta) string {
// sortVersionStrings sorts a list of semver version strings
func sortVersionStrings(versions []string) {
sort.Slice(versions, func(i, j int) bool {
version1, err := tagcmp.Parse(versions[i])
if err != nil {
panic(err)
}
version2, err := tagcmp.Parse(versions[j])
if err != nil {
panic(err)
}
return version1.IsLessThan(version2)
return safeCompareVersions(versions[i], versions[j])
})
}
// Invalid versions are sorted as less than valid ones.
func safeCompareVersions(v1, v2 string) bool {
version1, err1 := tagcmp.Parse(v1)
if err1 != nil {
return true
}
version2, err2 := tagcmp.Parse(v2)
if err2 != nil {
return false
}
return version1.IsLessThan(version2)
}
// GetRecipeCatalogueVersions list the recipe versions listed in the recipe catalogue.
func GetRecipeCatalogueVersions(recipeName string, catl RecipeCatalogue) ([]string, error) {
var versions []string
+38
View File
@@ -123,3 +123,41 @@ func TestGetVersionLabelLocalDoesNotUseTimeoutLabel(t *testing.T) {
assert.NotEqual(t, label, timeout)
}
}
// This is not a full test of comparison, as that occurs in the external
// tagcmp package. This is a sunny-day integration test
func TestSortRecipeVersionsSortsTwoValidStrings(t *testing.T) {
versions := RecipeVersions{
{"2.0.0": {}}, // Index 0
{"1.0.0": {}}, // Index 1
}
sortRecipeVersions(versions)
assert.Equal(t, "1.0.0", getVersionString(versions[0]), "Version 1.0.0 should be first")
assert.Equal(t, "2.0.0", getVersionString(versions[1]), "Version 2.0.0 should be second")
}
func TestSortRecipeVersionsSortsInvalidVersionStringToTheBottom(t *testing.T) {
versions := RecipeVersions{
{"2.0.0": {}}, // Index 0
{"invalid": {}}, // Index 1
}
sortRecipeVersions(versions)
assert.Equal(t, "invalid", getVersionString(versions[0]), "The invalid version should be first")
assert.Equal(t, "2.0.0", getVersionString(versions[1]), "Version 2.0.0 should be second")
}
func TestSortRecipeVersionsSortsInvalidVersionStringToTheBottomInReverse(t *testing.T) {
versions := RecipeVersions{
{"invalid": {}}, // Index 0
{"2.0.0": {}}, // Index 1
}
sortRecipeVersions(versions)
assert.Equal(t, "invalid", getVersionString(versions[0]), "The invalid version should be first")
assert.Equal(t, "2.0.0", getVersionString(versions[1]), "Version 2.0.0 should be second")
}
+1 -1
View File
@@ -3,7 +3,7 @@ version: "3.8"
services:
app:
image: nginx:1.31.6
image: nginx:1.31.8
secrets:
- test_pass_one
- test_pass_two
+31
View File
@@ -0,0 +1,31 @@
#!/usr/bin/env bash
# This file generates local DNS aliases for the client VM for integration testing
GIT_ROOT=$(git rev-parse --show-toplevel)
echo "generating hosts file..."
hosts_file_path="$GIT_ROOT/tests/resources/extra_hosts"
echo "removing old file..."
rm -f "$hosts_file_path"
# the server domain
server_ip="10.0.0.3"
domain="abra.local"
echo "$server_ip $domain" >> "$hosts_file_path"
# static subdomains used in integration tests
subdomains=("gitea" "zammad" "custom-html" "foo" "foobar")
for subdomain in "${subdomains[@]}"
do
echo "$server_ip $subdomain.$domain" >> "$hosts_file_path"
done
search_dir="$GIT_ROOT/tests/integration"
for entry in "$search_dir"/*.bats
do
file_name=$(basename "${entry}")
# converting file names to testing subdomains
subdomain=$(echo "$file_name" | tr . _)
echo "$server_ip $subdomain.$domain" >> "$hosts_file_path"
done
echo "writing extra hosts success"
+2 -1
View File
@@ -7,4 +7,5 @@
* Integration tests are in `./tests/integration`. Please see [these
docs](https://docs.coopcloud.tech/abra/hack/#integration-tests) for
instructions and tips on how to run them.
instructions and tips on how to run them. If you want to run it locally via nix,
read the [nix testing docs](../nix/hosts/README.md).
+9 -5
View File
@@ -598,8 +598,7 @@ teardown(){
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input
assert_success
run docker inspect --format='{{range .Config.Env}}{{println .}}{{end}}' \
$(docker ps -f name="$TEST_APP_DOMAIN_$TEST_SERVER" -q)
_inspect_env_test_app
assert_success
assert_output --partial "WITH_COMMENT=foo"
@@ -610,8 +609,7 @@ teardown(){
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --force
assert_success
run docker inspect --format='{{range .Config.Env}}{{println .}}{{end}}' \
$(docker ps -f name="$TEST_APP_DOMAIN_$TEST_SERVER" -q)
_inspect_env_test_app
assert_success
refute_output --partial "WITH_COMMENT=foo"
assert_output --partial "WITH_COMMENT=bar"
@@ -629,8 +627,14 @@ teardown(){
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input
assert_success
run docker service inspect --format '{{ range .Endpoint.Ports }}{{ .Protocol }}={{ .PublishedPort }}{{ end }}' \
if _is_local;
then
run docker service inspect --format "{{ range .Endpoint.Ports }}{{ .Protocol }}={{ .PublishedPort }}{{ end }}" \
"${TEST_APP_DOMAIN//./_}_app"
else
run ssh "$TEST_SERVER" "docker service inspect --format '{{ range .Endpoint.Ports }}{{ .Protocol }}={{ .PublishedPort }}{{ end }}' ${TEST_APP_DOMAIN//./_}_app"
fi
assert_success
assert_output --partial "tcp=1312"
assert_output --partial "udp=1312"
@@ -133,8 +133,7 @@ teardown(){
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.0+1.20.0" --no-input
assert_success
run docker inspect --format='{{range .Config.Env}}{{println .}}{{end}}' \
$(docker ps -f name="$TEST_APP_DOMAIN_$TEST_SERVER" -q)
_inspect_env_test_app
assert_success
assert_output --partial "$TEST_RECIIPE:0.1.0+1.20.0"
}
+11 -4
View File
@@ -140,15 +140,22 @@ teardown(){
_undeploy_app
sanitisedDomainName="${TEST_APP_DOMAIN//./_}"
run docker config create "${sanitisedDomainName}_test_conf_v99" "$ABRA_DIR/recipes/abra-test-recipe/abra.sh"
assert_success
remote_ssh_command=""
if _is_local;
then
run docker config create "${sanitisedDomainName}_test_conf_v99" "$ABRA_DIR/recipes/abra-test-recipe/abra.sh"
else
remote_ssh_command="ssh '$TEST_SERVER'"
run cat "$ABRA_DIR/recipes/abra-test-recipe/abra.sh" | ssh "$TEST_SERVER" docker config create "${sanitisedDomainName}_test_conf_v99 -"
assert_success
fi
assert bash -c "docker config ls | grep -q test_conf_v99"
assert bash -c "$remote_ssh_command docker config ls | grep -q test_conf_v99"
run $ABRA app rm "$TEST_APP_DOMAIN" --no-input
assert_success
refute bash -c "docker config ls | grep -q test_conf_v99"
refute bash -c "$remote_ssh_command docker config ls | grep -q test_conf_v99"
}
@test "remove .env file" {
@@ -54,8 +54,7 @@ teardown(){
--no-input
assert_success
run docker inspect --format='{{range .Config.Env}}{{println .}}{{end}}' \
$(docker ps -f name="$TEST_APP_DOMAIN_$TEST_SERVER" -q)
_inspect_env_test_app
assert_success
assert_output --partial "$TEST_RECIIPE:0.1.0+1.20.0"
}
@@ -53,8 +53,7 @@ teardown(){
run $ABRA app upgrade "$TEST_APP_DOMAIN" "0.2.0+1.21.0" --no-input
assert_success
run docker inspect --format='{{range .Config.Env}}{{println .}}{{end}}' \
$(docker ps -f name="$TEST_APP_DOMAIN_$TEST_SERVER" -q)
_inspect_env_test_app
assert_success
assert_output --partial "$TEST_RECIIPE:0.2.0+1.21.0"
assert_output --partial "$TEST_RECIPE:0.2.0+1.21.0"
}
+11 -2
View File
@@ -31,9 +31,18 @@ _ensure_ssh_agent() {
exit 1
fi
export SSH_AUTH_SOCK="$HOME/.ssh/ssh_auth_sock"
if [ ! -S ~/.ssh/ssh_auth_sock ]; then
if [[ ! -v SSH_AUTH_SOCK ]]; then
export SSH_AUTH_SOCK="$HOME/.ssh/ssh_auth_sock"
eval `ssh-agent`
ln -sf "$SSH_AUTH_SOCK" ~/.ssh/ssh_auth_sock
fi
}
_is_local() {
if [[ "$TEST_SERVER" == "default" ]];
then
return 0
else
return 1
fi
}
+30 -6
View File
@@ -1,13 +1,37 @@
#!/usr/bin/env bash
_ensure_swarm() {
if [ "$(docker info | grep Swarm | sed 's/Swarm: //g' | tr -d ' ')" == "inactive" ]; then
run docker swarm init --advertise-addr 127.0.0.1:2377
assert_success
if [ "$(docker info | grep Swarm | sed 's/Swarm: //g' | tr -d ' ')" == "inactive" ]; then
run docker swarm init --advertise-addr 127.0.0.1:2377
assert_success
fi
if ! $(docker network ls | grep -q 'proxy'); then
run docker network create -d overlay proxy
assert_success
if ! docker network ls | grep -q 'proxy'; then
run docker network create -d overlay proxy
assert_success
fi
if ! _is_local;
then
if [ "$(ssh "$TEST_SERVER" docker info | grep Swarm | sed 's/Swarm: //g' | tr -d ' ')" == "inactive" ]; then
run ssh "$TEST_SERVER" docker swarm init --advertise-addr 127.0.0.1:2377
assert_success
fi
if ! ssh "$TEST_SERVER" docker network ls | grep -q 'proxy'; then
run ssh "$TEST_SERVER" docker network create -d overlay proxy
assert_success
fi
fi
}
_inspect_env_test_app() {
if _is_local;
then
containerId="$(docker ps -f name="$TEST_APP_DOMAIN_$TEST_SERVER" -q)"
run docker inspect --format='{{range .Config.Env}}{{println .}}{{end}}' "$containerId"
else
containerId="$(ssh "$TEST_SERVER" docker ps -f name="$TEST_APP_DOMAIN" -q)"
run ssh "$TEST_SERVER" "docker inspect --format='{{range .Config.Env}}{{println .}}{{end}}' '$containerId'"
fi
}
-2
View File
@@ -38,8 +38,6 @@ teardown(){
run $ABRA recipe upgrade "custom-html" --no-input
assert_success
assert_output --partial 'can upgrade service: app'
assert_exists "$ABRA_DIR/recipes/custom-html"
}
+3 -3
View File
@@ -27,7 +27,7 @@ teardown(){
assert bash -c "docker context ls | grep -q $TEST_SERVER"
server_dir_perms=$(stat -c "%a" "$ABRA_DIR/servers/$TEST_SERVER")
assert_equal $server_dir_perms "700"
assert_equal "$server_dir_perms" "700"
}
@test "error if using name and --local together" {
@@ -43,8 +43,8 @@ teardown(){
assert bash -c "docker context ls | grep -q default"
assert_output --partial 'local server successfully added'
server_dir_perms=$(stat -c "%a" "$ABRA_DIR/servers/$TEST_SERVER")
assert_equal $server_dir_perms "700"
server_dir_perms=$(stat -c "%a" "$ABRA_DIR/servers/default")
assert_equal "$server_dir_perms" "700"
}
@test "create local server fails when no docker swarm" {
+57
View File
@@ -0,0 +1,57 @@
10.0.0.3 abra.local
10.0.0.3 gitea.abra.local
10.0.0.3 zammad.abra.local
10.0.0.3 custom-html.abra.local
10.0.0.3 foo.abra.local
10.0.0.3 foobar.abra.local
10.0.0.3 app_check_bats.abra.local
10.0.0.3 app_cmd_bats.abra.local
10.0.0.3 app_config_bats.abra.local
10.0.0.3 app_cp_bats.abra.local
10.0.0.3 app_deploy_bats.abra.local
10.0.0.3 app_deploy_env_version_bats.abra.local
10.0.0.3 app_deploy_overview_bats.abra.local
10.0.0.3 app_deploy_remote_recipes_bats.abra.local
10.0.0.3 app_env_bats.abra.local
10.0.0.3 app_env_version_bats.abra.local
10.0.0.3 app_labels_bats.abra.local
10.0.0.3 app_list_bats.abra.local
10.0.0.3 app_logs_bats.abra.local
10.0.0.3 app_move_bats.abra.local
10.0.0.3 app_new_bats.abra.local
10.0.0.3 app_ps_bats.abra.local
10.0.0.3 app_remove_bats.abra.local
10.0.0.3 app_restart_bats.abra.local
10.0.0.3 app_rollback_bats.abra.local
10.0.0.3 app_rollback_env_version_bats.abra.local
10.0.0.3 app_rollback_overview_bats.abra.local
10.0.0.3 app_run_bats.abra.local
10.0.0.3 app_secret_bats.abra.local
10.0.0.3 app_secret_env_version_bats.abra.local
10.0.0.3 app_services_bats.abra.local
10.0.0.3 app_undeploy_bats.abra.local
10.0.0.3 app_undeploy_env_version_bats.abra.local
10.0.0.3 app_undeploy_overview_bats.abra.local
10.0.0.3 app_upgrade_bats.abra.local
10.0.0.3 app_upgrade_env_version_bats.abra.local
10.0.0.3 app_upgrade_overview_bats.abra.local
10.0.0.3 app_volume_bats.abra.local
10.0.0.3 autocomplete_bats.abra.local
10.0.0.3 catalogue_bats.abra.local
10.0.0.3 dirs_bats.abra.local
10.0.0.3 install_bats.abra.local
10.0.0.3 recipe_diff_bats.abra.local
10.0.0.3 recipe_fetch_bats.abra.local
10.0.0.3 recipe_lint_bats.abra.local
10.0.0.3 recipe_list_bats.abra.local
10.0.0.3 recipe_new_bats.abra.local
10.0.0.3 recipe_release_bats.abra.local
10.0.0.3 recipe_reset_bats.abra.local
10.0.0.3 recipe_upgrade_bats.abra.local
10.0.0.3 recipe_version_bats.abra.local
10.0.0.3 server_add_bats.abra.local
10.0.0.3 server_list_bats.abra.local
10.0.0.3 server_prune_bats.abra.local
10.0.0.3 server_remove_bats.abra.local
10.0.0.3 upgrade_bats.abra.local
10.0.0.3 version_bats.abra.local
+7
View File
@@ -0,0 +1,7 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACBQ967UhxmFkq71WXvVRkEmtehCGsEnBVppAXxgtHHm7wAAAJhFGwTnRRsE
5wAAAAtzc2gtZWQyNTUxOQAAACBQ967UhxmFkq71WXvVRkEmtehCGsEnBVppAXxgtHHm7w
AAAEAnHEZcf2NeRQEcJC/aVgUWsdOz+vQgEG9ZY+3ErCeaKFD3rtSHGYWSrvVZe9VGQSa1
6EIawScFWmkBfGC0cebvAAAAFWFicmEgaW50ZWdyYXRpb24gdGVzdA==
-----END OPENSSH PRIVATE KEY-----
@@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFD3rtSHGYWSrvVZe9VGQSa16EIawScFWmkBfGC0cebv abra integration test
+1
View File
@@ -0,0 +1 @@
/path/to/this/repo
+1 -1
View File
@@ -3,7 +3,7 @@ version: "3.8"
services:
app:
image: nginx:1.31.6
image: nginx:1.31.8
networks:
- proxy
deploy:
+64
View File
@@ -0,0 +1,64 @@
# AGENTS.md - gotext Project Guidelines
This document is the **source of truth** for the `gotext` project's architecture, conventions, and development workflows. All contributors — human or AI agent — must adhere to these guidelines.
Agent-specific configuration files (e.g., `GEMINI.md`, `CLAUDE.md`) should reference this document rather than duplicating its content.
## Project Overview
`gotext` is a GNU Gettext implementation for Go (Golang). It provides a thread-safe, hierarchical translation system that supports PO/MO files, plural forms, and context-aware translations.
## Core Architecture
The project follows a hierarchical structure for managing translations:
1. **Global Package API (`gotext.go`)**: Provides convenient, global functions (e.g., `Get`, `GetD`, `GetN`) that rely on a shared `globalConfig`.
2. **Locales (`locale.go`)**: Represent a specific language (e.g., "en_US", "es_ES"). A `Locale` container manages multiple `Domains`.
3. **Domains (`domain.go`)**: The core translation engine for a specific set of messages. It handles:
- Message retrieval (with support for plural forms and `msgctxt`).
- Header parsing (including plural-form rules).
- Thread-safety via `sync.RWMutex`.
4. **Translators (`translator.go`)**: An interface for translation sources.
- **PO (`po.go`)**: Parsers for human-readable Gettext Portable Object files.
- **MO (`mo.go`)**: Parsers for binary Gettext Machine Object files.
5. **Plural Forms (`plurals/`)**: A dedicated package for compiling and evaluating GNU Gettext plural form expressions.
## CLI Tools
- **xgotext (`cli/xgotext/`)**: A tool to scan Go source files, extract translatable strings (matching `Get`, `GetD`, etc.), and generate/update PO files.
## Key Files & Symbols
- `gotext.go`: `Configure`, `Get`, `GetD`, `GetN`.
- `locale.go`: `Locale`, `NewLocale`, `AddDomain`.
- `domain.go`: `Domain`, `Get`, `GetN`, `parseHeaders`.
- `translator.go`: `Translator` interface.
- `plurals/compiler.go`: `Compile` function for plural expressions.
## Development Workflows
### Branching & PRs
- **Feature Branches**: All development MUST happen in dedicated feature branches (e.g., `feature/description-of-change`, `fix/bug-description`, `refactor/topic`).
- **Target Branch**: All Pull Requests MUST be made against the `master` branch.
- **Review Requirement**: No direct commits to `master` are allowed. All changes must be reviewed via a PR.
- **GitHub Integration**: If the `gh` CLI tool is installed and authenticated, it should be used for managing Pull Requests, Issues, and Reviews. Otherwise, these tasks must be performed manually through the GitHub web interface.
### Coding Standards
- **Idiomatic Go**: Follow standard Go conventions (`gofmt`, `go vet`).
- **Thread Safety**: Ensure all shared state (like in `Domain`) is protected by appropriate synchronization primitives.
- **Testing**:
- Every new feature or bug fix must include corresponding tests.
- Use the `fixtures/` directory for sample PO/MO files.
- Run `go test ./...` to ensure no regressions.
### Pre-Submit Checklist
Before submitting a Pull Request, verify:
1. All existing tests pass: `go test ./...`
2. Code passes static analysis: `go vet ./...`
3. Code is properly formatted: `gofmt`
4. New features or bug fixes include corresponding tests.
5. Commit messages are clear and descriptive.
+2 -45
View File
@@ -1,46 +1,3 @@
# Contributor Covenant Code of Conduct
# CODE OF CONDUCT
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at leonel.quinteros@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/
The canonical text of this project's Code of Conduct lives at [docs/CODE_OF_CONDUCT.md](docs/CODE_OF_CONDUCT.md).
+4 -14
View File
@@ -1,19 +1,9 @@
# CONTRIBUTING
This open source project welcomes everybody that wants to contribute to it by implementing new features, fixing bugs, testing, creating documentation or simply talk about it.
Thank you for considering contributing to `gotext`! All kinds of contributions — features, bug fixes, tests, documentation, or discussions — are welcome.
Most contributions will start by creating a new Issue to discuss what is the contribution about and to agree on the steps to move forward.
The canonical contribution guidelines live at [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md).
## Issues
All issues reports are welcome. Open a new Issue whenever you want to report a bug, request a change or make a proposal.
This should be your start point of contribution.
## Pull Requests
If you have any changes that can be merged, feel free to send a Pull Request.
Usually, you'd want to create a new Issue to discuss about the change you want to merge and why it's needed or what it solves.
Before contributing, please also read [AGENTS.md](AGENTS.md): it is the source of truth for project architecture, conventions, and development workflows (branching, PR process, coding standards, and the pre-submit checklist).
Start point: open an [Issue](https://github.com/leonelquinteros/gotext/issues) to discuss your idea, then send a Pull Request against `master`.
+9
View File
@@ -0,0 +1,9 @@
# GEMINI.md - Gemini CLI Instructions for gotext
> **Important**: Read and follow [`AGENTS.md`](./AGENTS.md) first. It is the source of truth for project architecture, conventions, and development workflows. The instructions below are specific to Gemini CLI and supplement — not replace — those guidelines.
## Gemini-Specific Instructions
- Always work in a feature branch. Never commit directly to `master`.
- Before submitting, verify changes with existing and new tests (`go test ./...`).
- Propose a PR to `master` after confirmation, using the `gh` CLI when available.
+101 -208
View File
@@ -9,258 +9,151 @@
[GNU gettext utilities](https://www.gnu.org/software/gettext) for Go.
`gotext` is a native Go implementation of the GNU Gettext utilities. It provides a thread-safe, flexible, and powerful way to handle internationalization (i18n) and localization (l10n) in your Go applications.
# Features
---
- Implements GNU gettext support in native Go.
- Complete support for [PO files](https://www.gnu.org/software/gettext/manual/html_node/PO-Files.html) including:
- Support for multiline strings and headers.
- Support for variables inside translation strings using Go's [fmt syntax](https://golang.org/pkg/fmt/).
- Support for [pluralization rules](https://www.gnu.org/software/gettext/manual/html_node/Translating-plural-forms.html).
- Support for [message contexts](https://www.gnu.org/software/gettext/manual/html_node/Contexts.html).
- Support for MO files.
- Thread-safe: This package is safe for concurrent use across multiple goroutines.
- It works with UTF-8 encoding as it's the default for Go language.
- Unit tests available.
- Language codes are automatically simplified from the form `en_UK` to `en` if the first isn't available.
- Ready to use inside Go templates.
- Objects are serializable to []byte to store them in cache.
- Support for Go Modules.
**📖 [Read the Full Documentation](https://leonelquinteros.github.io/gotext/)**
---
# License
### Table of Contents
- [Features](#features)
- [Installation](#installation)
- [Getting Started](#getting-started)
- [CLI Tool (xgotext)](#cli-tool-xgotext)
- [Advanced Usage](#advanced-usage)
- [Locale Object](#using-locale-object)
- [Plural Forms](#use-plural-forms-of-translations)
- [Dynamic Variables](#using-dynamic-variables-on-translations)
- [Directory Structure](#locales-directories-structure)
- [Contributing](#contributing)
- [License](#license)
[MIT license](LICENSE)
---
## Features
- Native Go implementation of Gettext (the core library has no external dependencies).
- Full support for **PO and MO files**.
- **Pluralization rules** support via GNU Gettext plural-form expressions.
- **Message context** support (`msgctxt`).
- Thread-safe for concurrent use.
- Works with UTF-8 by default.
- Integrated CLI tool (`xgotext`) for string extraction.
- Serializable objects for caching.
- Plain-string results that plug directly into Go's `text/template` and `html/template` pipelines.
# Documentation
---
Refer to package documentation at (https://pkg.go.dev/github.com/leonelquinteros/gotext)
# Installation
```
## Installation
```bash
go get github.com/leonelquinteros/gotext
```
- There are no requirements or dependencies to use this package.
- No need to install GNU gettext utilities (unless specific needs of CLI tools).
- No need for environment variables. Some naming conventions are applied but not needed.
---
# Usage examples
## Using package for single language/domain settings
For quick/simple translations you can use the package level functions directly.
## Getting Started
For a quick start, use the package-level API:
```go
package main
import (
"fmt"
"github.com/leonelquinteros/gotext"
)
func main() {
// Configure package
gotext.Configure("/path/to/locales/root/dir", "en_UK", "domain-name")
// Configure package: locales path, language, and domain
gotext.Configure("/path/to/locales", "en_US", "default")
// Translate text from default domain
fmt.Println(gotext.Get("My text on 'domain-name' domain"))
// Simple translation
fmt.Println(gotext.Get("Hello, world!"))
// Translate text from a different domain without reconfigure
fmt.Println(gotext.GetD("domain2", "Another text on a different domain"))
// Translation with variables
fmt.Println(gotext.Get("Hello, %s!", "Gopher"))
}
```
## Using dynamic variables on translations
For more details, see the [Getting Started Guide](docs/GETTING_STARTED.md).
All translation strings support dynamic variables to be inserted without translate.
Use the fmt.Printf syntax (from Go's "fmt" package) to specify how to print the non-translated variable inside the translation string.
---
## CLI Tool (xgotext)
`gotext` includes a command-line tool to extract translatable strings from your Go source code.
**Install xgotext:**
```bash
go install github.com/leonelquinteros/gotext/cli/xgotext@latest
```
**Extract strings:**
```bash
xgotext -in . -out locales/templates
```
The command generates `.pot` templates under `locales/templates`; merge those templates into language-specific `.po` files.
See the [xgotext Documentation](docs/xgotext.md) for full usage details.
---
## Advanced Usage
### Using Locale object
For managing multiple languages or domains independently:
```go
import (
"fmt"
"github.com/leonelquinteros/gotext"
)
func main() {
// Configure package
gotext.Configure("/path/to/locales/root/dir", "en_UK", "domain-name")
// Set variables
name := "John"
// Translate text with variables
fmt.Println(gotext.Get("Hi, my name is %s", name))
}
l := gotext.NewLocale("/path/to/locales", "es_UY")
l.AddDomain("default")
fmt.Println(l.Get("Translate this"))
```
## Using Locale object
When having multiple languages/domains/libraries at the same time, you can create Locale objects for each variation
so you can handle each settings on their own.
### Use plural forms of translations
`gotext` handles complex pluralization rules defined in PO headers:
```go
import (
"fmt"
"github.com/leonelquinteros/gotext"
)
func main() {
// Create Locale with library path and language code
l := gotext.NewLocale("/path/to/locales/root/dir", "es_UY")
// Load domain '/path/to/locales/root/dir/es_UY/default.po'
l.AddDomain("default")
// Translate text from default domain
fmt.Println(l.Get("Translate this"))
// Load different domain
l.AddDomain("translations")
// Translate text from domain
fmt.Println(l.GetD("translations", "Translate this"))
}
// GetN(singular, plural, quantity, args...)
fmt.Println(gotext.GetN("I have one apple.", "I have %d apples.", 5, 5))
```
See the [Plural Forms Guide](docs/PLURALS.md) for more examples.
This is also helpful for using inside templates (from the "text/template" package), where you can pass the Locale object to the template.
If you set the Locale object as "Loc" in the template, then the template code would look like:
```
{{ .Loc.Get "Translate this" }}
```
## Using the Po object to handle .po files and PO-formatted strings
For when you need to work with PO files and strings,
you can directly use the Po object to parse it and access the translations in there in the same way.
### Using dynamic variables on translations
Supports standard `fmt` package syntax:
```go
import (
"fmt"
"github.com/leonelquinteros/gotext"
)
func main() {
// Set PO content
str := `
msgid "Translate this"
msgstr "Translated text"
msgid "Another string"
msgstr ""
msgid "One with var: %s"
msgstr "This one sets the var: %s"
`
// Create Po object
po := gotext.NewPo()
po.Parse(str)
fmt.Println(po.Get("Translate this"))
}
name := "John"
fmt.Println(gotext.Get("Hi, my name is %s", name))
```
### More advanced scenarios
- **Embedded translation files** (`embed.FS`) and named `%(name)s` formatting: see the [Advanced Usage Guide](docs/ADVANCED.md).
- **Languages fallback**: pass multiple languages to `Configure` as a colon-separated list (`gotext.Configure("/path/to/locales", "es_UY:es:en_US", "default")`).
- **Serialization/caching** of parsed locales: `Locale.MarshalBinary` / `UnmarshalBinary`.
## Use plural forms of translations
PO format supports defining one or more plural forms for the same translation.
Relying on the PO file headers, a Plural-Forms formula can be set on the translation file
as defined in (https://www.gnu.org/savannah-checkouts/gnu/gettext/manual/html_node/Plural-forms.html)
```go
import (
"fmt"
"github.com/leonelquinteros/gotext"
)
func main() {
// Set PO content
str := `
msgid ""
msgstr ""
# Header below
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "Translate this"
msgstr "Translated text"
msgid "Another string"
msgstr ""
msgid "One with var: %s"
msgid_plural "Several with vars: %s"
msgstr[0] "This one is the singular: %s"
msgstr[1] "This one is the plural: %s"
`
// Create Po object
po := new(gotext.Po)
po.Parse(str)
fmt.Println(po.GetN("One with var: %s", "Several with vars: %s", 54, v))
// "This one is the plural: Variable"
}
```
# Locales directories structure
The package will assume a directories structure starting with a base path that will be provided to the package configuration
or to object constructors depending on the use, but either will use the same convention to lookup inside the base path.
Inside the base directory where will be the language directories named using the language and country 2-letter codes (en_US, es_AR, ...).
All package functions can lookup after the simplified version for each language in case the full code isn't present but the more general language code exists.
So if the language set is `en_UK`, but there is no directory named after that code and there is a directory named `en`,
all package functions will be able to resolve this generalization and provide translations for the more general library.
The language codes are assumed to be [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) codes (2-letter codes).
That said, most functions will work with any coding standard as long the directory name matches the language code set on the configuration.
Then, there can be a `LC_MESSAGES` containing all PO files or the PO files themselves.
A library directory structure can look like:
---
## Locales directories structure
The package expects a standard Gettext directory structure:
```
/path/to/locales
/path/to/locales/en_US
/path/to/locales/en_US/LC_MESSAGES
/path/to/locales/en_US/LC_MESSAGES/default.po
/path/to/locales/en_US/LC_MESSAGES/extras.po
/path/to/locales/en_UK
/path/to/locales/en_UK/LC_MESSAGES
/path/to/locales/en_UK/LC_MESSAGES/default.po
/path/to/locales/en_UK/LC_MESSAGES/extras.po
/path/to/locales/en_AU
/path/to/locales/en_AU/LC_MESSAGES
/path/to/locales/en_AU/LC_MESSAGES/default.po
/path/to/locales/en_AU/LC_MESSAGES/extras.po
/path/to/locales/es
/path/to/locales/es/default.po
/path/to/locales/es/extras.po
/path/to/locales/es_ES
/path/to/locales/es_ES/default.po
/path/to/locales/es_ES/extras.po
/path/to/locales/fr
/path/to/locales/fr/default.po
/path/to/locales/fr/extras.po
/en_US
/LC_MESSAGES
default.po
/es_ES
/LC_MESSAGES
default.po
```
Translation files can live either inside an `LC_MESSAGES` subdirectory or directly under the language code (`/es_ES/default.po`); both layouts are supported.
And so on...
It supports automatic language simplification (e.g., falling back from `en_UK` to `en`).
---
# Contribute
## Contributing
We welcome contributions of all kinds!
- [Contributing Guidelines](CONTRIBUTING.md)
- [Code of Conduct](CODE_OF_CONDUCT.md)
- Please, contribute.
- Use the package on your projects.
- Report issues on Github.
- Send pull requests for bugfixes and improvements.
- Send proposals on Github issues.
---
## License
`gotext` is released under the [MIT License](LICENSE).
+317 -158
View File
@@ -4,7 +4,7 @@ import (
"bytes"
"encoding/gob"
"fmt"
"regexp"
"maps"
"sort"
"strconv"
"strings"
@@ -64,9 +64,6 @@ func (m HeaderMap) Del(key string) {
// Get value for key from HeaderMap
func (m HeaderMap) Get(key string) string {
if m == nil {
return ""
}
v := m[key]
if len(v) == 0 {
return ""
@@ -81,9 +78,6 @@ func (m HeaderMap) Set(key, value string) {
// Values returns all values for a given key from HeaderMap
func (m HeaderMap) Values(key string) []string {
if m == nil {
return nil
}
return m[key]
}
@@ -100,29 +94,111 @@ func NewDomain() *Domain {
return domain
}
func cloneRefs(refs []string) []string {
if refs == nil {
return nil
}
owned := make([]string, len(refs))
copy(owned, refs)
return owned
}
func cloneHeaderMap(headers HeaderMap) HeaderMap {
if headers == nil {
return nil
}
owned := make(HeaderMap, len(headers))
for key, values := range headers {
owned[key] = cloneRefs(values)
}
return owned
}
func cloneTranslation(trans *Translation) *Translation {
if trans == nil {
return nil
}
owned := NewTranslation()
owned.ID = trans.ID
owned.PluralID = trans.PluralID
owned.dirty = trans.dirty
owned.Refs = cloneRefs(trans.Refs)
maps.Copy(owned.Trs, trans.Trs)
return owned
}
func cloneTranslations(translations map[string]*Translation) map[string]*Translation {
if translations == nil {
return nil
}
owned := make(map[string]*Translation, len(translations))
for id, trans := range translations {
owned[id] = cloneTranslation(trans)
}
return owned
}
func cloneContextTranslations(contexts map[string]map[string]*Translation) map[string]map[string]*Translation {
if contexts == nil {
return nil
}
owned := make(map[string]map[string]*Translation, len(contexts))
for context, translations := range contexts {
owned[context] = cloneTranslations(translations)
}
return owned
}
func (do *Domain) hasTranslation(id string) bool {
do.trMutex.RLock()
defer do.trMutex.RUnlock()
return do.translations[id] != nil
}
func (do *Domain) hasContextTranslation(id, context string) bool {
do.trMutex.RLock()
defer do.trMutex.RUnlock()
return do.contextTranslations[context][id] != nil
}
// SetPluralResolver sets a custom plural resolver function
func (do *Domain) SetPluralResolver(f func(int) int) {
do.pluralMutex.Lock()
defer do.pluralMutex.Unlock()
do.customPluralResolver = f
}
func (do *Domain) pluralForm(n int) int {
// do we really need locking here? not sure how this plurals.Expression works, so sticking with it for now
// Snapshot plural state before calling an expression or custom resolver.
do.pluralMutex.RLock()
defer do.pluralMutex.RUnlock()
pluralforms := do.pluralforms
customPluralResolver := do.customPluralResolver
do.pluralMutex.RUnlock()
if pluralforms != nil {
return pluralforms.Eval(uint32(n))
}
// Failure fallback
if do.pluralforms == nil {
if do.customPluralResolver != nil {
return do.customPluralResolver(n)
}
/* Use the Germanic plural rule. */
if n == 1 {
return 0
}
return 1
if customPluralResolver != nil {
return customPluralResolver(n)
}
return do.pluralforms.Eval(uint32(n))
/* Use the Germanic plural rule. */
if n == 1 {
return 0
}
return 1
}
// parseHeaders retrieves data from previously parsed headers. it's called by both Mo and Po when parsing
@@ -136,18 +212,16 @@ func (do *Domain) parseHeaders() {
languageKey := "Language"
pluralFormsKey := "Plural-Forms"
rawLines := strings.Split(raw, "\n")
for _, line := range rawLines {
for line := range strings.SplitSeq(raw, "\n") {
if len(line) == 0 {
continue
}
colonIdx := strings.Index(line, ":")
if colonIdx < 0 {
key, value, ok := strings.Cut(line, ":")
if !ok {
continue
}
key := line[:colonIdx]
lowerKey := strings.ToLower(key)
if lowerKey == strings.ToLower(languageKey) {
languageKey = key
@@ -155,7 +229,7 @@ func (do *Domain) parseHeaders() {
pluralFormsKey = key
}
value := strings.TrimSpace(line[colonIdx+1:])
value = strings.TrimSpace(value)
do.Headers.Add(key, value)
}
@@ -168,22 +242,18 @@ func (do *Domain) parseHeaders() {
return
}
// Split plural form header value
pfs := strings.Split(do.PluralForms, ";")
// Parse values
for _, i := range pfs {
vs := strings.SplitN(i, "=", 2)
if len(vs) != 2 {
for i := range strings.SplitSeq(do.PluralForms, ";") {
key, value, ok := strings.Cut(i, "=")
if !ok {
continue
}
switch strings.TrimSpace(vs[0]) {
switch strings.TrimSpace(key) {
case "nplurals":
do.nplurals, _ = strconv.Atoi(vs[1])
do.nplurals, _ = strconv.Atoi(value)
case "plural":
do.plural = vs[1]
do.plural = value
if expr, err := plurals.Compile(do.plural); err == nil {
do.pluralforms = expr
@@ -221,17 +291,19 @@ func (do *Domain) DropStaleTranslations() {
// SetRefs set source references for a given translation
func (do *Domain) SetRefs(str string, refs []string) {
ownedRefs := cloneRefs(refs)
do.trMutex.Lock()
do.pluralMutex.Lock()
defer do.trMutex.Unlock()
defer do.pluralMutex.Unlock()
if trans, ok := do.translations[str]; ok {
trans.Refs = refs
if trans, ok := do.translations[str]; ok && trans != nil {
trans.SetRefs(ownedRefs)
} else {
trans = NewTranslation()
trans := NewTranslation()
trans.ID = str
trans.SetRefs(refs)
trans.SetRefs(ownedRefs)
do.translations[str] = trans
}
}
@@ -243,8 +315,8 @@ func (do *Domain) GetRefs(str string) []string {
defer do.trMutex.RUnlock()
if do.translations != nil {
if trans, ok := do.translations[str]; ok {
return trans.Refs
if trans, ok := do.translations[str]; ok && trans != nil {
return cloneRefs(trans.Refs)
}
}
return nil
@@ -268,7 +340,7 @@ func (do *Domain) Set(id, str string) {
}
// Get retrieves the Translation for the given string.
func (do *Domain) Get(str string, vars ...interface{}) string {
func (do *Domain) Get(str string, vars ...any) string {
// Sync read
do.trMutex.RLock()
defer do.trMutex.RUnlock()
@@ -284,8 +356,8 @@ func (do *Domain) Get(str string, vars ...interface{}) string {
}
// Append retrieves the Translation for the given string.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func (do *Domain) Append(b []byte, str string, vars ...interface{}) []byte {
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
func (do *Domain) Append(b []byte, str string, vars ...any) []byte {
// Sync read
do.trMutex.RLock()
defer do.trMutex.RUnlock()
@@ -322,40 +394,48 @@ func (do *Domain) SetN(id, plural string, n int, str string) {
}
// GetN retrieves the (N)th plural form of Translation for the given string.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func (do *Domain) GetN(str, plural string, n int, vars ...interface{}) string {
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
func (do *Domain) GetN(str, plural string, n int, vars ...any) string {
// Get plural form before acquiring the translation lock so a custom
// resolver is never called while a project lock is held.
pluralForm := do.pluralForm(n)
// Sync read
do.trMutex.RLock()
defer do.trMutex.RUnlock()
if do.translations != nil {
if _, ok := do.translations[str]; ok {
return FormatString(do.translations[str].GetN(do.pluralForm(n)), vars...)
return FormatString(do.translations[str].GetN(pluralForm), vars...)
}
}
// Parse plural forms to distinguish between plural and singular
if do.pluralForm(n) == 0 {
if pluralForm == 0 {
return FormatString(str, vars...)
}
return FormatString(plural, vars...)
}
// AppendN adds the (N)th plural form of Translation for the given string.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func (do *Domain) AppendN(b []byte, str, plural string, n int, vars ...interface{}) []byte {
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
func (do *Domain) AppendN(b []byte, str, plural string, n int, vars ...any) []byte {
// Get plural form before acquiring the translation lock so a custom
// resolver is never called while a project lock is held.
pluralForm := do.pluralForm(n)
// Sync read
do.trMutex.RLock()
defer do.trMutex.RUnlock()
if do.translations != nil {
if _, ok := do.translations[str]; ok {
return Appendf(b, do.translations[str].GetN(do.pluralForm(n)), vars...)
return Appendf(b, do.translations[str].GetN(pluralForm), vars...)
}
}
// Parse plural forms to distinguish between plural and singular
if do.pluralForm(n) == 0 {
if pluralForm == 0 {
return Appendf(b, str, vars...)
}
return Appendf(b, plural, vars...)
@@ -388,8 +468,8 @@ func (do *Domain) SetC(id, ctx, str string) {
}
// GetC retrieves the corresponding Translation for a given string in the given context.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func (do *Domain) GetC(str, ctx string, vars ...interface{}) string {
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
func (do *Domain) GetC(str, ctx string, vars ...any) string {
do.trMutex.RLock()
defer do.trMutex.RUnlock()
@@ -408,8 +488,8 @@ func (do *Domain) GetC(str, ctx string, vars ...interface{}) string {
}
// AppendC retrieves the corresponding Translation for a given string in the given context.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func (do *Domain) AppendC(b []byte, str, ctx string, vars ...interface{}) []byte {
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
func (do *Domain) AppendC(b []byte, str, ctx string, vars ...any) []byte {
do.trMutex.RLock()
defer do.trMutex.RUnlock()
@@ -443,12 +523,14 @@ func (do *Domain) SetNC(id, plural, ctx string, n int, str string) {
} else {
trans = NewTranslation()
trans.ID = id
trans.PluralID = plural
trans.SetN(pluralForm, str)
context[id] = trans
}
} else {
trans := NewTranslation()
trans.ID = id
trans.PluralID = plural
trans.SetN(pluralForm, str)
do.contextTranslations[ctx] = map[string]*Translation{
id: trans,
@@ -457,19 +539,22 @@ func (do *Domain) SetNC(id, plural, ctx string, n int, str string) {
}
// GetNC retrieves the (N)th plural form of Translation for the given string in the given context.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func (do *Domain) GetNC(str, plural string, n int, ctx string, vars ...interface{}) string {
do.trMutex.RLock()
defer do.trMutex.RUnlock()
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
func (do *Domain) GetNC(str, plural string, n int, ctx string, vars ...any) string {
if do.hasContextTranslation(str, ctx) {
// Get plural form before acquiring the translation lock so a custom
// resolver is never called while a project lock is held.
pluralForm := do.pluralForm(n)
if do.contextTranslations != nil {
if _, ok := do.contextTranslations[ctx]; ok {
if do.contextTranslations[ctx] != nil {
if _, ok := do.contextTranslations[ctx][str]; ok {
return FormatString(do.contextTranslations[ctx][str].GetN(do.pluralForm(n)), vars...)
}
do.trMutex.RLock()
if translations, ok := do.contextTranslations[ctx]; ok && translations != nil {
if trans, ok := translations[str]; ok && trans != nil {
translated := trans.GetN(pluralForm)
do.trMutex.RUnlock()
return FormatString(translated, vars...)
}
}
do.trMutex.RUnlock()
}
if n == 1 {
@@ -479,19 +564,22 @@ func (do *Domain) GetNC(str, plural string, n int, ctx string, vars ...interface
}
// AppendNC retrieves the (N)th plural form of Translation for the given string in the given context.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func (do *Domain) AppendNC(b []byte, str, plural string, n int, ctx string, vars ...interface{}) []byte {
do.trMutex.RLock()
defer do.trMutex.RUnlock()
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
func (do *Domain) AppendNC(b []byte, str, plural string, n int, ctx string, vars ...any) []byte {
if do.hasContextTranslation(str, ctx) {
// Get plural form before acquiring the translation lock so a custom
// resolver is never called while a project lock is held.
pluralForm := do.pluralForm(n)
if do.contextTranslations != nil {
if _, ok := do.contextTranslations[ctx]; ok {
if do.contextTranslations[ctx] != nil {
if _, ok := do.contextTranslations[ctx][str]; ok {
return Appendf(b, do.contextTranslations[ctx][str].GetN(do.pluralForm(n)), vars...)
}
do.trMutex.RLock()
if translations, ok := do.contextTranslations[ctx]; ok && translations != nil {
if trans, ok := translations[str]; ok && trans != nil {
translated := trans.GetN(pluralForm)
do.trMutex.RUnlock()
return Appendf(b, translated, vars...)
}
}
do.trMutex.RUnlock()
}
if n == 1 {
@@ -507,6 +595,14 @@ func (do *Domain) IsTranslated(str string) bool {
// IsTranslatedN reports whether a plural string is translated
func (do *Domain) IsTranslatedN(str string, n int) bool {
if !do.hasTranslation(str) {
return false
}
// Get plural form before acquiring the translation lock so a custom
// resolver is never called while a project lock is held.
pluralForm := do.pluralForm(n)
do.trMutex.RLock()
defer do.trMutex.RUnlock()
@@ -514,10 +610,10 @@ func (do *Domain) IsTranslatedN(str string, n int) bool {
return false
}
tr, ok := do.translations[str]
if !ok {
if !ok || tr == nil {
return false
}
return tr.IsTranslatedN(do.pluralForm(n))
return tr.IsTranslatedN(pluralForm)
}
// IsTranslatedC reports whether a context string is translated
@@ -527,6 +623,14 @@ func (do *Domain) IsTranslatedC(str, ctx string) bool {
// IsTranslatedNC reports whether a plural context string is translated
func (do *Domain) IsTranslatedNC(str string, n int, ctx string) bool {
if !do.hasContextTranslation(str, ctx) {
return false
}
// Get plural form before acquiring the translation lock so a custom
// resolver is never called while a project lock is held.
pluralForm := do.pluralForm(n)
do.trMutex.RLock()
defer do.trMutex.RUnlock()
@@ -534,36 +638,24 @@ func (do *Domain) IsTranslatedNC(str string, n int, ctx string) bool {
return false
}
translations, ok := do.contextTranslations[ctx]
if !ok {
if !ok || translations == nil {
return false
}
tr, ok := translations[str]
if !ok {
if !ok || tr == nil {
return false
}
return tr.IsTranslatedN(do.pluralForm(n))
return tr.IsTranslatedN(pluralForm)
}
// GetTranslations returns a copy of every translation in the domain. It does not support contexts.
func (do *Domain) GetTranslations() map[string]*Translation {
all := make(map[string]*Translation, len(do.translations))
do.trMutex.RLock()
defer do.trMutex.RUnlock()
all := make(map[string]*Translation, len(do.translations))
for msgID, trans := range do.translations {
newTrans := NewTranslation()
newTrans.ID = trans.ID
newTrans.PluralID = trans.PluralID
newTrans.dirty = trans.dirty
if len(trans.Refs) > 0 {
newTrans.Refs = make([]string, len(trans.Refs))
copy(newTrans.Refs, trans.Refs)
}
for k, v := range trans.Trs {
newTrans.Trs[k] = v
}
all[msgID] = newTrans
all[msgID] = cloneTranslation(trans)
}
return all
@@ -571,32 +663,18 @@ func (do *Domain) GetTranslations() map[string]*Translation {
// GetCtxTranslations returns a copy of every translation in the domain with context
func (do *Domain) GetCtxTranslations() map[string]map[string]*Translation {
all := make(map[string]map[string]*Translation, len(do.contextTranslations))
do.trMutex.RLock()
defer do.trMutex.RUnlock()
all := make(map[string]map[string]*Translation, len(do.contextTranslations))
for ctx, translations := range do.contextTranslations {
for msgID, trans := range translations {
newTrans := NewTranslation()
newTrans.ID = trans.ID
newTrans.PluralID = trans.PluralID
newTrans.dirty = trans.dirty
if len(trans.Refs) > 0 {
newTrans.Refs = make([]string, len(trans.Refs))
copy(newTrans.Refs, trans.Refs)
}
for k, v := range trans.Trs {
newTrans.Trs[k] = v
}
if all[ctx] == nil {
all[ctx] = make(map[string]*Translation)
}
all[ctx][msgID] = newTrans
all[ctx][msgID] = cloneTranslation(trans)
}
}
return all
@@ -604,10 +682,11 @@ func (do *Domain) GetCtxTranslations() map[string]map[string]*Translation {
// SourceReference is a struct to hold source reference information
type SourceReference struct {
path string
line int
context string
trans *Translation
path string
line int
context string
hasContext bool
trans *Translation
}
func extractPathAndLine(ref string) (string, int) {
@@ -627,6 +706,11 @@ func extractPathAndLine(ref string) (string, int) {
// MarshalText implements encoding.TextMarshaler interface
// Assists round-trip of POT/PO content
func (do *Domain) MarshalText() ([]byte, error) {
do.trMutex.RLock()
do.pluralMutex.RLock()
defer do.trMutex.RUnlock()
defer do.pluralMutex.RUnlock()
var buf bytes.Buffer
if len(do.headerComments) > 0 {
buf.WriteString(strings.Join(do.headerComments, "\n"))
@@ -681,31 +765,29 @@ func (do *Domain) MarshalText() ([]byte, error) {
v := do.Headers[k]
for _, value := range v {
buf.WriteString("\n\"" + k + ": " + value + "\\n\"")
encoded := EscapeSpecialCharacters(k + ": " + value + "\n")
buf.WriteString("\n\"" + encoded + "\"")
}
}
// Just as with headers, output translations in consistent order (to minimise diffs between round-trips), with (first) source reference taking priority, followed by context and finally ID
references := make([]SourceReference, 0)
for name, ctx := range do.contextTranslations {
for id, trans := range ctx {
if id == "" {
continue
}
for _, trans := range ctx {
if len(trans.Refs) > 0 {
path, line := extractPathAndLine(trans.Refs[0])
references = append(references, SourceReference{
path,
line,
name,
trans,
path: path,
line: line,
context: name,
hasContext: true,
trans: trans,
})
} else {
references = append(references, SourceReference{
"",
0,
name,
trans,
context: name,
hasContext: true,
trans: trans,
})
}
}
@@ -719,17 +801,13 @@ func (do *Domain) MarshalText() ([]byte, error) {
if len(trans.Refs) > 0 {
path, line := extractPathAndLine(trans.Refs[0])
references = append(references, SourceReference{
path,
line,
"",
trans,
path: path,
line: line,
trans: trans,
})
} else {
references = append(references, SourceReference{
"",
0,
"",
trans,
trans: trans,
})
}
}
@@ -754,6 +832,9 @@ func (do *Domain) MarshalText() ([]byte, error) {
if references[i].context > references[j].context {
return false
}
if references[i].hasContext != references[j].hasContext {
return !references[i].hasContext
}
return references[i].trans.ID < references[j].trans.ID
})
@@ -765,7 +846,7 @@ func (do *Domain) MarshalText() ([]byte, error) {
buf.WriteByte(byte('\n'))
}
if ref.context == "" {
if !ref.hasContext {
buf.WriteString("\nmsgid \"" + EscapeSpecialCharacters(trans.ID) + "\"")
} else {
buf.WriteString("\nmsgctxt \"" + EscapeSpecialCharacters(ref.context) + "\"\nmsgid \"" + EscapeSpecialCharacters(trans.ID) + "\"")
@@ -774,9 +855,16 @@ func (do *Domain) MarshalText() ([]byte, error) {
if trans.PluralID == "" {
buf.WriteString("\nmsgstr \"" + EscapeSpecialCharacters(trans.Trs[0]) + "\"")
} else {
buf.WriteString("\nmsgid_plural \"" + trans.PluralID + "\"")
for i, tr := range trans.Trs {
buf.WriteString("\nmsgstr[" + EscapeSpecialCharacters(strconv.Itoa(i)) + "] \"" + tr + "\"")
buf.WriteString("\nmsgid_plural \"" + EscapeSpecialCharacters(trans.PluralID) + "\"")
pluralIndexes := make([]int, 0, len(trans.Trs))
for i := range trans.Trs {
pluralIndexes = append(pluralIndexes, i)
}
sort.Ints(pluralIndexes)
for _, i := range pluralIndexes {
buf.WriteString("\nmsgstr[" + strconv.Itoa(i) + "] \"" + EscapeSpecialCharacters(trans.Trs[i]) + "\"")
}
}
}
@@ -786,8 +874,23 @@ func (do *Domain) MarshalText() ([]byte, error) {
// EscapeSpecialCharacters escapes special characters in a string
func EscapeSpecialCharacters(s string) string {
s = regexp.MustCompile(`([^\\])(")`).ReplaceAllString(s, "$1\\\"") // Escape non-escaped double quotation marks
var escaped strings.Builder
escaped.Grow(len(s))
for i := range len(s) {
switch s[i] {
case '\\':
escaped.WriteByte('\\')
escaped.WriteByte('\\')
case '"':
escaped.WriteByte('\\')
escaped.WriteByte('"')
default:
escaped.WriteByte(s[i])
}
}
s = escaped.String()
if strings.Count(s, "\n") == 0 {
return s
}
@@ -821,16 +924,26 @@ func EscapeSpecialCharacters(s string) string {
return strings.Join(data, "\n")
}
func (do *Domain) translatorEncodingSnapshot() *TranslatorEncoding {
do.trMutex.RLock()
do.pluralMutex.RLock()
defer do.trMutex.RUnlock()
defer do.pluralMutex.RUnlock()
return &TranslatorEncoding{
Headers: cloneHeaderMap(do.Headers),
Language: do.Language,
PluralForms: do.PluralForms,
Nplurals: do.nplurals,
Plural: do.plural,
Translations: cloneTranslations(do.translations),
Contexts: cloneContextTranslations(do.contextTranslations),
}
}
// MarshalBinary implements encoding.BinaryMarshaler interface
func (do *Domain) MarshalBinary() ([]byte, error) {
obj := new(TranslatorEncoding)
obj.Headers = do.Headers
obj.Language = do.Language
obj.PluralForms = do.PluralForms
obj.Nplurals = do.nplurals
obj.Plural = do.plural
obj.Translations = do.translations
obj.Contexts = do.contextTranslations
obj := do.translatorEncodingSnapshot()
var buff bytes.Buffer
encoder := gob.NewEncoder(&buff)
@@ -845,22 +958,68 @@ func (do *Domain) UnmarshalBinary(data []byte) error {
obj := new(TranslatorEncoding)
decoder := gob.NewDecoder(buff)
err := decoder.Decode(obj)
if err != nil {
if err := decoder.Decode(obj); err != nil {
return err
}
do.Headers = obj.Headers
headers := cloneHeaderMap(obj.Headers)
if headers == nil {
headers = make(HeaderMap)
}
translations := cloneTranslations(obj.Translations)
if translations == nil {
translations = make(map[string]*Translation)
}
contextTranslations := cloneContextTranslations(obj.Contexts)
if contextTranslations == nil {
contextTranslations = make(map[string]map[string]*Translation)
}
for id, trans := range translations {
if trans == nil {
trans = NewTranslation()
trans.ID = id
translations[id] = trans
}
}
for context, translationsForContext := range contextTranslations {
if translationsForContext == nil {
contextTranslations[context] = make(map[string]*Translation)
continue
}
for id, trans := range translationsForContext {
if trans == nil {
trans = NewTranslation()
trans.ID = id
translationsForContext[id] = trans
}
}
}
var pluralforms plurals.Expression
if expr, err := plurals.Compile(obj.Plural); err == nil {
pluralforms = expr
}
do.trMutex.Lock()
do.pluralMutex.Lock()
do.Headers = headers
do.Language = obj.Language
do.PluralForms = obj.PluralForms
do.nplurals = obj.Nplurals
do.plural = obj.Plural
do.translations = obj.Translations
do.contextTranslations = obj.Contexts
do.pluralforms = pluralforms
do.translations = translations
do.contextTranslations = contextTranslations
do.pluralTranslations = make(map[string]*Translation)
do.headerComments = make([]string, 0)
do.trBuffer = nil
do.ctxBuffer = ""
do.refBuffer = ""
if expr, err := plurals.Compile(do.plural); err == nil {
do.pluralforms = expr
}
do.pluralMutex.Unlock()
do.trMutex.Unlock()
return nil
}
+173 -111
View File
@@ -55,51 +55,59 @@ func init() {
domain: "default",
languages: []string{FallbackLocale},
library: "/usr/local/share/locale",
locales: nil,
locales: make([]*Locale, 0),
}
// Register Translator types for gob encoding
gob.Register(TranslatorEncoding{})
}
// loadLocales creates a new Locale object for every language (specified using Configure)
// at package level based on the configuration of global configuration .
// It is called when trying to use Get or GetD methods.
// loadLocales ensures package-level locales and their configured domain are loaded.
func loadLocales(rebuildCache bool) {
globalConfig.Lock()
if globalConfig.locales == nil || rebuildCache {
var locales []*Locale
locales := make([]*Locale, 0, len(globalConfig.languages))
for _, language := range globalConfig.languages {
locales = append(locales, NewLocale(globalConfig.library, language))
}
globalConfig.locales = locales
}
for _, locale := range globalConfig.locales {
if _, ok := locale.Domains[globalConfig.domain]; !ok || rebuildCache {
locale.AddDomain(globalConfig.domain)
}
locale.SetDomain(globalConfig.domain)
}
locales := append([]*Locale(nil), globalConfig.locales...)
domain := globalConfig.domain
globalConfig.Unlock()
for _, locale := range locales {
if locale == nil {
continue
}
if rebuildCache || !locale.hasDomain(domain) {
locale.AddDomain(domain)
}
locale.SetDomain(domain)
}
}
// GetDomain is the domain getter for the package configuration
func GetDomain() string {
var dom string
globalConfig.RLock()
if globalConfig.locales != nil {
// All locales have the same domain
dom = globalConfig.locales[0].GetDomain()
}
if dom == "" {
dom = globalConfig.domain
domain := globalConfig.domain
var locale *Locale
for _, candidate := range globalConfig.locales {
if candidate != nil {
locale = candidate
break
}
}
globalConfig.RUnlock()
return dom
if locale != nil {
if localeDomain := locale.GetDomain(); localeDomain != "" {
return localeDomain
}
}
return domain
}
// SetDomain sets the name for the domain to be used at package level.
@@ -107,8 +115,8 @@ func GetDomain() string {
func SetDomain(dom string) {
globalConfig.Lock()
globalConfig.domain = dom
if globalConfig.locales != nil {
for _, locale := range globalConfig.locales {
for _, locale := range globalConfig.locales {
if locale != nil {
locale.SetDomain(dom)
}
}
@@ -131,8 +139,10 @@ func GetLanguage() string {
// GetLanguages returns all languages that have been supplied.
func GetLanguages() []string {
globalConfig.RLock()
defer globalConfig.RUnlock()
return globalConfig.languages
languages := make([]string, len(globalConfig.languages))
copy(languages, globalConfig.languages)
globalConfig.RUnlock()
return languages
}
// SetLanguage sets the language code (or colon separated language codes) to be used at package level.
@@ -140,7 +150,7 @@ func GetLanguages() []string {
func SetLanguage(lang string) {
globalConfig.Lock()
var languages []string
for _, language := range strings.Split(lang, ":") {
for language := range strings.SplitSeq(lang, ":") {
languages = append(languages, SimplifiedLocale(language))
}
globalConfig.languages = languages
@@ -171,17 +181,20 @@ func SetLibrary(lib string) {
// GetLocales returns the locales that have been set for the package configuration.
func GetLocales() []*Locale {
globalConfig.RLock()
defer globalConfig.RUnlock()
return globalConfig.locales
locales := make([]*Locale, len(globalConfig.locales))
copy(locales, globalConfig.locales)
globalConfig.RUnlock()
return locales
}
// GetStorage is the locale storage getter for the package configuration.
//
// Deprecated: Storage has been renamed to Locale for consistency, use GetLocales instead.
func GetStorage() *Locale {
locales := GetLocales()
if len(locales) > 0 {
return locales[0]
for _, locale := range GetLocales() {
if locale != nil {
return locale
}
}
return nil
}
@@ -190,24 +203,40 @@ func GetStorage() *Locale {
// NewLocale(). This makes it possible to attach custom Domain objects from in-memory po/mo.
// The library, language and domain of the first Locale will set the default global configuration.
func SetLocales(locales []*Locale) {
ownedLocales := make([]*Locale, len(locales))
copy(ownedLocales, locales)
globalConfig.Lock()
defer globalConfig.Unlock()
globalConfig.locales = ownedLocales
globalConfig.locales = locales
globalConfig.library = locales[0].path
globalConfig.domain = locales[0].defaultDomain
languages := make([]string, 0, len(ownedLocales))
var first *Locale
for _, locale := range ownedLocales {
if locale == nil {
continue
}
var languages []string
for _, locale := range locales {
locale.RLock()
if first == nil {
first = locale
globalConfig.library = locale.path
globalConfig.domain = locale.defaultDomain
}
languages = append(languages, locale.lang)
locale.RUnlock()
}
globalConfig.languages = languages
globalConfig.Unlock()
}
// SetStorage allows overriding the global Locale object with one built manually with NewLocale().
//
// Deprecated: Storage has been renamed to Locale for consistency, use SetLocales instead.
func SetStorage(locale *Locale) {
if locale == nil {
SetLocales(nil)
return
}
SetLocales([]*Locale{locale})
}
@@ -219,7 +248,7 @@ func Configure(lib, lang, dom string) {
globalConfig.Lock()
globalConfig.library = lib
var languages []string
for _, language := range strings.Split(lang, ":") {
for language := range strings.SplitSeq(lang, ":") {
languages = append(languages, SimplifiedLocale(language))
}
globalConfig.languages = languages
@@ -230,114 +259,137 @@ func Configure(lib, lang, dom string) {
}
// Get uses the default domain globally set to return the corresponding Translation of a given string.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func Get(str string, vars ...interface{}) string {
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
func Get(str string, vars ...any) string {
return GetD(GetDomain(), str, vars...)
}
// GetN retrieves the (N)th plural form of Translation for the given string in the default domain.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func GetN(str, plural string, n int, vars ...interface{}) string {
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
func GetN(str, plural string, n int, vars ...any) string {
return GetND(GetDomain(), str, plural, n, vars...)
}
// GetD returns the corresponding Translation in the given domain for a given string.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func GetD(dom, str string, vars ...interface{}) string {
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
func GetD(dom, str string, vars ...any) string {
// Try to load default package Locales
loadLocales(false)
globalConfig.RLock()
defer globalConfig.RUnlock()
var tr string
for i, locale := range globalConfig.locales {
if _, ok := locale.Domains[dom]; !ok {
locale.AddDomain(dom)
}
if !locale.IsTranslatedD(dom, str) && i < (len(globalConfig.locales)-1) {
locales := GetLocales()
var fallback *Locale
for _, locale := range locales {
if locale == nil {
continue
}
tr = locale.GetD(dom, str, vars...)
break
fallback = locale
if !locale.hasDomain(dom) {
locale.AddDomain(dom)
}
if locale.IsTranslatedD(dom, str) {
return locale.GetD(dom, str, vars...)
}
}
return tr
if fallback != nil {
return fallback.GetD(dom, str, vars...)
}
return FormatString(str, vars...)
}
// GetND retrieves the (N)th plural form of Translation in the given domain for a given string.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func GetND(dom, str, plural string, n int, vars ...interface{}) string {
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
func GetND(dom, str, plural string, n int, vars ...any) string {
// Try to load default package Locales
loadLocales(false)
globalConfig.RLock()
defer globalConfig.RUnlock()
var tr string
for i, locale := range globalConfig.locales {
if _, ok := locale.Domains[dom]; !ok {
locale.AddDomain(dom)
}
if !locale.IsTranslatedND(dom, str, n) && i < (len(globalConfig.locales)-1) {
locales := GetLocales()
var fallback *Locale
for _, locale := range locales {
if locale == nil {
continue
}
tr = locale.GetND(dom, str, plural, n, vars...)
break
fallback = locale
if !locale.hasDomain(dom) {
locale.AddDomain(dom)
}
if locale.IsTranslatedND(dom, str, n) {
return locale.GetND(dom, str, plural, n, vars...)
}
}
return tr
if fallback != nil {
return fallback.GetND(dom, str, plural, n, vars...)
}
if n == 1 {
return FormatString(str, vars...)
}
return FormatString(plural, vars...)
}
// GetC uses the default domain globally set to return the corresponding Translation of the given string in the given context.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func GetC(str, ctx string, vars ...interface{}) string {
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
func GetC(str, ctx string, vars ...any) string {
return GetDC(GetDomain(), str, ctx, vars...)
}
// GetNC retrieves the (N)th plural form of Translation for the given string in the given context in the default domain.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func GetNC(str, plural string, n int, ctx string, vars ...interface{}) string {
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
func GetNC(str, plural string, n int, ctx string, vars ...any) string {
return GetNDC(GetDomain(), str, plural, n, ctx, vars...)
}
// GetDC returns the corresponding Translation in the given domain for the given string in the given context.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func GetDC(dom, str, ctx string, vars ...interface{}) string {
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
func GetDC(dom, str, ctx string, vars ...any) string {
// Try to load default package Locales
loadLocales(false)
globalConfig.RLock()
defer globalConfig.RUnlock()
var tr string
for i, locale := range globalConfig.locales {
if !locale.IsTranslatedDC(dom, str, ctx) && i < (len(globalConfig.locales)-1) {
locales := GetLocales()
var fallback *Locale
for _, locale := range locales {
if locale == nil {
continue
}
tr = locale.GetDC(dom, str, ctx, vars...)
break
fallback = locale
if !locale.hasDomain(dom) {
locale.AddDomain(dom)
}
if locale.IsTranslatedDC(dom, str, ctx) {
return locale.GetDC(dom, str, ctx, vars...)
}
}
return tr
if fallback != nil {
return fallback.GetDC(dom, str, ctx, vars...)
}
return FormatString(str, vars...)
}
// GetNDC retrieves the (N)th plural form of Translation in the given domain for a given string.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func GetNDC(dom, str, plural string, n int, ctx string, vars ...interface{}) string {
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
func GetNDC(dom, str, plural string, n int, ctx string, vars ...any) string {
// Try to load default package Locales
loadLocales(false)
// Return Translation
globalConfig.RLock()
defer globalConfig.RUnlock()
var tr string
for i, locale := range globalConfig.locales {
if !locale.IsTranslatedNDC(dom, str, n, ctx) && i < (len(globalConfig.locales)-1) {
locales := GetLocales()
var fallback *Locale
for _, locale := range locales {
if locale == nil {
continue
}
tr = locale.GetNDC(dom, str, plural, n, ctx, vars...)
break
fallback = locale
if !locale.hasDomain(dom) {
locale.AddDomain(dom)
}
if locale.IsTranslatedNDC(dom, str, n, ctx) {
return locale.GetNDC(dom, str, plural, n, ctx, vars...)
}
}
return tr
if fallback != nil {
return fallback.GetNDC(dom, str, plural, n, ctx, vars...)
}
if n == 1 {
return FormatString(str, vars...)
}
return FormatString(plural, vars...)
}
// IsTranslated reports whether a string is translated in given languages.
@@ -366,18 +418,23 @@ func IsTranslatedND(dom, str string, n int, langs ...string) bool {
}
loadLocales(false)
globalConfig.RLock()
defer globalConfig.RUnlock()
locales := GetLocales()
for _, lang := range langs {
lang = SimplifiedLocale(lang)
for _, supportedLocale := range globalConfig.locales {
if lang != supportedLocale.GetActualLanguage(dom) {
for _, supportedLocale := range locales {
if supportedLocale == nil {
continue
}
return supportedLocale.IsTranslatedND(dom, str, n)
if lang != supportedLocale.GetLanguage() && lang != supportedLocale.GetActualLanguage(dom) {
continue
}
if !supportedLocale.hasDomain(dom) {
supportedLocale.AddDomain(dom)
}
if supportedLocale.IsTranslatedND(dom, str, n) {
return true
}
}
}
return false
@@ -398,7 +455,7 @@ func IsTranslatedNC(str string, n int, ctx string, langs ...string) bool {
// IsTranslatedDC reports whether a domain context string is translated in given languages.
// When the langs argument is omitted, the output of GetLanguages is used.
func IsTranslatedDC(dom, str, ctx string, langs ...string) bool {
return IsTranslatedNDC(dom, str, 0, ctx, langs...)
return IsTranslatedNDC(dom, str, 1, ctx, langs...)
}
// IsTranslatedNDC reports whether a plural domain context string is translated in any of given languages.
@@ -409,18 +466,23 @@ func IsTranslatedNDC(dom, str string, n int, ctx string, langs ...string) bool {
}
loadLocales(false)
globalConfig.RLock()
defer globalConfig.RUnlock()
locales := GetLocales()
for _, lang := range langs {
lang = SimplifiedLocale(lang)
for _, locale := range globalConfig.locales {
if lang != locale.GetActualLanguage(dom) {
for _, supportedLocale := range locales {
if supportedLocale == nil {
continue
}
return locale.IsTranslatedNDC(dom, str, n, ctx)
if lang != supportedLocale.GetLanguage() && lang != supportedLocale.GetActualLanguage(dom) {
continue
}
if !supportedLocale.hasDomain(dom) {
supportedLocale.AddDomain(dom)
}
if supportedLocale.IsTranslatedNDC(dom, str, n, ctx) {
return true
}
}
}
return false
+31 -27
View File
@@ -29,16 +29,21 @@ func SimplifiedLocale(lang string) string {
}
// FormatString applies text formatting only when needed to parse variables.
func FormatString(str string, vars ...interface{}) string {
func FormatString(str string, vars ...any) string {
if len(vars) > 0 {
return fmt.Sprintf(str, vars...)
return FormatStringWithArgs(str, vars)
}
return str
}
// FormatStringWithArgs applies text formatting as a proxy for fmt.Sprintf.
func FormatStringWithArgs(str string, vars []any) string {
return fmt.Sprintf(str, vars...)
}
// Appendf applies text formatting only when needed to parse variables.
func Appendf(b []byte, str string, vars ...interface{}) []byte {
func Appendf(b []byte, str string, vars ...any) []byte {
if len(vars) > 0 {
return fmt.Appendf(b, str, vars...)
}
@@ -47,50 +52,49 @@ func Appendf(b []byte, str string, vars ...interface{}) []byte {
}
// NPrintf support named format
// NPrintf("%(name)s is Type %(type)s", map[string]interface{}{"name": "Gotext", "type": "struct"})
func NPrintf(format string, params map[string]interface{}) {
// NPrintf("%(name)s is Type %(type)s", map[string]any{"name": "Gotext", "type": "struct"})
func NPrintf(format string, params map[string]any) {
f, p := parseSprintf(format, params)
fmt.Printf(f, p...)
}
// Sprintf support named format
//
// Sprintf("%(name)s is Type %(type)s", map[string]interface{}{"name": "Gotext", "type": "struct"})
func Sprintf(format string, params map[string]interface{}) string {
// Sprintf("%(name)s is Type %(type)s", map[string]any{"name": "Gotext", "type": "struct"})
func Sprintf(format string, params map[string]any) string {
f, p := parseSprintf(format, params)
return fmt.Sprintf(f, p...)
}
func parseSprintf(format string, params map[string]interface{}) (string, []interface{}) {
func parseSprintf(format string, params map[string]any) (string, []any) {
f, n := reformatSprintf(format)
var p []interface{}
for _, v := range n {
p = append(p, params[v])
var p []any
if len(n) > 0 {
p = make([]any, len(n))
for i, v := range n {
p[i] = params[v]
}
}
return f, p
}
func reformatSprintf(f string) (string, []string) {
m := re.FindAllStringSubmatch(f, -1)
i := re.FindAllStringSubmatchIndex(f, -1)
matches := re.FindAllStringSubmatchIndex(f, -1)
ord := make([]string, 0, len(matches))
pair := make([]int, 1, 1+2*len(matches))
pair[0] = 0
ord := []string{}
for _, v := range m {
ord = append(ord, v[1])
}
pair := []int{0}
for _, v := range i {
pair = append(pair, v[2]-1)
pair = append(pair, v[3]+1)
for _, match := range matches {
ord = append(ord, f[match[2]:match[3]])
pair = append(pair, match[2]-1, match[3]+1)
}
pair = append(pair, len(f))
plen := len(pair)
out := ""
for n := 0; n < plen; n += 2 {
out += f[pair[n]:pair[n+1]]
var out strings.Builder
out.Grow(len(f))
for n := 0; n < len(pair); n += 2 {
out.WriteString(f[pair[n]:pair[n+1]])
}
return out, ord
return out.String(), ord
}
+214 -163
View File
@@ -9,6 +9,7 @@ import (
"bytes"
"encoding/gob"
"io/fs"
"maps"
"os"
"path"
"sync"
@@ -22,8 +23,6 @@ multiple languages at the same time by working with this object.
Example:
import (
"encoding/gob"
"bytes"
"fmt"
"github.com/leonelquinteros/gotext"
)
@@ -90,95 +89,114 @@ func NewLocaleFSWithPath(l string, filesystem fs.FS, p string) *Locale {
return loc
}
func (l *Locale) findExt(dom, ext string) string {
filename := path.Join(l.path, l.lang, "LC_MESSAGES", dom+"."+ext)
if l.fileExists(filename) {
return filename
func (l *Locale) snapshotPathLanguageFS() (string, string, fs.FS) {
if l == nil {
return "", "", nil
}
if len(l.lang) > 2 {
filename = path.Join(l.path, l.lang[:2], "LC_MESSAGES", dom+"."+ext)
if l.fileExists(filename) {
return filename
}
}
filename = path.Join(l.path, l.lang, dom+"."+ext)
if l.fileExists(filename) {
return filename
}
if len(l.lang) > 2 {
filename = path.Join(l.path, l.lang[:2], dom+"."+ext)
if l.fileExists(filename) {
return filename
}
}
return ""
l.RLock()
p := l.path
lang := l.lang
filesystem := l.fs
l.RUnlock()
return p, lang, filesystem
}
// GetActualLanguage inspects the filesystem and decides whether to strip
// a CC part of the ll_CC locale string.
func (l *Locale) GetActualLanguage(dom string) string {
extensions := []string{"mo", "po"}
var fp string
for _, ext := range extensions {
// 'll' (or 'll_CC') exists, and it was specified as-is
fp = path.Join(l.path, l.lang, "LC_MESSAGES", dom+"."+ext)
if l.fileExists(fp) {
return l.lang
}
// 'll' exists, but 'll_CC' was specified
if len(l.lang) > 2 {
fp = path.Join(l.path, l.lang[:2], "LC_MESSAGES", dom+"."+ext)
if l.fileExists(fp) {
return l.lang[:2]
}
}
// 'll' (or 'll_CC') exists outside of LC_category, and it was specified as-is
fp = path.Join(l.path, l.lang, dom+"."+ext)
if l.fileExists(fp) {
return l.lang
}
// 'll' exists outside of LC_category, but 'll_CC' was specified
if len(l.lang) > 2 {
fp = path.Join(l.path, l.lang[:2], dom+"."+ext)
if l.fileExists(fp) {
return l.lang[:2]
}
}
}
return ""
}
func (l *Locale) fileExists(filename string) bool {
if l.fs != nil {
_, err := fs.Stat(l.fs, filename)
func fileExistsIn(filesystem fs.FS, filename string) bool {
if filesystem != nil {
_, err := fs.Stat(filesystem, filename)
return err == nil
}
_, err := os.Stat(filename)
return err == nil
}
func (l *Locale) findExtWithFS(dom, ext string) (fs.FS, string) {
p, lang, filesystem := l.snapshotPathLanguageFS()
filename := path.Join(p, lang, "LC_MESSAGES", dom+"."+ext)
if fileExistsIn(filesystem, filename) {
return filesystem, filename
}
if len(lang) > 2 {
filename = path.Join(p, lang[:2], "LC_MESSAGES", dom+"."+ext)
if fileExistsIn(filesystem, filename) {
return filesystem, filename
}
}
filename = path.Join(p, lang, dom+"."+ext)
if fileExistsIn(filesystem, filename) {
return filesystem, filename
}
if len(lang) > 2 {
filename = path.Join(p, lang[:2], dom+"."+ext)
if fileExistsIn(filesystem, filename) {
return filesystem, filename
}
}
return filesystem, ""
}
// GetActualLanguage inspects the filesystem and decides whether to strip
// a CC part of the ll_CC locale string.
func (l *Locale) GetActualLanguage(dom string) string {
p, lang, filesystem := l.snapshotPathLanguageFS()
extensions := []string{"po", "mo"}
var fp string
for _, ext := range extensions {
// 'll' (or 'll_CC') exists, and it was specified as-is
fp = path.Join(p, lang, "LC_MESSAGES", dom+"."+ext)
if fileExistsIn(filesystem, fp) {
return lang
}
// 'll' exists, but 'll_CC' was specified
if len(lang) > 2 {
fp = path.Join(p, lang[:2], "LC_MESSAGES", dom+"."+ext)
if fileExistsIn(filesystem, fp) {
return lang[:2]
}
}
// 'll' (or 'll_CC') exists outside of LC_category, and it was specified as-is
fp = path.Join(p, lang, dom+"."+ext)
if fileExistsIn(filesystem, fp) {
return lang
}
// 'll' exists outside of LC_category, but 'll_CC' was specified
if len(lang) > 2 {
fp = path.Join(p, lang[:2], dom+"."+ext)
if fileExistsIn(filesystem, fp) {
return lang[:2]
}
}
}
return ""
}
// AddDomain creates a new domain for a given locale object and initializes the Po object.
// If the domain exists, it gets reloaded.
func (l *Locale) AddDomain(dom string) {
if l == nil {
return
}
var poObj Translator
file := l.findExt(dom, "po")
filesystem, file := l.findExtWithFS(dom, "po")
if file != "" {
poObj = NewPoFS(l.fs)
poObj = NewPoFS(filesystem)
// Parse file.
poObj.ParseFile(file)
} else {
file = l.findExt(dom, "mo")
filesystem, file = l.findExtWithFS(dom, "mo")
if file != "" {
poObj = NewMoFS(l.fs)
poObj = NewMoFS(filesystem)
// Parse file.
poObj.ParseFile(file)
} else {
// fallback return if no file found with
return
}
}
@@ -212,9 +230,34 @@ func (l *Locale) AddTranslator(dom string, tr Translator) {
l.Unlock()
}
func (l *Locale) translatorFor(dom string) Translator {
if l == nil {
return nil
}
l.RLock()
translator := l.Domains[dom]
l.RUnlock()
return translator
}
func (l *Locale) hasDomain(dom string) bool {
if l == nil {
return false
}
l.RLock()
_, ok := l.Domains[dom]
l.RUnlock()
return ok
}
// GetDomain is the domain getter for Locale configuration
func (l *Locale) GetDomain() string {
if l == nil {
return ""
}
l.RLock()
dom := l.defaultDomain
l.RUnlock()
@@ -223,6 +266,10 @@ func (l *Locale) GetDomain() string {
// SetDomain sets the name for the domain to be used.
func (l *Locale) SetDomain(dom string) {
if l == nil {
return
}
l.Lock()
l.defaultDomain = dom
l.Unlock()
@@ -230,6 +277,10 @@ func (l *Locale) SetDomain(dom string) {
// GetLanguage is the lang getter for Locale configuration
func (l *Locale) GetLanguage() string {
if l == nil {
return ""
}
l.RLock()
lang := l.lang
l.RUnlock()
@@ -237,51 +288,37 @@ func (l *Locale) GetLanguage() string {
}
// Get uses a domain "default" to return the corresponding Translation of a given string.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func (l *Locale) Get(str string, vars ...interface{}) string {
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
func (l *Locale) Get(str string, vars ...any) string {
return l.GetD(l.GetDomain(), str, vars...)
}
// GetN retrieves the (N)th plural form of Translation for the given string in the "default" domain.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func (l *Locale) GetN(str, plural string, n int, vars ...interface{}) string {
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
func (l *Locale) GetN(str, plural string, n int, vars ...any) string {
return l.GetND(l.GetDomain(), str, plural, n, vars...)
}
// GetD returns the corresponding Translation in the given domain for the given string.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func (l *Locale) GetD(dom, str string, vars ...interface{}) string {
// Sync read
l.RLock()
defer l.RUnlock()
if l.Domains != nil {
if _, ok := l.Domains[dom]; ok {
if l.Domains[dom] != nil {
return l.Domains[dom].Get(str, vars...)
}
}
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
func (l *Locale) GetD(dom, str string, vars ...any) string {
translator := l.translatorFor(dom)
if translator != nil {
return translator.Get(str, vars...)
}
return FormatString(str, vars...)
}
// GetND retrieves the (N)th plural form of Translation in the given domain for the given string.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func (l *Locale) GetND(dom, str, plural string, n int, vars ...interface{}) string {
// Sync read
l.RLock()
defer l.RUnlock()
if l.Domains != nil {
if _, ok := l.Domains[dom]; ok {
if l.Domains[dom] != nil {
return l.Domains[dom].GetN(str, plural, n, vars...)
}
}
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
func (l *Locale) GetND(dom, str, plural string, n int, vars ...any) string {
translator := l.translatorFor(dom)
if translator != nil {
return translator.GetN(str, plural, n, vars...)
}
// Use western default rule (plural > 1) to handle missing domain default result.
// Use western default rule (n != 1) to handle missing domain default result.
if n == 1 {
return FormatString(str, vars...)
}
@@ -289,51 +326,37 @@ func (l *Locale) GetND(dom, str, plural string, n int, vars ...interface{}) stri
}
// GetC uses a domain "default" to return the corresponding Translation of the given string in the given context.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func (l *Locale) GetC(str, ctx string, vars ...interface{}) string {
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
func (l *Locale) GetC(str, ctx string, vars ...any) string {
return l.GetDC(l.GetDomain(), str, ctx, vars...)
}
// GetNC retrieves the (N)th plural form of Translation for the given string in the given context in the "default" domain.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func (l *Locale) GetNC(str, plural string, n int, ctx string, vars ...interface{}) string {
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
func (l *Locale) GetNC(str, plural string, n int, ctx string, vars ...any) string {
return l.GetNDC(l.GetDomain(), str, plural, n, ctx, vars...)
}
// GetDC returns the corresponding Translation in the given domain for the given string in the given context.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func (l *Locale) GetDC(dom, str, ctx string, vars ...interface{}) string {
// Sync read
l.RLock()
defer l.RUnlock()
if l.Domains != nil {
if _, ok := l.Domains[dom]; ok {
if l.Domains[dom] != nil {
return l.Domains[dom].GetC(str, ctx, vars...)
}
}
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
func (l *Locale) GetDC(dom, str, ctx string, vars ...any) string {
translator := l.translatorFor(dom)
if translator != nil {
return translator.GetC(str, ctx, vars...)
}
return FormatString(str, vars...)
}
// GetNDC retrieves the (N)th plural form of Translation in the given domain for the given string in the given context.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func (l *Locale) GetNDC(dom, str, plural string, n int, ctx string, vars ...interface{}) string {
// Sync read
l.RLock()
defer l.RUnlock()
if l.Domains != nil {
if _, ok := l.Domains[dom]; ok {
if l.Domains[dom] != nil {
return l.Domains[dom].GetNC(str, plural, n, ctx, vars...)
}
}
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
func (l *Locale) GetNDC(dom, str, plural string, n int, ctx string, vars ...any) string {
translator := l.translatorFor(dom)
if translator != nil {
return translator.GetNC(str, plural, n, ctx, vars...)
}
// Use western default rule (plural > 1) to handle missing domain default result.
// Use western default rule (n != 1) to handle missing domain default result.
if n == 1 {
return FormatString(str, vars...)
}
@@ -343,13 +366,26 @@ func (l *Locale) GetNDC(dom, str, plural string, n int, ctx string, vars ...inte
// GetTranslations returns a copy of all translations in all domains of this locale. It does not support contexts.
func (l *Locale) GetTranslations() map[string]*Translation {
all := make(map[string]*Translation)
if l == nil {
return all
}
l.RLock()
defer l.RUnlock()
translators := make([]Translator, 0, len(l.Domains))
for _, translator := range l.Domains {
for msgID, trans := range translator.GetDomain().GetTranslations() {
all[msgID] = trans
translators = append(translators, translator)
}
l.RUnlock()
for _, translator := range translators {
if translator == nil {
continue
}
domain := translator.GetDomain()
if domain == nil {
continue
}
maps.Copy(all, domain.GetTranslations())
}
return all
@@ -357,7 +393,7 @@ func (l *Locale) GetTranslations() map[string]*Translation {
// IsTranslated reports whether a string is translated
func (l *Locale) IsTranslated(str string) bool {
return l.IsTranslatedND(l.GetDomain(), str, 0)
return l.IsTranslatedND(l.GetDomain(), str, 1)
}
// IsTranslatedN reports whether a plural string is translated
@@ -367,27 +403,25 @@ func (l *Locale) IsTranslatedN(str string, n int) bool {
// IsTranslatedD reports whether a domain string is translated
func (l *Locale) IsTranslatedD(dom, str string) bool {
return l.IsTranslatedND(dom, str, 0)
return l.IsTranslatedND(dom, str, 1)
}
// IsTranslatedND reports whether a plural domain string is translated
func (l *Locale) IsTranslatedND(dom, str string, n int) bool {
l.RLock()
defer l.RUnlock()
if l.Domains == nil {
translator := l.translatorFor(dom)
if translator == nil {
return false
}
translator, ok := l.Domains[dom]
if !ok {
domain := translator.GetDomain()
if domain == nil {
return false
}
return translator.GetDomain().IsTranslatedN(str, n)
return domain.IsTranslatedN(str, n)
}
// IsTranslatedC reports whether a context string is translated
func (l *Locale) IsTranslatedC(str, ctx string) bool {
return l.IsTranslatedNDC(l.GetDomain(), str, 0, ctx)
return l.IsTranslatedNDC(l.GetDomain(), str, 1, ctx)
}
// IsTranslatedNC reports whether a plural context string is translated
@@ -397,22 +431,20 @@ func (l *Locale) IsTranslatedNC(str string, n int, ctx string) bool {
// IsTranslatedDC reports whether a domain context string is translated
func (l *Locale) IsTranslatedDC(dom, str, ctx string) bool {
return l.IsTranslatedNDC(dom, str, 0, ctx)
return l.IsTranslatedNDC(dom, str, 1, ctx)
}
// IsTranslatedNDC reports whether a plural domain context string is translated
func (l *Locale) IsTranslatedNDC(dom string, str string, n int, ctx string) bool {
l.RLock()
defer l.RUnlock()
if l.Domains == nil {
translator := l.translatorFor(dom)
if translator == nil {
return false
}
translator, ok := l.Domains[dom]
if !ok {
domain := translator.GetDomain()
if domain == nil {
return false
}
return translator.GetDomain().IsTranslatedNC(str, n, ctx)
return domain.IsTranslatedNC(str, n, ctx)
}
// LocaleEncoding is used as intermediary storage to encode Locale objects to Gob.
@@ -425,18 +457,35 @@ type LocaleEncoding struct {
// MarshalBinary implements encoding BinaryMarshaler interface
func (l *Locale) MarshalBinary() ([]byte, error) {
obj := new(LocaleEncoding)
obj.DefaultDomain = l.defaultDomain
obj.Domains = make(map[string][]byte)
for k, v := range l.Domains {
var err error
obj.Domains[k], err = v.MarshalBinary()
var localePath, lang, defaultDomain string
domains := make(map[string]Translator)
if l != nil {
l.RLock()
localePath = l.path
lang = l.lang
defaultDomain = l.defaultDomain
domains = make(map[string]Translator, len(l.Domains))
for name, translator := range l.Domains {
if translator != nil {
domains[name] = translator
}
}
l.RUnlock()
}
obj := &LocaleEncoding{
Path: localePath,
Lang: lang,
Domains: make(map[string][]byte, len(domains)),
DefaultDomain: defaultDomain,
}
for name, translator := range domains {
data, err := translator.MarshalBinary()
if err != nil {
return nil, err
}
obj.Domains[name] = data
}
obj.Lang = l.lang
obj.Path = l.path
var buff bytes.Buffer
encoder := gob.NewEncoder(&buff)
@@ -445,7 +494,7 @@ func (l *Locale) MarshalBinary() ([]byte, error) {
return buff.Bytes(), err
}
// UnmarshalBinary implements encoding BinaryUnmarshaler interface
// UnmarshalBinary implements encoding.BinaryUnmarshaler interface
func (l *Locale) UnmarshalBinary(data []byte) error {
buff := bytes.NewBuffer(data)
obj := new(LocaleEncoding)
@@ -456,12 +505,7 @@ func (l *Locale) UnmarshalBinary(data []byte) error {
return err
}
l.defaultDomain = obj.DefaultDomain
l.lang = obj.Lang
l.path = obj.Path
// Decode Domains
l.Domains = make(map[string]Translator)
domains := make(map[string]Translator, len(obj.Domains))
for k, v := range obj.Domains {
var tr TranslatorEncoding
buff := bytes.NewBuffer(v)
@@ -471,8 +515,15 @@ func (l *Locale) UnmarshalBinary(data []byte) error {
return err
}
l.Domains[k] = tr.GetTranslator()
domains[k] = tr.GetTranslator()
}
l.Lock()
l.defaultDomain = obj.DefaultDomain
l.lang = obj.Lang
l.path = obj.Path
l.Domains = domains
l.Unlock()
return nil
}
+40
View File
@@ -0,0 +1,40 @@
site_name: gotext
site_description: GNU gettext utilities for Go
site_author: Leonel Quinteros
repo_url: https://github.com/leonelquinteros/gotext
edit_uri: edit/master/docs/
theme:
name: material
palette:
primary: indigo
accent: indigo
features:
- navigation.tabs
- navigation.sections
- navigation.top
- search.suggest
- search.highlight
nav:
- Home: index.md
- Getting Started: GETTING_STARTED.md
- User Guides:
- Plural Forms: PLURALS.md
- xgotext CLI: xgotext.md
- Character Encoding: ENCODING.md
- Advanced Usage: ADVANCED.md
- Best Practices: BEST_PRACTICES.md
- API Reference: https://pkg.go.dev/github.com/leonelquinteros/gotext
- Contributing:
- Guidelines: CONTRIBUTING.md
- Code of Conduct: CODE_OF_CONDUCT.md
- License: LICENSE.md
markdown_extensions:
- admonition
- pymdownx.highlight:
anchor_linenums: true
- pymdownx.inlinehilite
- pymdownx.snippets
- pymdownx.superfences
+65 -97
View File
@@ -76,42 +76,42 @@ func (mo *Mo) GetDomain() *Domain {
}
// Get returns the translation for the given string
func (mo *Mo) Get(str string, vars ...interface{}) string {
func (mo *Mo) Get(str string, vars ...any) string {
return mo.domain.Get(str, vars...)
}
// Append a translation string into the domain
func (mo *Mo) Append(b []byte, str string, vars ...interface{}) []byte {
func (mo *Mo) Append(b []byte, str string, vars ...any) []byte {
return mo.domain.Append(b, str, vars...)
}
// GetN returns the translation for the given string and plural form
func (mo *Mo) GetN(str, plural string, n int, vars ...interface{}) string {
func (mo *Mo) GetN(str, plural string, n int, vars ...any) string {
return mo.domain.GetN(str, plural, n, vars...)
}
// AppendN appends a translation string for the given plural form into the domain
func (mo *Mo) AppendN(b []byte, str, plural string, n int, vars ...interface{}) []byte {
func (mo *Mo) AppendN(b []byte, str, plural string, n int, vars ...any) []byte {
return mo.domain.AppendN(b, str, plural, n, vars...)
}
// GetC returns the translation for the given string and context
func (mo *Mo) GetC(str, ctx string, vars ...interface{}) string {
func (mo *Mo) GetC(str, ctx string, vars ...any) string {
return mo.domain.GetC(str, ctx, vars...)
}
// AppendC appends a translation string for the given context into the domain
func (mo *Mo) AppendC(b []byte, str, ctx string, vars ...interface{}) []byte {
func (mo *Mo) AppendC(b []byte, str, ctx string, vars ...any) []byte {
return mo.domain.AppendC(b, str, ctx, vars...)
}
// GetNC returns the translation for the given string, plural form and context
func (mo *Mo) GetNC(str, plural string, n int, ctx string, vars ...interface{}) string {
func (mo *Mo) GetNC(str, plural string, n int, ctx string, vars ...any) string {
return mo.domain.GetNC(str, plural, n, ctx, vars...)
}
// AppendNC appends a translation string for the given plural form and context into the domain
func (mo *Mo) AppendNC(b []byte, str, plural string, n int, ctx string, vars ...interface{}) []byte {
func (mo *Mo) AppendNC(b []byte, str, plural string, n int, ctx string, vars ...any) []byte {
return mo.domain.AppendNC(b, str, plural, n, ctx, vars...)
}
@@ -163,22 +163,18 @@ func (mo *Mo) Parse(buf []byte) {
defer mo.domain.trMutex.Unlock()
defer mo.domain.pluralMutex.Unlock()
r := bytes.NewReader(buf)
var magicNumber uint32
if err := binary.Read(r, binary.LittleEndian, &magicNumber); err != nil {
if len(buf) < 28 {
return
// return fmt.Errorf("gettext: %v", err)
}
var bo binary.ByteOrder
switch magicNumber {
switch binary.LittleEndian.Uint32(buf[:4]) {
case MoMagicLittleEndian:
bo = binary.LittleEndian
case MoMagicBigEndian:
bo = binary.BigEndian
default:
return
// return fmt.Errorf("gettext: %v", "invalid magic number")
}
var header struct {
@@ -187,82 +183,58 @@ func (mo *Mo) Parse(buf []byte) {
MsgIDCount uint32
MsgIDOffset uint32
MsgStrOffset uint32
HashSize uint32
HashOffset uint32
}
if err := binary.Read(r, bo, &header); err != nil {
return
// return fmt.Errorf("gettext: %v", err)
}
if v := header.MajorVersion; v != 0 && v != 1 {
return
// return fmt.Errorf("gettext: %v", "invalid version number")
}
if v := header.MinorVersion; v != 0 && v != 1 {
return
// return fmt.Errorf("gettext: %v", "invalid version number")
}
msgIDStart := make([]uint32, header.MsgIDCount)
msgIDLen := make([]uint32, header.MsgIDCount)
if _, err := r.Seek(int64(header.MsgIDOffset), 0); err != nil {
bufLen := uint64(len(buf))
header.MajorVersion = bo.Uint16(buf[4:6])
header.MinorVersion = bo.Uint16(buf[6:8])
header.MsgIDCount = bo.Uint32(buf[8:12])
header.MsgIDOffset = bo.Uint32(buf[12:16])
header.MsgStrOffset = bo.Uint32(buf[16:20])
if header.MajorVersion != 0 && header.MajorVersion != 1 {
return
// return fmt.Errorf("gettext: %v", err)
}
for i := 0; i < int(header.MsgIDCount); i++ {
if err := binary.Read(r, bo, &msgIDLen[i]); err != nil {
if header.MinorVersion != 0 && header.MinorVersion != 1 {
return
}
directorySize := uint64(header.MsgIDCount) * 8
if uint64(header.MsgIDOffset)+directorySize > bufLen ||
uint64(header.MsgStrOffset)+directorySize > bufLen {
return
}
for i := uint32(0); i < header.MsgIDCount; i++ {
msgIDEntry := int(uint64(header.MsgIDOffset) + uint64(i)*8)
msgIDLen := bo.Uint32(buf[msgIDEntry:])
msgIDStart := bo.Uint32(buf[msgIDEntry+4:])
if uint64(msgIDStart) > bufLen ||
uint64(msgIDLen) > bufLen-uint64(msgIDStart) {
return
// return fmt.Errorf("gettext: %v", err)
}
if err := binary.Read(r, bo, &msgIDStart[i]); err != nil {
msgStrEntry := int(uint64(header.MsgStrOffset) + uint64(i)*8)
msgStrLen := bo.Uint32(buf[msgStrEntry:])
msgStrStart := bo.Uint32(buf[msgStrEntry+4:])
if uint64(msgStrStart) > bufLen ||
uint64(msgStrLen) > bufLen-uint64(msgStrStart) {
return
// return fmt.Errorf("gettext: %v", err)
}
}
msgStrStart := make([]int32, header.MsgIDCount)
msgStrLen := make([]int32, header.MsgIDCount)
if _, err := r.Seek(int64(header.MsgStrOffset), 0); err != nil {
return
// return fmt.Errorf("gettext: %v", err)
}
for i := 0; i < int(header.MsgIDCount); i++ {
if err := binary.Read(r, bo, &msgStrLen[i]); err != nil {
return
// return fmt.Errorf("gettext: %v", err)
}
if err := binary.Read(r, bo, &msgStrStart[i]); err != nil {
return
// return fmt.Errorf("gettext: %v", err)
}
}
for i := uint32(0); i < header.MsgIDCount; i++ {
msgIDEntry := int(uint64(header.MsgIDOffset) + uint64(i)*8)
msgIDLen := bo.Uint32(buf[msgIDEntry:])
msgIDStart := bo.Uint32(buf[msgIDEntry+4:])
msgIDEnd := int(uint64(msgIDStart) + uint64(msgIDLen))
for i := 0; i < int(header.MsgIDCount); i++ {
if _, err := r.Seek(int64(msgIDStart[i]), 0); err != nil {
return
// return fmt.Errorf("gettext: %v", err)
}
msgIDData := make([]byte, msgIDLen[i])
if _, err := r.Read(msgIDData); err != nil {
return
// return fmt.Errorf("gettext: %v", err)
}
msgStrEntry := int(uint64(header.MsgStrOffset) + uint64(i)*8)
msgStrLen := bo.Uint32(buf[msgStrEntry:])
msgStrStart := bo.Uint32(buf[msgStrEntry+4:])
msgStrEnd := int(uint64(msgStrStart) + uint64(msgStrLen))
if _, err := r.Seek(int64(msgStrStart[i]), 0); err != nil {
return
// return fmt.Errorf("gettext: %v", err)
}
msgStrData := make([]byte, msgStrLen[i])
if _, err := r.Read(msgStrData); err != nil {
return
// return fmt.Errorf("gettext: %v", err)
}
if len(msgIDData) == 0 {
mo.addTranslation(msgIDData, msgStrData)
} else {
mo.addTranslation(msgIDData, msgStrData)
}
mo.addTranslation(buf[int(msgIDStart):msgIDEnd], buf[int(msgStrStart):msgStrEnd])
}
// Parse headers
@@ -279,36 +251,32 @@ func (mo *Mo) addTranslation(msgid, msgstr []byte) {
translation := NewTranslation()
var msgctxt []byte
var msgidPlural []byte
hasContext := false
d := bytes.Split(msgid, []byte(EotSeparator))
if len(d) == 1 {
msgid = d[0]
} else {
msgid, msgctxt = d[1], d[0]
if eotIndex := bytes.IndexByte(msgid, EotSeparator[0]); eotIndex >= 0 {
hasContext = true
msgctxt = msgid[:eotIndex]
msgid = msgid[eotIndex+len(EotSeparator):]
}
dd := bytes.Split(msgid, []byte(NulSeparator))
if len(dd) > 1 {
msgid = dd[0]
dd = dd[1:]
if nulIndex := bytes.IndexByte(msgid, NulSeparator[0]); nulIndex >= 0 {
msgidPlural = msgid[nulIndex+1:]
msgid = msgid[:nulIndex]
}
translation.ID = string(msgid)
msgidPlural = bytes.Join(dd, []byte(NulSeparator))
if len(msgidPlural) > 0 {
translation.PluralID = string(msgidPlural)
}
ddd := bytes.Split(msgstr, []byte(NulSeparator))
if len(ddd) > 0 {
for i, s := range ddd {
translation.Trs[i] = string(s)
}
i := 0
for s := range bytes.SplitSeq(msgstr, []byte(NulSeparator)) {
translation.Trs[i] = string(s)
i++
}
if len(msgctxt) > 0 {
// With context...
if hasContext {
// With context, including an explicitly empty context...
if _, ok := mo.domain.contextTranslations[string(msgctxt)]; !ok {
mo.domain.contextTranslations[string(msgctxt)] = make(map[string]*Translation)
}
+362 -178
View File
@@ -12,18 +12,12 @@ package plurals
import (
"errors"
"fmt"
"regexp"
"slices"
"strconv"
"strings"
"unicode"
)
type match struct {
openPos int
closePos int
}
var pat = regexp.MustCompile(`(\?|:|\|\||&&|==|!=|>=|>|<=|<|%|\d+|n)`)
type testToken interface {
compile(tokens []string) (test test, err error)
}
@@ -31,30 +25,30 @@ type testToken interface {
type cmpTestBuilder func(val uint32, flipped bool) test
type logicTestBuild func(left test, right test) test
var ternaryToken ternaryStruct
func compileTernary(tokens []string, depth int) (expr Expression, err error) {
if depth > maxParseDepth {
return nil, errors.New("expression nesting is too deep")
}
type ternaryStruct struct{}
func (ternaryStruct) compile(tokens []string) (expr Expression, err error) {
main, err := splitTokens(tokens, "?")
if err != nil {
return expr, err
return nil, err
}
test, err := compileTest(strings.Join(main.Left, ""))
test, err := compileTestDepth(strings.Join(main.Left, ""), depth+1)
if err != nil {
return expr, err
return nil, err
}
actions, err := splitTokens(main.Right, ":")
actions, err := splitTernaryTokens(main.Right)
if err != nil {
return expr, err
return nil, err
}
trueAction, err := compileExpression(strings.Join(actions.Left, ""))
trueAction, err := compileExpressionDepth(strings.Join(actions.Left, ""), depth+1)
if err != nil {
return expr, err
return nil, err
}
falseAction, err := compileExpression(strings.Join(actions.Right, ""))
falseAction, err := compileExpressionDepth(strings.Join(actions.Right, ""), depth+1)
if err != nil {
return expr, nil
return nil, err
}
return ternary{
test: test,
@@ -63,93 +57,145 @@ func (ternaryStruct) compile(tokens []string) (expr Expression, err error) {
}, nil
}
const maxParseDepth = 1024
func splitTernaryTokens(tokens []string) (s splitted, err error) {
depth := 0
for index, token := range tokens {
switch token {
case "?":
depth++
case ":":
if depth == 0 {
return splitted{
Left: tokens[:index],
Right: tokens[index+1:],
}, nil
}
depth--
}
}
return s, errors.New("':' not found for ternary expression")
}
var constToken constValStruct
type constValStruct struct{}
func (constValStruct) compile(tokens []string) (expr Expression, err error) {
if len(tokens) == 0 {
return expr, errors.New("got nothing instead of constant")
return nil, errors.New("got nothing instead of constant")
}
if len(tokens) != 1 {
return expr, fmt.Errorf("invalid constant: %s", strings.Join(tokens, ""))
return nil, fmt.Errorf("invalid constant: %s", strings.Join(tokens, ""))
}
i, err := strconv.Atoi(tokens[0])
if err != nil {
return expr, err
return nil, err
}
return constValue{value: i}, nil
}
func compileLogicTest(tokens []string, sep string, builder logicTestBuild) (test test, err error) {
func compileLogicTestDepth(tokens []string, sep string, builder logicTestBuild, depth int) (test test, err error) {
if depth > maxParseDepth {
return nil, errors.New("expression nesting is too deep")
}
split, err := splitTokens(tokens, sep)
if err != nil {
return test, err
return nil, err
}
left, err := compileTest(strings.Join(split.Left, ""))
left, err := compileTestDepth(strings.Join(split.Left, ""), depth+1)
if err != nil {
return test, err
return nil, err
}
right, err := compileTest(strings.Join(split.Right, ""))
right, err := compileTestDepth(strings.Join(split.Right, ""), depth+1)
if err != nil {
return test, err
return nil, err
}
return builder(left, right), nil
}
var orToken orStruct
type orStruct struct{}
func (orStruct) compile(tokens []string) (test test, err error) {
return compileLogicTest(tokens, "||", buildOr)
}
func buildOr(left test, right test) test {
return or{left: left, right: right}
}
var andToken andStruct
type andStruct struct{}
func (andStruct) compile(tokens []string) (test test, err error) {
return compileLogicTest(tokens, "&&", buildAnd)
}
func buildAnd(left test, right test) test {
return and{left: left, right: right}
}
func unwrapSingleGroup(tokens []string) ([]string, error) {
for len(tokens) == 1 && strings.HasPrefix(tokens[0], "(") {
inner, err := tokenize(tokens[0])
if err != nil {
return nil, err
}
if len(inner) == 1 && inner[0] == tokens[0] {
break
}
tokens = inner
}
return tokens, nil
}
func isSimpleN(tokens []string) bool {
tokens, err := unwrapSingleGroup(tokens)
if err != nil || len(tokens) != 1 {
return false
}
return tokens[0] == "n"
}
func parseLiteralTokens(tokens []string) (uint32, error) {
tokens, err := unwrapSingleGroup(tokens)
if err != nil {
return 0, err
}
if len(tokens) != 1 {
return 0, errors.New("expected a single integer")
}
return parseUint32(tokens[0])
}
func containsSimpleN(tokens []string) bool {
for _, token := range tokens {
if isSimpleN([]string{token}) {
return true
}
}
return false
}
func compileMod(tokens []string) (math math, err error) {
split, err := splitTokens(tokens, "%")
if err != nil {
return math, err
}
if len(split.Left) != 1 || split.Left[0] != "n" {
if !isSimpleN(split.Left) {
return math, errors.New("modulus operation requires 'n' as left operand")
}
if len(split.Right) != 1 {
return math, errors.New("modulus operation requires simple integer as right operand")
}
i, err := parseUint32(split.Right[0])
i, err := parseLiteralTokens(split.Right)
if err != nil {
return math, err
}
return mod{value: uint32(i)}, nil
if i == 0 {
return math, errors.New("modulus operation requires a non-zero integer as right operand")
}
return mod{value: i}, nil
}
func subPipe(modTokens []string, actionTokens []string, builder cmpTestBuilder, flipped bool) (test test, err error) {
modifier, err := compileMod(modTokens)
if err != nil {
return test, err
return nil, err
}
if len(actionTokens) != 1 {
return test, errors.New("can only get modulus of integer")
if len(actionTokens) == 0 {
return nil, errors.New("can only get modulus of integer")
}
i, err := parseUint32(actionTokens[0])
i, err := parseLiteralTokens(actionTokens)
if err != nil {
return test, err
return nil, err
}
action := builder(uint32(i), flipped)
action := builder(i, flipped)
return pipe{
modifier: modifier,
action: action,
@@ -159,31 +205,37 @@ func subPipe(modTokens []string, actionTokens []string, builder cmpTestBuilder,
func compileEquality(tokens []string, sep string, builder cmpTestBuilder) (test test, err error) {
split, err := splitTokens(tokens, sep)
if err != nil {
return test, err
return nil, err
}
if len(split.Left) == 1 && split.Left[0] == "n" {
if len(split.Right) != 1 {
return test, errors.New("test can only compare n to integers")
}
i, err := parseUint32(split.Right[0])
left, err := unwrapSingleGroup(split.Left)
if err != nil {
return nil, err
}
right, err := unwrapSingleGroup(split.Right)
if err != nil {
return nil, err
}
if isSimpleN(left) {
i, err := parseLiteralTokens(right)
if err != nil {
return test, err
return nil, err
}
return builder(i, false), nil
} else if len(split.Right) == 1 && split.Right[0] == "n" {
if len(split.Left) != 1 {
return test, errors.New("test can only compare n to integers")
}
i, err := parseUint32(split.Left[0])
}
if isSimpleN(right) {
i, err := parseLiteralTokens(left)
if err != nil {
return test, err
return nil, err
}
return builder(i, true), nil
} else if contains(split.Left, "n") && contains(split.Left, "%") {
return subPipe(split.Left, split.Right, builder, false)
}
return test, errors.New("equality test must have 'n' as one of the two tests")
if containsSimpleN(left) && slices.Contains(left, "%") {
return subPipe(left, right, builder, false)
}
if containsSimpleN(right) && slices.Contains(right, "%") {
return subPipe(right, left, builder, true)
}
return nil, errors.New("equality test must have 'n' as one of the two tests")
}
var eqToken eqStruct
@@ -258,8 +310,8 @@ type testTokenDef struct {
}
var precedence = []testTokenDef{
{op: "||", token: orToken},
{op: "&&", token: andToken},
{op: "||"},
{op: "&&"},
{op: "==", token: eqToken},
{op: "!=", token: neqToken},
{op: ">=", token: gteToken},
@@ -273,20 +325,10 @@ type splitted struct {
Right []string
}
// Find index of token in list of tokens
func index(tokens []string, sep string) int {
for index, token := range tokens {
if token == sep {
return index
}
}
return -1
}
// Split a list of tokens by a token into a splitted struct holding the tokens
// before and after the token to be split by.
func splitTokens(tokens []string, sep string) (s splitted, err error) {
index := index(tokens, sep)
index := slices.Index(tokens, sep)
if index == -1 {
return s, fmt.Errorf("'%s' not found in ['%s']", sep, strings.Join(tokens, "','"))
}
@@ -296,135 +338,277 @@ func splitTokens(tokens []string, sep string) (s splitted, err error) {
}, nil
}
// Scan a string for parenthesis
func scan(s string) <-chan match {
ch := make(chan match)
go func() {
depth := 0
opener := 0
for index, char := range s {
switch char {
case '(':
if depth == 0 {
opener = index
}
depth++
case ')':
depth--
if depth == 0 {
ch <- match{
openPos: opener,
closePos: index + 1,
}
}
}
func trimLexicalWhitespace(s string) string {
return strings.Map(func(r rune) rune {
if unicode.IsSpace(r) {
return -1
}
close(ch)
}()
return ch
return r
}, s)
}
// Split the string into tokens
func split(s string) <-chan string {
ch := make(chan string)
go func() {
s = strings.ReplaceAll(s, " ", "")
if !strings.Contains(s, "(") {
ch <- s
} else {
last := 0
end := len(s)
for info := range scan(s) {
if last != info.openPos {
ch <- s[last:info.openPos]
}
ch <- s[info.openPos:info.closePos]
last = info.closePos
func lexicalToken(s string, index int) (token string, next int, err error) {
if index >= len(s) {
return "", index, errors.New("unexpected end of expression")
}
switch s[index] {
case '?', ':', '%', '(', ')':
return s[index : index+1], index + 1, nil
case '|':
if index+1 >= len(s) || s[index+1] != '|' {
return "", index, errors.New("logical OR must use '||'")
}
return "||", index + 2, nil
case '&':
if index+1 >= len(s) || s[index+1] != '&' {
return "", index, errors.New("logical AND must use '&&'")
}
return "&&", index + 2, nil
case '=':
if index+1 >= len(s) || s[index+1] != '=' {
return "", index, errors.New("equality must use '=='")
}
return "==", index + 2, nil
case '!':
if index+1 >= len(s) || s[index+1] != '=' {
return "", index, errors.New("inequality must use '!='")
}
return "!=", index + 2, nil
case '>':
if index+1 < len(s) && s[index+1] == '=' {
return ">=", index + 2, nil
}
return ">", index + 1, nil
case '<':
if index+1 < len(s) && s[index+1] == '=' {
return "<=", index + 2, nil
}
return "<", index + 1, nil
case 'n':
return "n", index + 1, nil
default:
if s[index] >= '0' && s[index] <= '9' {
next = index + 1
for next < len(s) && s[next] >= '0' && s[next] <= '9' {
next++
}
if last != end {
ch <- s[last:]
return s[index:next], next, nil
}
return "", index, fmt.Errorf("invalid character %q in expression", s[index])
}
}
func validateParentheses(s string) error {
depth := 0
for index := 0; index < len(s); {
token, next, err := lexicalToken(s, index)
if err != nil {
return err
}
switch token {
case "(":
depth++
case ")":
depth--
if depth < 0 {
return errors.New("unmatched closing parenthesis")
}
}
close(ch)
}()
return ch
index = next
}
if depth != 0 {
return errors.New("unmatched opening parenthesis")
}
return nil
}
func stripRedundantOuterParens(s string) string {
if len(s) < 2 || s[0] != '(' {
return s
}
leading := 0
for leading < len(s) && s[leading] == '(' {
leading++
}
openers := make([]int, 0, leading)
outer := leading
for index := range s {
switch s[index] {
case '(':
openers = append(openers, index)
case ')':
if len(openers) == 0 {
return s
}
opener := openers[len(openers)-1]
openers = openers[:len(openers)-1]
if opener < outer && index != len(s)-1-opener {
outer = opener
}
}
}
if len(openers) != 0 {
return s
}
return s[outer : len(s)-outer]
}
// Tokenizes a string into a list of strings, tokens grouped by parenthesis are
// not split! If the string starts with ( and ends in ), those are stripped.
func tokenize(s string) []string {
/*
TODO: Properly detect if the string starts with a ( and ends with a )
and that those two form a matching pair.
func tokenize(s string) ([]string, error) {
s = trimLexicalWhitespace(s)
if s == "" {
return []string{}, nil
}
if err := validateParentheses(s); err != nil {
return nil, err
}
s = stripRedundantOuterParens(s)
if s == "" {
return []string{}, nil
}
Eg: (foo) -> true; (foo)(bar) -> false;
*/
if len(s) == 0 {
return []string{}
}
if s[0] == '(' && s[len(s)-1] == ')' {
s = s[1 : len(s)-1]
}
ret := []string{}
for chunk := range split(s) {
if len(chunk) != 0 {
if chunk[0] == '(' && chunk[len(chunk)-1] == ')' {
ret = append(ret, chunk)
} else {
for _, token := range pat.FindAllStringSubmatch(chunk, -1) {
ret = append(ret, token[0])
for index := 0; index < len(s); {
if s[index] == '(' {
start := index
depth := 0
groupEnd := false
for index < len(s) {
switch s[index] {
case '(':
depth++
case ')':
depth--
if depth == 0 {
index++
ret = append(ret, s[start:index])
groupEnd = true
}
}
if groupEnd {
break
}
index++
}
} else {
fmt.Printf("Empty chunk in string '%s'\n", s)
if depth != 0 {
return nil, errors.New("unmatched opening parenthesis")
}
continue
}
token, next, err := lexicalToken(s, index)
if err != nil {
return nil, err
}
if token == ")" {
return nil, errors.New("unmatched closing parenthesis")
}
ret = append(ret, token)
index = next
}
return ret
return ret, nil
}
// Compile a string containing a plural form expression to a Expression object.
func Compile(s string) (expr Expression, err error) {
if s == "0" {
return constValue{value: 0}, nil
s = trimLexicalWhitespace(s)
if s == "" {
return nil, errors.New("empty expression")
}
if !strings.Contains(s, "?") {
s += "?1:0"
}
return compileExpression(s)
return compileExpressionDepth(s, 0)
}
// Check if a token is in a slice of strings
func contains(haystack []string, needle string) bool {
for _, s := range haystack {
if s == needle {
return true
// Compiles an expression (ternary, numeric leaf, or test).
func compileExpressionDepth(s string, depth int) (expr Expression, err error) {
if depth > maxParseDepth {
return nil, errors.New("expression nesting is too deep")
}
tokens, err := tokenize(s)
if err != nil {
return nil, err
}
if slices.Contains(tokens, "?") {
return compileTernary(tokens, depth)
}
for _, tokenDef := range precedence {
if !slices.Contains(tokens, tokenDef.op) {
continue
}
condition, err := compileTestDepth(s, depth+1)
if err != nil {
return nil, err
}
return testValue{condition: condition}, nil
}
return false
}
// Compiles an expression (ternary or constant)
func compileExpression(s string) (expr Expression, err error) {
tokens := tokenize(s)
if contains(tokens, "?") {
return ternaryToken.compile(tokens)
if isSimpleN(tokens) {
return variableValue{}, nil
}
if slices.Contains(tokens, "%") {
modifier, err := compileMod(tokens)
if err != nil {
return nil, err
}
return mathValue{value: modifier}, nil
}
return constToken.compile(tokens)
}
// Compiles a test (comparison)
func compileTest(s string) (test test, err error) {
tokens := tokenize(s)
func compileTestDepth(s string, depth int) (test test, err error) {
if depth > maxParseDepth {
return nil, errors.New("expression nesting is too deep")
}
tokens, err := tokenize(s)
if err != nil {
return nil, err
}
for _, tokenDef := range precedence {
if contains(tokens, tokenDef.op) {
if !slices.Contains(tokens, tokenDef.op) {
continue
}
switch tokenDef.op {
case "||":
return compileLogicTestDepth(tokens, "||", buildOr, depth)
case "&&":
return compileLogicTestDepth(tokens, "&&", buildAnd, depth)
default:
return tokenDef.token.compile(tokens)
}
}
return test, errors.New("cannot compile")
if slices.Contains(tokens, "%") {
m, err := compileMod(tokens)
if err != nil {
return nil, err
}
return pipe{
modifier: m,
action: notequal{value: 0},
}, nil
}
if len(tokens) == 1 && strings.HasPrefix(tokens[0], "(") {
inner, err := tokenize(tokens[0])
if err != nil {
return nil, err
}
if len(inner) == 1 && inner[0] == tokens[0] {
return nil, errors.New("cannot compile parenthesized test")
}
return compileTestDepth(tokens[0], depth+1)
}
if len(tokens) == 1 && tokens[0] == "n" {
return notequal{value: 0}, nil
}
if value, err := parseLiteralTokens(tokens); err == nil {
return literalTest{value: value}, nil
}
return nil, errors.New("cannot compile")
}
func parseUint32(s string) (ui uint32, err error) {
s = trimLexicalWhitespace(s)
i, err := strconv.ParseUint(s, 10, 32)
if err != nil {
return ui, err
+34
View File
@@ -20,6 +20,37 @@ func (c constValue) Eval(n uint32) int {
return c.value
}
type variableValue struct{}
func (variableValue) Eval(n uint32) int {
return int(n)
}
type mathValue struct {
value math
}
func (m mathValue) Eval(n uint32) int {
if m.value == nil {
return -1
}
return int(m.value.calc(n))
}
type testValue struct {
condition test
}
func (t testValue) Eval(n uint32) int {
if t.condition == nil {
return -1
}
if t.condition.test(n) {
return 1
}
return 0
}
type test interface {
test(n uint32) bool
}
@@ -31,6 +62,9 @@ type ternary struct {
}
func (t ternary) Eval(n uint32) int {
if t.test == nil {
return -1
}
if t.test.test(n) {
if t.trueExpr == nil {
return -1
+3
View File
@@ -15,5 +15,8 @@ type mod struct {
}
func (m mod) calc(n uint32) uint32 {
if m.value == 0 {
return 0
}
return n % m.value
}
+17
View File
@@ -22,6 +22,14 @@ func (e notequal) test(n uint32) bool {
return n != e.value
}
type literalTest struct {
value uint32
}
func (l literalTest) test(n uint32) bool {
return l.value != 0
}
type gt struct {
value uint32
flipped bool
@@ -77,6 +85,9 @@ type and struct {
}
func (e and) test(n uint32) bool {
if e.left == nil || e.right == nil {
return false
}
if !e.left.test(n) {
return false
}
@@ -89,6 +100,9 @@ type or struct {
}
func (e or) test(n uint32) bool {
if e.left == nil || e.right == nil {
return false
}
if e.left.test(n) {
return true
}
@@ -101,5 +115,8 @@ type pipe struct {
}
func (e pipe) test(n uint32) bool {
if e.modifier == nil || e.action == nil {
return false
}
return e.action.test(e.modifier.calc(n))
}
+305 -78
View File
@@ -42,6 +42,10 @@ type Po struct {
domain *Domain
fs fs.FS
parseBufferHasID bool
parseBufferHasPlural bool
parseBufferHasContext bool
}
type parseState int
@@ -52,6 +56,7 @@ const (
msgID
msgIDPlural
msgStr
msgStrInvalid
)
// NewPo should always be used to instantiate a new Po object
@@ -94,7 +99,7 @@ func (po *Po) GetRefs(str string) []string {
// SetPluralResolver sets the plural resolver function
func (po *Po) SetPluralResolver(f func(int) int) {
po.domain.customPluralResolver = f
po.domain.SetPluralResolver(f)
}
// Set translation
@@ -103,12 +108,12 @@ func (po *Po) Set(id, str string) {
}
// Get translation
func (po *Po) Get(str string, vars ...interface{}) string {
func (po *Po) Get(str string, vars ...any) string {
return po.domain.Get(str, vars...)
}
// Append translation
func (po *Po) Append(b []byte, str string, vars ...interface{}) []byte {
func (po *Po) Append(b []byte, str string, vars ...any) []byte {
return po.domain.Append(b, str, vars...)
}
@@ -118,12 +123,12 @@ func (po *Po) SetN(id, plural string, n int, str string) {
}
// GetN gets the plural translation
func (po *Po) GetN(str, plural string, n int, vars ...interface{}) string {
func (po *Po) GetN(str, plural string, n int, vars ...any) string {
return po.domain.GetN(str, plural, n, vars...)
}
// AppendN appends the plural translation
func (po *Po) AppendN(b []byte, str, plural string, n int, vars ...interface{}) []byte {
func (po *Po) AppendN(b []byte, str, plural string, n int, vars ...any) []byte {
return po.domain.AppendN(b, str, plural, n, vars...)
}
@@ -133,12 +138,12 @@ func (po *Po) SetC(id, ctx, str string) {
}
// GetC gets the translation for a given context
func (po *Po) GetC(str, ctx string, vars ...interface{}) string {
func (po *Po) GetC(str, ctx string, vars ...any) string {
return po.domain.GetC(str, ctx, vars...)
}
// AppendC appends the translation for a given context
func (po *Po) AppendC(b []byte, str, ctx string, vars ...interface{}) []byte {
func (po *Po) AppendC(b []byte, str, ctx string, vars ...any) []byte {
return po.domain.AppendC(b, str, ctx, vars...)
}
@@ -148,12 +153,12 @@ func (po *Po) SetNC(id, plural, ctx string, n int, str string) {
}
// GetNC gets the plural translation for a given context
func (po *Po) GetNC(str, plural string, n int, ctx string, vars ...interface{}) string {
func (po *Po) GetNC(str, plural string, n int, ctx string, vars ...any) string {
return po.domain.GetNC(str, plural, n, ctx, vars...)
}
// AppendNC appends the plural translation for a given context
func (po *Po) AppendNC(b []byte, str, plural string, n int, ctx string, vars ...interface{}) []byte {
func (po *Po) AppendNC(b []byte, str, plural string, n int, ctx string, vars ...any) []byte {
return po.domain.AppendNC(b, str, plural, n, ctx, vars...)
}
@@ -214,16 +219,17 @@ func (po *Po) Parse(buf []byte) {
defer po.domain.trMutex.Unlock()
defer po.domain.pluralMutex.Unlock()
// Get lines
lines := strings.Split(string(buf), "\n")
// Init buffer
po.domain.trBuffer = NewTranslation()
po.domain.ctxBuffer = ""
po.domain.refBuffer = ""
po.parseBufferHasID = false
po.parseBufferHasPlural = false
po.parseBufferHasContext = false
state := head
for _, l := range lines {
activeMsgStrIndex := 0
for l := range strings.Lines(string(buf)) {
// Trim spaces
l = strings.TrimSpace(l)
@@ -233,44 +239,71 @@ func (po *Po) Parse(buf []byte) {
continue
}
// Buffer context and continue
if strings.HasPrefix(l, "msgctxt") {
po.parseContext(l)
state = msgCtxt
// Multi line strings and headers
if strings.HasPrefix(l, "\"") {
if !po.parseString(l, state, activeMsgStrIndex) && state != head {
state = msgStrInvalid
}
continue
}
// Buffer msgid and continue
if strings.HasPrefix(l, "msgid") && !strings.HasPrefix(l, "msgid_plural") {
po.parseID(l)
state = msgID
// Buffer context and continue
if po.hasKeywordBoundary(l, "msgctxt") {
if po.parseContext(l) {
state = msgCtxt
} else {
state = msgStrInvalid
}
continue
}
// Check for plural form
if strings.HasPrefix(l, "msgid_plural") {
po.parsePluralID(l)
po.domain.pluralTranslations[po.domain.trBuffer.PluralID] = po.domain.trBuffer
state = msgIDPlural
if po.hasKeywordBoundary(l, "msgid_plural") {
if state != msgID || !po.parsePluralID(l) {
state = msgStrInvalid
} else {
state = msgIDPlural
}
continue
}
// Buffer msgid and continue
if po.hasKeywordBoundary(l, "msgid") {
if po.parseID(l) {
state = msgID
} else {
state = msgStrInvalid
}
continue
}
// Save Translation
if strings.HasPrefix(l, "msgstr") {
po.parseMessage(l)
state = msgStr
continue
}
if po.hasKeywordBoundary(l, "msgstr") {
if state != msgID && state != msgIDPlural && state != msgStr {
activeMsgStrIndex = 0
state = msgStrInvalid
continue
}
// Multi line strings and headers
if strings.HasPrefix(l, "\"") && strings.HasSuffix(l, "\"") {
po.parseString(l, state)
var ok bool
activeMsgStrIndex, ok = po.parseMessage(l)
if ok {
state = msgStr
} else {
activeMsgStrIndex = 0
state = msgStrInvalid
}
continue
}
}
// Save last Translation buffer.
po.saveBuffer()
// Save last Translation buffer, but do not synthesize an entry after an
// invalid directive. Preserve the historical empty-input entry.
if po.parseBufferHasID {
po.saveBuffer()
} else if len(buf) == 0 {
po.domain.translations[""] = po.domain.trBuffer
}
// Parse headers
po.domain.parseHeaders()
@@ -285,22 +318,30 @@ func (po *Po) Parse(buf []byte) {
// saveBuffer takes the context and Translation buffers
// and saves it on the translations collection
func (po *Po) saveBuffer() {
// With no context...
if po.domain.ctxBuffer == "" {
po.domain.translations[po.domain.trBuffer.ID] = po.domain.trBuffer
} else {
// With context...
if !po.parseBufferHasID {
return
}
// Store a completed plural ID after all of its continuation lines have
// been decoded.
if po.parseBufferHasPlural {
po.domain.pluralTranslations[po.domain.trBuffer.PluralID] = po.domain.trBuffer
}
if po.parseBufferHasContext {
if _, ok := po.domain.contextTranslations[po.domain.ctxBuffer]; !ok {
po.domain.contextTranslations[po.domain.ctxBuffer] = make(map[string]*Translation)
}
po.domain.contextTranslations[po.domain.ctxBuffer][po.domain.trBuffer.ID] = po.domain.trBuffer
// Cleanup current context buffer if needed
if po.domain.trBuffer.ID != "" {
po.domain.ctxBuffer = ""
}
// Context applies only to the record it prefixes, even when its ID is empty.
po.domain.ctxBuffer = ""
po.parseBufferHasContext = false
} else {
po.domain.translations[po.domain.trBuffer.ID] = po.domain.trBuffer
}
po.parseBufferHasID = false
po.parseBufferHasPlural = false
// Flush Translation buffer
if po.domain.refBuffer == "" {
po.domain.trBuffer = NewTranslation()
@@ -328,68 +369,106 @@ func (po *Po) parseComment(l string, state parseState) {
// parseContext takes a line starting with "msgctxt",
// saves the current Translation buffer and creates a new context.
func (po *Po) parseContext(l string) {
func (po *Po) parseContext(l string) bool {
value, ok := po.parseQuotedDirective(l, "msgctxt")
if !ok {
return false
}
// Save current Translation buffer.
po.saveBuffer()
// Buffer context
po.domain.ctxBuffer, _ = strconv.Unquote(strings.TrimSpace(strings.TrimPrefix(l, "msgctxt")))
// Buffer context, including an explicitly empty context.
po.domain.ctxBuffer = value
po.parseBufferHasContext = true
return true
}
// parseID takes a line starting with "msgid",
// saves the current Translation and creates a new msgid buffer.
func (po *Po) parseID(l string) {
func (po *Po) parseID(l string) bool {
value, ok := po.parseQuotedDirective(l, "msgid")
if !ok {
return false
}
// Save current Translation buffer.
po.saveBuffer()
// Set id
po.domain.trBuffer.ID, _ = strconv.Unquote(strings.TrimSpace(strings.TrimPrefix(l, "msgid")))
po.domain.trBuffer.ID = value
po.parseBufferHasID = true
return true
}
// parsePluralID saves the plural id buffer from a line starting with "msgid_plural"
func (po *Po) parsePluralID(l string) {
po.domain.trBuffer.PluralID, _ = strconv.Unquote(strings.TrimSpace(strings.TrimPrefix(l, "msgid_plural")))
func (po *Po) parsePluralID(l string) bool {
value, ok := po.parseQuotedDirective(l, "msgid_plural")
if !ok {
return false
}
po.domain.trBuffer.PluralID = value
po.parseBufferHasPlural = true
return true
}
// parseMessage takes a line starting with "msgstr" and saves it into the current buffer.
func (po *Po) parseMessage(l string) {
l = strings.TrimSpace(strings.TrimPrefix(l, "msgstr"))
func (po *Po) parseMessage(l string) (int, bool) {
if !po.hasKeywordBoundary(l, "msgstr") {
return 0, false
}
l = strings.TrimSpace(l[len("msgstr"):])
// Check for indexed Translation forms
if strings.HasPrefix(l, "[") {
idx := strings.Index(l, "]")
if idx == -1 {
idx := strings.IndexByte(l, ']')
if idx <= 1 || !isASCIIPluralIndex(l[1:idx]) {
// Skip wrong index formatting
return
return 0, false
}
// Parse index
i, err := strconv.Atoi(l[1:idx])
if err != nil {
// Skip wrong index formatting
return
return 0, false
}
// Parse Translation string
po.domain.trBuffer.Trs[i], _ = strconv.Unquote(strings.TrimSpace(l[idx+1:]))
clean, ok := decodePOQuotedString(strings.TrimSpace(l[idx+1:]))
if !ok {
return 0, false
}
po.domain.trBuffer.Trs[i] = clean
// Loop
return
return i, true
}
// Save single Translation form under 0 index
po.domain.trBuffer.Trs[0], _ = strconv.Unquote(l)
clean, ok := decodePOQuotedString(l)
if !ok {
return 0, false
}
po.domain.trBuffer.Trs[0] = clean
return 0, true
}
// parseString takes a well formatted string without prefix
// and creates headers or attach multi-line strings when corresponding
func (po *Po) parseString(l string, state parseState) {
clean, _ := strconv.Unquote(l)
func (po *Po) parseString(l string, state parseState, activeMsgStrIndex int) bool {
if state == msgStrInvalid {
return false
}
clean, ok := decodePOQuotedString(l)
if !ok {
return false
}
switch state {
case msgStr:
// Append to last Translation found
po.domain.trBuffer.Trs[len(po.domain.trBuffer.Trs)-1] += clean
// Append to active Translation form
po.domain.trBuffer.Trs[activeMsgStrIndex] += clean
case msgID:
// Multiline msgid - Append to current id
@@ -402,26 +481,174 @@ func (po *Po) parseString(l string, state parseState) {
case msgCtxt:
// Multiline context - Append to current context
po.domain.ctxBuffer += clean
}
return true
}
// isValidLine checks for line prefixes to detect valid syntax.
func (po *Po) isValidLine(l string) bool {
// Check prefix
valid := []string{
"\"",
"msgctxt",
"msgid",
"msgid_plural",
"msgstr",
if strings.HasPrefix(l, "\"") {
return true
}
for _, v := range valid {
if strings.HasPrefix(l, v) {
for _, keyword := range []string{
"msgctxt",
"msgid_plural",
"msgid",
"msgstr",
} {
if po.hasKeywordBoundary(l, keyword) {
return true
}
}
return false
}
func (po *Po) hasKeywordBoundary(l, keyword string) bool {
if !strings.HasPrefix(l, keyword) {
return false
}
if len(l) == len(keyword) {
return true
}
next := l[len(keyword)]
if keyword == "msgstr" && next == '[' {
return true
}
return next == ' ' || next == '\t'
}
func (po *Po) parseQuotedDirective(l, keyword string) (string, bool) {
if !po.hasKeywordBoundary(l, keyword) {
return "", false
}
value, ok := decodePOQuotedString(strings.TrimSpace(l[len(keyword):]))
if !ok {
return "", false
}
return value, true
}
// decodePOQuotedString decodes a GNU PO double-quoted string.
func decodePOQuotedString(source string) (string, bool) {
if len(source) < 2 || source[0] != '"' {
return "", false
}
var decoded []byte
for i := 1; i < len(source); {
switch source[i] {
case '"':
if i != len(source)-1 {
return "", false
}
if decoded == nil {
return source[1:i], true
}
return string(decoded), true
case '\\':
if decoded == nil {
decoded = make([]byte, 0, len(source)-2)
decoded = append(decoded, source[1:i]...)
}
i++
if i >= len(source) {
return "", false
}
switch source[i] {
case 'a':
decoded = append(decoded, '\a')
i++
case 'b':
decoded = append(decoded, '\b')
i++
case 'f':
decoded = append(decoded, '\f')
i++
case 'n':
decoded = append(decoded, '\n')
i++
case 'r':
decoded = append(decoded, '\r')
i++
case 't':
decoded = append(decoded, '\t')
i++
case 'v':
decoded = append(decoded, '\v')
i++
case '\\':
decoded = append(decoded, '\\')
i++
case '"':
decoded = append(decoded, '"')
i++
case '0', '1', '2', '3', '4', '5', '6', '7':
var value byte
for digits := 0; i < len(source) && digits < 3; digits++ {
if source[i] < '0' || source[i] > '7' {
break
}
value = value<<3 | (source[i] - '0')
i++
}
decoded = append(decoded, value)
case 'x':
i++
if i >= len(source) || !isPOHexDigit(source[i]) {
return "", false
}
var value byte
for i < len(source) && isPOHexDigit(source[i]) {
value = value<<4 | poHexValue(source[i])
i++
}
decoded = append(decoded, value)
default:
return "", false
}
default:
if decoded != nil {
decoded = append(decoded, source[i])
}
i++
}
}
return "", false
}
func isPOHexDigit(c byte) bool {
return c >= '0' && c <= '9' ||
c >= 'a' && c <= 'f' ||
c >= 'A' && c <= 'F'
}
func poHexValue(c byte) byte {
switch {
case c >= '0' && c <= '9':
return c - '0'
case c >= 'a' && c <= 'f':
return c - 'a' + 10
default:
return c - 'A' + 10
}
}
func isASCIIPluralIndex(index string) bool {
if index == "" {
return false
}
for i := 0; i < len(index); i++ {
if index[i] < '0' || index[i] > '9' {
return false
}
}
return true
}
+6 -14
View File
@@ -49,11 +49,8 @@ func (t *Translation) Set(str string) {
// Get returns the string of the translation
func (t *Translation) Get() string {
// Look for Translation index 0
if _, ok := t.Trs[0]; ok {
if t.Trs[0] != "" {
return t.Trs[0]
}
if tr := t.Trs[0]; tr != "" {
return tr
}
// Return untranslated id by default
@@ -68,11 +65,8 @@ func (t *Translation) SetN(n int, str string) {
// GetN returns the string of the plural translation
func (t *Translation) GetN(n int) string {
// Look for Translation index
if _, ok := t.Trs[n]; ok {
if t.Trs[n] != "" {
return t.Trs[n]
}
if tr := t.Trs[n]; tr != "" {
return tr
}
// Return untranslated singular if corresponding
@@ -86,12 +80,10 @@ func (t *Translation) GetN(n int) string {
// IsTranslated reports whether a string is translated
func (t *Translation) IsTranslated() bool {
tr, ok := t.Trs[0]
return tr != "" && ok
return t.Trs[0] != ""
}
// IsTranslatedN reports whether a plural string is translated
func (t *Translation) IsTranslatedN(n int) bool {
tr, ok := t.Trs[n]
return tr != "" && ok
return t.Trs[n] != ""
}
+60 -14
View File
@@ -9,18 +9,20 @@ import (
"errors"
"io/fs"
"os"
"github.com/leonelquinteros/gotext/plurals"
)
// Translator interface is used by Locale and Po objects.Translator
// Translator interface is used by Locale and Po objects.
// It contains all methods needed to parse translation sources and obtain corresponding translations.
// Also implements gob.GobEncoder/gob.DobDecoder interfaces to allow serialization of Locale objects.
// It includes encoding.BinaryMarshaler and encoding.BinaryUnmarshaler methods for Locale serialization.
type Translator interface {
ParseFile(f string)
Parse(buf []byte)
Get(str string, vars ...interface{}) string
GetN(str, plural string, n int, vars ...interface{}) string
GetC(str, ctx string, vars ...interface{}) string
GetNC(str, plural string, n int, ctx string, vars ...interface{}) string
Get(str string, vars ...any) string
GetN(str, plural string, n int, vars ...any) string
GetC(str, ctx string, vars ...any) string
GetNC(str, plural string, n int, ctx string, vars ...any) string
MarshalBinary() ([]byte, error)
UnmarshalBinary([]byte) error
@@ -31,10 +33,10 @@ type Translator interface {
// It contains all methods needed to parse translation sources and to append entries to the object.
type AppendTranslator interface {
Translator
Append(b []byte, str string, vars ...interface{}) []byte
AppendN(b []byte, str, plural string, n int, vars ...interface{}) []byte
AppendC(b []byte, str, ctx string, vars ...interface{}) []byte
AppendNC(b []byte, str, plural string, n int, ctx string, vars ...interface{}) []byte
Append(b []byte, str string, vars ...any) []byte
AppendN(b []byte, str, plural string, n int, vars ...any) []byte
AppendC(b []byte, str, ctx string, vars ...any) []byte
AppendNC(b []byte, str, plural string, n int, ctx string, vars ...any) []byte
}
// TranslatorEncoding is used as intermediary storage to encode Translator objects to Gob.
@@ -63,14 +65,58 @@ type TranslatorEncoding struct {
// deserialize into a Po-compatible object.
func (te *TranslatorEncoding) GetTranslator() Translator {
po := NewPo()
po.domain = NewDomain()
po.domain.Headers = te.Headers
if te == nil {
return po
}
headers := te.Headers
if headers == nil {
headers = make(HeaderMap)
}
translations := te.Translations
if translations == nil {
translations = make(map[string]*Translation)
}
for id, translation := range translations {
if translation == nil {
translation = NewTranslation()
translation.ID = id
translations[id] = translation
}
}
contexts := te.Contexts
if contexts == nil {
contexts = make(map[string]map[string]*Translation)
}
for context, translationsForContext := range contexts {
if translationsForContext == nil {
contexts[context] = make(map[string]*Translation)
continue
}
for id, translation := range translationsForContext {
if translation == nil {
translation = NewTranslation()
translation.ID = id
translationsForContext[id] = translation
}
}
}
po.domain.Headers = headers
po.domain.Language = te.Language
po.domain.PluralForms = te.PluralForms
po.domain.nplurals = te.Nplurals
po.domain.plural = te.Plural
po.domain.translations = te.Translations
po.domain.contextTranslations = te.Contexts
po.domain.translations = translations
po.domain.contextTranslations = contexts
if expr, err := plurals.Compile(te.Plural); err == nil {
po.domain.pluralforms = expr
}
po.Headers = po.domain.Headers
po.Language = po.domain.Language
po.PluralForms = po.domain.PluralForms
return po
}
+1 -1
View File
@@ -1,4 +1,4 @@
FROM golang:1.27@sha256:e0174e51e81218523251d85d248a90d24c3d5e81543b4f07a5d66229397db190
FROM golang:1.27@sha256:820153096fda4de6ac1b7194d12e416b7dd8f4e9a33d17e0345b111b945162d8
ENV GOOS=linux
ENV GOARCH=arm
+1 -1
View File
@@ -1,4 +1,4 @@
FROM golang:1.27@sha256:e0174e51e81218523251d85d248a90d24c3d5e81543b4f07a5d66229397db190
FROM golang:1.27@sha256:820153096fda4de6ac1b7194d12e416b7dd8f4e9a33d17e0345b111b945162d8
ENV GOOS=linux
ENV GOARCH=arm64
+1 -1
View File
@@ -1,4 +1,4 @@
# This is a renovate-friendly source of Docker images.
FROM python:3.13.6-slim-bullseye@sha256:e98b521460ee75bca92175c16247bdf7275637a8faaeb2bcfa19d879ae5c4b9a AS python
FROM otel/weaver:v0.21.2@sha256:2401de985c38bdb98b43918e2f43aa36b2afed4aa5669ac1c1de0a17301cd36d AS weaver
FROM otel/weaver:v0.28.0@sha256:5857bb81d8cc7ed608cdcae37c4b459eb020f21dd4acade35f7933cd6762fbcc AS weaver
FROM avtodev/markdown-lint:v1@sha256:6aeedc2f49138ce7a1cd0adffc1b1c0321b841dc2102408967d9301c031949ee AS markdown
+5
View File
@@ -61,7 +61,11 @@ var X86 struct {
HasBMI2 bool // Bit manipulation instruction set 2
HasCX16 bool // Compare and exchange 16 Bytes
HasERMS bool // Enhanced REP for MOVSB and STOSB
HasF16C bool // 16-bit floating-point conversion instructions
HasFMA bool // Fused-multiply-add instructions
HasLAHF bool // LAHF/SAHF available in 64-bit mode
HasLZCNT bool // Leading zeros count instruction
HasMOVBE bool // Move data after byte swap
HasOSXSAVE bool // OS supports XSAVE/XRESTOR for saving/restoring XMM registers.
HasPCLMULQDQ bool // PCLMULQDQ instruction - most often used for AES-GCM
HasPOPCNT bool // Hamming weight instruction POPCNT.
@@ -75,6 +79,7 @@ var X86 struct {
HasAVXIFMA bool // Advanced vector extension Integer Fused Multiply Add
HasAVXVNNI bool // Advanced vector extension Vector Neural Network Instructions
HasAVXVNNIInt8 bool // Advanced vector extension Vector Neural Network Int8 instructions
HasXSAVE bool // XSAVE/XRSTOR/XSETBV/XGETBV instructions (hardware support; see also HasOSXSAVE)
_ CacheLinePad
}
+23
View File
@@ -43,7 +43,11 @@ func initOptions() {
{Name: "bmi2", Feature: &X86.HasBMI2},
{Name: "cx16", Feature: &X86.HasCX16},
{Name: "erms", Feature: &X86.HasERMS},
{Name: "f16c", Feature: &X86.HasF16C},
{Name: "fma", Feature: &X86.HasFMA},
{Name: "lahf", Feature: &X86.HasLAHF},
{Name: "lzcnt", Feature: &X86.HasLZCNT},
{Name: "movbe", Feature: &X86.HasMOVBE},
{Name: "osxsave", Feature: &X86.HasOSXSAVE},
{Name: "pclmulqdq", Feature: &X86.HasPCLMULQDQ},
{Name: "popcnt", Feature: &X86.HasPOPCNT},
@@ -56,6 +60,7 @@ func initOptions() {
{Name: "avxifma", Feature: &X86.HasAVXIFMA},
{Name: "avxvnni", Feature: &X86.HasAVXVNNI},
{Name: "avxvnniint8", Feature: &X86.HasAVXVNNIInt8},
{Name: "xsave", Feature: &X86.HasXSAVE},
// These capabilities should always be enabled on amd64:
{Name: "sse2", Feature: &X86.HasSSE2, Required: runtime.GOARCH == "amd64"},
@@ -83,10 +88,13 @@ func archInit() {
cpuid_AVX512VPOPCNTDQ = 1 << 14
cpuid_SSE41 = 1 << 19
cpuid_SSE42 = 1 << 20
cpuid_MOVBE = 1 << 22
cpuid_POPCNT = 1 << 23
cpuid_AES = 1 << 25
cpuid_XSAVE = 1 << 26
cpuid_OSXSAVE = 1 << 27
cpuid_AVX = 1 << 28
cpuid_F16C = 1 << 29
// "Extended Feature Flag" bits returned in EBX for CPUID EAX=0x7 ECX=0x0
cpuid_BMI1 = 1 << 3
@@ -110,6 +118,9 @@ func archInit() {
// edx bits
cpuid_FSRM = 1 << 4
// ecx bits for CPUID 0x80000001
cpuid_LAHF = 1 << 0
cpuid_LZCNT = 1 << 5
// edx bits for CPUID 0x80000001
cpuid_RDTSCP = 1 << 27
)
@@ -158,9 +169,21 @@ func archInit() {
X86.HasSSE42 = isSet(ecx1, cpuid_SSE42)
X86.HasPOPCNT = isSet(ecx1, cpuid_POPCNT)
X86.HasAES = isSet(ecx1, cpuid_AES)
X86.HasXSAVE = isSet(ecx1, cpuid_XSAVE)
X86.HasOSXSAVE = isSet(ecx1, cpuid_OSXSAVE)
X86.HasF16C = isSet(ecx1, cpuid_F16C)
X86.HasMOVBE = isSet(ecx1, cpuid_MOVBE)
X86.HasRDRAND = isSet(ecx1, cpuid_RDRAND)
// Extended processor info and feature bits, under the separate extended
// (0x8000_0000-prefixed) leaf hierarchy, so maxID (the basic leaf count)
// doesn't gate it.
if maxExtID, _, _, _ := cpuid(0x80000000, 0); maxExtID >= 0x80000001 {
_, _, ecxExt1, _ := cpuid(0x80000001, 0)
X86.HasLAHF = isSet(ecxExt1, cpuid_LAHF)
X86.HasLZCNT = isSet(ecxExt1, cpuid_LZCNT)
}
var osSupportsAVX, osSupportsAVX512 bool
// For XGETBV, OSXSAVE bit is required and sufficient.
if X86.HasOSXSAVE {
+20
View File
@@ -13,6 +13,16 @@
// Just jump to package syscall's implementation for all these functions.
// The runtime may know about them.
TEXT ·Syscall(SB),NOSPLIT,$0-56
MOVD $syscall·Syscall(SB), R31
MOVD R31, CTR
JMP (CTR)
TEXT ·Syscall6(SB),NOSPLIT,$0-80
MOVD $syscall·Syscall6(SB), R31
MOVD R31, CTR
JMP (CTR)
TEXT ·SyscallNoError(SB),NOSPLIT,$0-48
BL runtime·entersyscall(SB)
MOVD a1+8(FP), R3
@@ -28,6 +38,16 @@ TEXT ·SyscallNoError(SB),NOSPLIT,$0-48
BL runtime·exitsyscall(SB)
RET
TEXT ·RawSyscall(SB),NOSPLIT,$0-56
MOVD $syscall·RawSyscall(SB), R31
MOVD R31, CTR
JMP (CTR)
TEXT ·RawSyscall6(SB),NOSPLIT,$0-80
MOVD $syscall·RawSyscall6(SB), R31
MOVD R31, CTR
JMP (CTR)
TEXT ·RawSyscallNoError(SB),NOSPLIT,$0-48
MOVD a1+8(FP), R3
MOVD a2+16(FP), R4
+48
View File
@@ -0,0 +1,48 @@
// Copyright 2026 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build linux && sparc64 && gc
#include "textflag.h"
//
// System calls for sparc64, Linux
//
// Just jump to package syscall's implementation for all these functions.
// The runtime may know about them.
TEXT ·Syscall(SB), NOSPLIT, $0-56
JMP syscall·Syscall(SB)
TEXT ·Syscall6(SB), NOSPLIT, $0-80
JMP syscall·Syscall6(SB)
TEXT ·SyscallNoError(SB), NOSPLIT, $0-48
CALL runtime·entersyscall(SB)
MOVD trap+0(FP), R1 // syscall entry
MOVD a1+8(FP), R8
MOVD a2+16(FP), R9
MOVD a3+24(FP), R10
TA $0x6d
MOVD R8, r1+32(FP)
MOVD R9, r2+40(FP)
CALL runtime·exitsyscall(SB)
RET
TEXT ·RawSyscall(SB), NOSPLIT, $0-56
JMP syscall·RawSyscall(SB)
TEXT ·RawSyscall6(SB), NOSPLIT, $0-80
JMP syscall·RawSyscall6(SB)
TEXT ·RawSyscallNoError(SB), NOSPLIT, $0-48
MOVD trap+0(FP), R1 // syscall entry
MOVD a1+8(FP), R8
MOVD a2+16(FP), R9
MOVD a3+24(FP), R10
TA $0x6d
MOVD R8, r1+32(FP)
MOVD R9, r2+40(FP)
RET
+4 -186
View File
@@ -30,21 +30,8 @@
#define SVC_LOAD BYTE $0x0A; BYTE $0x08 // SVC 08 LOAD
#define SVC_DELETE BYTE $0x0A; BYTE $0x09 // SVC 09 DELETE
DATA zosLibVec<>(SB)/8, $0
GLOBL zosLibVec<>(SB), NOPTR, $8
TEXT ·initZosLibVec(SB), NOSPLIT|NOFRAME, $0-0
MOVW PSALAA, R8
MOVD LCA64(R8), R8
MOVD CAA(R8), R8
MOVD EDCHPXV(R8), R8
MOVD R8, zosLibVec<>(SB)
RET
TEXT ·GetZosLibVec(SB), NOSPLIT|NOFRAME, $0-0
MOVD zosLibVec<>(SB), R8
MOVD R8, ret+0(FP)
RET
JMP runtime·GetZosLibVec(SB)
TEXT ·clearErrno(SB), NOSPLIT, $0-0
BL addrerrno<>(SB)
@@ -160,163 +147,13 @@ TEXT ·gettid(SB), NOSPLIT, $0
// errno and errno2 is retrieved
//
TEXT ·CallLeFuncWithErr(SB), NOSPLIT, $0
MOVW PSALAA, R8
MOVD LCA64(R8), R8
MOVD CAA(R8), R9
MOVD g, GOCB(R9)
// Restore LE stack.
MOVD SAVSTACK_ASYNC(R8), R9 // R9-> LE stack frame saving address
MOVD 0(R9), R4 // R4-> restore previously saved stack frame pointer
MOVD parms_base+8(FP), R7 // R7 -> argument array
MOVD parms_len+16(FP), R8 // R8 number of arguments
// arg 1 ---> R1
CMP R8, $0
BEQ docall
SUB $1, R8
MOVD 0(R7), R1
// arg 2 ---> R2
CMP R8, $0
BEQ docall
SUB $1, R8
ADD $8, R7
MOVD 0(R7), R2
// arg 3 --> R3
CMP R8, $0
BEQ docall
SUB $1, R8
ADD $8, R7
MOVD 0(R7), R3
CMP R8, $0
BEQ docall
MOVD $2176+16, R6 // starting LE stack address-8 to store 4th argument
repeat:
ADD $8, R7
MOVD 0(R7), R0 // advance arg pointer by 8 byte
ADD $8, R6 // advance LE argument address by 8 byte
MOVD R0, (R4)(R6*1) // copy argument from go-slice to le-frame
SUB $1, R8
CMP R8, $0
BNE repeat
docall:
MOVD funcdesc+0(FP), R8 // R8-> function descriptor
LMG 0(R8), R5, R6
MOVD $0, 0(R9) // R9 address of SAVSTACK_ASYNC
LE_CALL // balr R7, R6 (return #1)
NOPH
MOVD R3, ret+32(FP)
CMP R3, $-1 // compare result to -1
BNE done
// retrieve errno and errno2
MOVD zosLibVec<>(SB), R8
ADD $(__errno), R8
LMG 0(R8), R5, R6
LE_CALL // balr R7, R6 __errno (return #3)
NOPH
MOVWZ 0(R3), R3
MOVD R3, err+48(FP)
MOVD zosLibVec<>(SB), R8
ADD $(__err2ad), R8
LMG 0(R8), R5, R6
LE_CALL // balr R7, R6 __err2ad (return #2)
NOPH
MOVW (R3), R2 // retrieve errno2
MOVD R2, errno2+40(FP) // store in return area
done:
MOVD R4, 0(R9) // Save stack pointer.
RET
JMP runtime·CallLeFuncWithErr(SB)
//
// Call LE function, if the return is 0
// errno and errno2 is retrieved
//
TEXT ·CallLeFuncWithPtrReturn(SB), NOSPLIT, $0
MOVW PSALAA, R8
MOVD LCA64(R8), R8
MOVD CAA(R8), R9
MOVD g, GOCB(R9)
// Restore LE stack.
MOVD SAVSTACK_ASYNC(R8), R9 // R9-> LE stack frame saving address
MOVD 0(R9), R4 // R4-> restore previously saved stack frame pointer
MOVD parms_base+8(FP), R7 // R7 -> argument array
MOVD parms_len+16(FP), R8 // R8 number of arguments
// arg 1 ---> R1
CMP R8, $0
BEQ docall
SUB $1, R8
MOVD 0(R7), R1
// arg 2 ---> R2
CMP R8, $0
BEQ docall
SUB $1, R8
ADD $8, R7
MOVD 0(R7), R2
// arg 3 --> R3
CMP R8, $0
BEQ docall
SUB $1, R8
ADD $8, R7
MOVD 0(R7), R3
CMP R8, $0
BEQ docall
MOVD $2176+16, R6 // starting LE stack address-8 to store 4th argument
repeat:
ADD $8, R7
MOVD 0(R7), R0 // advance arg pointer by 8 byte
ADD $8, R6 // advance LE argument address by 8 byte
MOVD R0, (R4)(R6*1) // copy argument from go-slice to le-frame
SUB $1, R8
CMP R8, $0
BNE repeat
docall:
MOVD funcdesc+0(FP), R8 // R8-> function descriptor
LMG 0(R8), R5, R6
MOVD $0, 0(R9) // R9 address of SAVSTACK_ASYNC
LE_CALL // balr R7, R6 (return #1)
NOPH
MOVD R3, ret+32(FP)
CMP R3, $0 // compare result to 0
BNE done
// retrieve errno and errno2
MOVD zosLibVec<>(SB), R8
ADD $(__errno), R8
LMG 0(R8), R5, R6
LE_CALL // balr R7, R6 __errno (return #3)
NOPH
MOVWZ 0(R3), R3
MOVD R3, err+48(FP)
MOVD zosLibVec<>(SB), R8
ADD $(__err2ad), R8
LMG 0(R8), R5, R6
LE_CALL // balr R7, R6 __err2ad (return #2)
NOPH
MOVW (R3), R2 // retrieve errno2
MOVD R2, errno2+40(FP) // store in return area
XOR R2, R2
MOVWZ R2, (R3) // clear errno2
done:
MOVD R4, 0(R9) // Save stack pointer.
RET
JMP runtime·CallLeFuncWithPtrReturn(SB)
//
// function to test if a pointer can be safely dereferenced (content read)
// return 0 for succces
@@ -360,23 +197,4 @@ TEXT ·ptrtest(SB), NOSPLIT, $0-16
//
// func safeload(ptr uintptr) ( value uintptr, error uintptr)
TEXT ·safeload(SB), NOSPLIT, $0-24
MOVD ptr+0(FP), R10 // test pointer in R10
MOVD $0x0, R6
BYTE $0xE3; BYTE $0x20; BYTE $0x04; BYTE $0xB8; BYTE $0x00; BYTE $0x17 // llgt 2,1208
BYTE $0xB9; BYTE $0x17; BYTE $0x00; BYTE $0x22 // llgtr 2,2
BYTE $0xA5; BYTE $0x26; BYTE $0x7F; BYTE $0xFF // nilh 2,32767
BYTE $0xE3; BYTE $0x22; BYTE $0x00; BYTE $0x58; BYTE $0x00; BYTE $0x04 // lg 2,88(2)
BYTE $0xE3; BYTE $0x22; BYTE $0x00; BYTE $0x08; BYTE $0x00; BYTE $0x04 // lg 2,8(2)
BYTE $0x41; BYTE $0x22; BYTE $0x03; BYTE $0x68 // la 2,872(2)
BYTE $0xB9; BYTE $0x82; BYTE $0x00; BYTE $0x33 // xgr 3,3
BYTE $0xA7; BYTE $0x55; BYTE $0x00; BYTE $0x04 // bras 5,lbl1
BYTE $0xA7; BYTE $0x39; BYTE $0x00; BYTE $0x01 // lghi 3,1
BYTE $0xB9; BYTE $0x02; BYTE $0x00; BYTE $0x33 // lbl1 ltgr 3,3
BYTE $0xA7; BYTE $0x74; BYTE $0x00; BYTE $0x08 // brc b'0111',lbl2
BYTE $0xE3; BYTE $0x52; BYTE $0x00; BYTE $0x00; BYTE $0x00; BYTE $0x24 // stg 5,0(2)
BYTE $0xE3; BYTE $0x6A; BYTE $0x00; BYTE $0x00; BYTE $0x00; BYTE $0x04 // lg 6,0(10)
BYTE $0xB9; BYTE $0x82; BYTE $0x00; BYTE $0x99 // lbl2 xgr 9,9
BYTE $0xE3; BYTE $0x92; BYTE $0x00; BYTE $0x00; BYTE $0x00; BYTE $0x24 // stg 9,0(2)
MOVD R6, value+8(FP) // result in R6
MOVD R3, error+16(FP) // error in R3
RET
JMP runtime·ZosSafeLoad(SB)
+9 -9
View File
@@ -20,15 +20,15 @@ TEXT ·bpxcall(SB), NOSPLIT|NOFRAME, $0
MOVD plist_base+0(FP), R1 // r1 points to plist
MOVD bpx_offset+24(FP), R2 // r2 offset to BPX vector table
MOVD R14, R7 // save r14
MOVD R15, R8 // save r15
MOVWZ 16(R0), R9
MOVWZ 544(R9), R9
MOVWZ 24(R9), R9 // call vector in r9
ADD R2, R9 // add offset to vector table
MOVWZ (R9), R9 // r9 points to entry point
BYTE $0x0D // BL R14,R9 --> basr r14,r9
BYTE $0xE9 // clobbers 0,1,14,15
MOVD R8, R15 // restore 15
MOVD R15, R9 // save r15
MOVWZ 16(R0), R15
MOVWZ 544(R15), R15
MOVWZ 24(R15), R15 // call vector in r15
ADD R2, R15 // add offset to vector table
MOVWZ (R15), R15 // r15 points to entry point
BYTE $0x0D // BL R14,R15 --> basr r14,r15
BYTE $0xEF // clobbers 0,1,14,15
MOVD R9, R15 // restore 15
JMP R7 // return via saved return address
// func A2e(arr [] byte)
+2 -2
View File
@@ -13,12 +13,12 @@ func FcntlInt(fd uintptr, cmd, arg int) (int, error) {
// FcntlFlock performs a fcntl syscall for the F_GETLK, F_SETLK or F_SETLKW command.
func FcntlFlock(fd uintptr, cmd int, lk *Flock_t) error {
_, err := fcntl(int(fd), cmd, int(uintptr(unsafe.Pointer(lk))))
_, err := fcntlPtr(int(fd), cmd, unsafe.Pointer(lk))
return err
}
// FcntlFstore performs a fcntl syscall for the F_PREALLOCATE command.
func FcntlFstore(fd uintptr, cmd int, fstore *Fstore_t) error {
_, err := fcntl(int(fd), cmd, int(uintptr(unsafe.Pointer(fstore))))
_, err := fcntlPtr(int(fd), cmd, unsafe.Pointer(fstore))
return err
}
+1 -1
View File
@@ -579,7 +579,7 @@ ccflags="$@"
$2 ~ /^DIOC/ ||
$2 ~ /^RUSAGE_(SELF|CHILDREN|THREAD)/ ||
$2 ~ /^RLIMIT_(AS|CORE|CPU|DATA|FSIZE|LOCKS|MEMLOCK|MSGQUEUE|NICE|NOFILE|NPROC|RSS|RTPRIO|RTTIME|SIGPENDING|STACK)|RLIM_INFINITY/ ||
$2 ~ /^PRIO_(PROCESS|PGRP|USER)/ ||
$2 ~ /^PRIO_(PROCESS|PGRP|USER|DARWIN_(THREAD|PROCESS|BG|NONUI))/ ||
$2 ~ /^CLONE_[A-Z_]+/ ||
$2 !~ /^(BPF_TIMEVAL|BPF_FIB_LOOKUP_[A-Z]+|BPF_F_LINK)$/ &&
$2 ~ /^(BPF|DLT)_/ ||
+537
View File
@@ -0,0 +1,537 @@
// Copyright 2026 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build zos
package unix
import "unsafe"
// z/OS ptrace implementation notes:
//
// This file provides ptrace wrappers for z/OS that match the Linux API as closely
// as possible. However, there are fundamental differences in how z/OS implements
// process tracing compared to Linux:
//
// 1. SYSTEM CALL INTERFACE:
// - z/OS uses BPX4PTR (5 parameters) vs Linux ptrace syscall (4 parameters)
// - The 5th parameter (buffer) is used for block operations
//
// 2. MEMORY OPERATIONS:
// - z/OS: PT_READ_BLOCK/PT_WRITE_BLOCK for efficient block transfers
// - Linux: PTRACE_PEEKTEXT/POKETEXT for word-by-word access
//
// 3. REGISTER OPERATIONS:
// - z/OS: PT_READ_GPR/PT_WRITE_GPR with register number for 64-bit values
// - Linux: PTRACE_GETREGSET/SETREGSET with iovec structure
//
// 4. SINGLE-STEPPING:
// - z/OS: No native single-step; requires temporary breakpoints or PER
// - Linux: PTRACE_SINGLESTEP natively supported
// a) Read instruction at current PSW
// b) Calculate next instruction address (handle branches)
// c) Set temporary breakpoint (SVC 144 = 0x0A90) at next address
// d) Continue execution until breakpoint hit
// e) Restore original instruction and adjust PSW
//
// 5. SYSCALL TRACING:
// - z/OS: No PTRACE_SYSCALL equivalent; requires breakpoint-based approach
// - Linux: PTRACE_SYSCALL stops at syscall entry/exit
// a) Identify BPX syscall vector location
// b) Set breakpoints at syscall entry points
// c) Detect BASR instructions calling into syscall vector
// d) Parse arguments from registers before/after syscall
//
// 6. LIMITATIONS:
// - PtraceSetOptions: No equivalent (returns ENOSYS)
// - PtraceGetEventMsg: No equivalent (returns ENOSYS)
// - PtraceSingleStep: Requires complex implementation (returns ENOSYS)
// - PtraceInterrupt: No equivalent (returns ENOSYS)
// - PtracePokeUser: No PT_WRITE_U (returns ENOSYS)
//
// For reference implementations of single-stepping and syscall tracing on z/OS,
// ptrace is the basic wrapper for BPX4PTR on z/OS.
// Note: z/OS ptrace (BPX4PTR) takes 5 parameters vs 4 on Linux/Darwin.
// The 5th parameter (buffer) is used for operations like PT_READ_BLOCK/PT_WRITE_BLOCK.
//
// data is an integer-valued argument (signal number, register index, byte count,
// or a target-process address). It is not a Go-managed pointer. We must not pass
// it to Bpx4ptr as unsafe.Pointer(data) because the garbage collector would treat
// that integer as a pointer and may crash trying to follow it.
//
// Instead we inline the parms array that Bpx4ptr would construct, placing &data
// at parms[3]. BPX4PTR dereferences parms[3] to obtain the data value, so it
// receives data unchanged while the GC only ever sees a valid stack pointer.
func ptrace(request int, pid int, addr uintptr, data uintptr) (err error) {
req32 := int32(request)
pid32 := int32(pid)
var rv, rc, rn int32
var buffer unsafe.Pointer
parms := [8]unsafe.Pointer{
unsafe.Pointer(&req32),
unsafe.Pointer(&pid32),
unsafe.Pointer(&addr),
unsafe.Pointer(&data), // &data, not unsafe.Pointer(data)
unsafe.Pointer(&buffer),
unsafe.Pointer(&rv),
unsafe.Pointer(&rc),
unsafe.Pointer(&rn),
}
bpxcall(parms[:], BPX4PTR)
if rv != 0 {
err = errnoErr2(Errno(rc), uintptr(rn))
}
return
}
// ptracePtr is a variant that accepts unsafe.Pointer for the data parameter.
// addr is a target-process address (a raw integer), not a Go-managed pointer.
// We must not pass it to Bpx4ptr as unsafe.Pointer(addr); instead we inline the
// parms array and place &addr at parms[2], exactly as ptrace() does for data.
func ptracePtr(request int, pid int, addr uintptr, data unsafe.Pointer) (err error) {
req32 := int32(request)
pid32 := int32(pid)
var rv, rc, rn int32
var buffer unsafe.Pointer
parms := [8]unsafe.Pointer{
unsafe.Pointer(&req32),
unsafe.Pointer(&pid32),
unsafe.Pointer(&addr), // &addr, not unsafe.Pointer(addr)
unsafe.Pointer(&data),
unsafe.Pointer(&buffer),
unsafe.Pointer(&rv),
unsafe.Pointer(&rc),
unsafe.Pointer(&rn),
}
bpxcall(parms[:], BPX4PTR)
if rv != 0 {
err = errnoErr2(Errno(rc), uintptr(rn))
}
return
}
// ptracePtrWithBuffer is used for operations that require the buffer parameter.
// addr is a target-process address; same &addr treatment as ptracePtr.
func ptracePtrWithBuffer(request int, pid int, addr uintptr, data unsafe.Pointer, buffer unsafe.Pointer) (err error) {
req32 := int32(request)
pid32 := int32(pid)
var rv, rc, rn int32
parms := [8]unsafe.Pointer{
unsafe.Pointer(&req32),
unsafe.Pointer(&pid32),
unsafe.Pointer(&addr), // &addr, not unsafe.Pointer(addr)
unsafe.Pointer(&data),
unsafe.Pointer(&buffer),
unsafe.Pointer(&rv),
unsafe.Pointer(&rc),
unsafe.Pointer(&rn),
}
bpxcall(parms[:], BPX4PTR)
if rv != 0 {
err = errnoErr2(Errno(rc), uintptr(rn))
}
return
}
// High-level convenience functions matching the Linux/Darwin API
func PtraceAttach(pid int) (err error) {
return ptrace(PT_ATTACH, pid, 0, 0)
}
func PtraceDetach(pid int) (err error) {
return ptrace(PT_DETACH, pid, 0, 0)
}
func PtraceCont(pid int, signal int) (err error) {
// PT_CONTINUE can be interrupted by signals (EINTR), retry if needed
for {
err = ptrace(PT_CONTINUE, pid, 0, uintptr(signal))
if err != EINTR {
break
}
}
return err
}
func PtraceKill(pid int) (err error) {
return ptrace(PT_KILL, pid, 0, 0)
}
// ptracePeek implements the peek operations using PT_READ_BLOCK.
// On z/OS, PT_READ_BLOCK can read arbitrary-length data efficiently.
// Unlike Linux which reads word-by-word, z/OS can read blocks directly.
func ptracePeek(req int, pid int, addr uintptr, out []byte) (count int, err error) {
if len(out) == 0 {
return 0, nil
}
// z/OS PT_READ_BLOCK can read blocks directly without word-by-word alignment
// The buffer parameter points to the output buffer
buffer := unsafe.Pointer(&out[0])
err = ptracePtrWithBuffer(req, pid, addr, unsafe.Pointer(uintptr(len(out))), buffer)
if err != nil {
return 0, err
}
return len(out), nil
}
// PtracePeekText reads from the traced process's text segment.
func PtracePeekText(pid int, addr uintptr, out []byte) (count int, err error) {
return ptracePeek(PT_READ_BLOCK, pid, addr, out)
}
// PtracePeekData reads from the traced process's data segment.
func PtracePeekData(pid int, addr uintptr, out []byte) (count int, err error) {
return ptracePeek(PT_READ_BLOCK, pid, addr, out)
}
// PtracePeekUser reads from the traced process's user area.
// On z/OS, PT_READ_U reads a 32-bit word from the user area at the specified offset.
// To match Linux behavior, we implement word-by-word reading for arbitrary lengths.
func PtracePeekUser(pid int, addr uintptr, out []byte) (count int, err error) {
if len(out) == 0 {
return 0, nil
}
// PT_READ_U reads 32-bit words from user area
// We need to read word-by-word to handle arbitrary lengths
n := 0
for n < len(out) {
var data uint32
err = ptracePtr(PT_READ_U, pid, addr+uintptr(n), unsafe.Pointer(&data))
if err != nil {
return n, err
}
// Copy up to 4 bytes to output
remaining := len(out) - n
if remaining >= 4 {
out[n] = byte(data >> 24)
out[n+1] = byte(data >> 16)
out[n+2] = byte(data >> 8)
out[n+3] = byte(data)
n += 4
} else {
// Handle partial word at the end
for i := 0; i < remaining; i++ {
out[n+i] = byte(data >> (24 - i*8))
}
n += remaining
}
}
return n, nil
}
// ptracePoke implements the poke operations using PT_WRITE_BLOCK.
func ptracePoke(pokeReq int, peekReq int, pid int, addr uintptr, data []byte) (count int, err error) {
if len(data) == 0 {
return 0, nil
}
// z/OS PT_WRITE_BLOCK can write blocks directly
buffer := unsafe.Pointer(&data[0])
err = ptracePtrWithBuffer(pokeReq, pid, addr, unsafe.Pointer(uintptr(len(data))), buffer)
if err != nil {
return 0, err
}
return len(data), nil
}
// PtracePokeText writes to the traced process's text segment.
func PtracePokeText(pid int, addr uintptr, data []byte) (count int, err error) {
return ptracePoke(PT_WRITE_BLOCK, PT_READ_BLOCK, pid, addr, data)
}
// PtracePokeData writes to the traced process's data segment.
func PtracePokeData(pid int, addr uintptr, data []byte) (count int, err error) {
return ptracePoke(PT_WRITE_BLOCK, PT_READ_BLOCK, pid, addr, data)
}
// PtracePokeUser writes to the traced process's user area.
// Note: z/OS does not have a direct PT_WRITE_U equivalent.
// This function returns ENOSYS to indicate it's not supported.
// DIFFERENCE FROM LINUX: Linux supports PTRACE_POKEUSR, z/OS does not.
func PtracePokeUser(pid int, addr uintptr, data []byte) (count int, err error) {
// z/OS doesn't have PT_WRITE_U
return 0, ENOSYS
}
// PtraceGetRegs retrieves the general purpose registers from the traced process.
// On z/OS, PT_READ_GPR can read individual registers as 64-bit values when called
// with a register number, or read all registers when called with a buffer.
// This function reads each register individually to ensure full 64-bit values.
// DIFFERENCE FROM LINUX: Linux uses PTRACE_GETREGSET with iovec, z/OS uses PT_READ_GPR.
func PtraceGetRegs(pid int, regsout *PtraceRegs) (err error) {
// Use PT_REGHSET to enable reading high GPRs for AMODE 64 programs
rv, rc, rn := Bpx4ptr(int32(PT_REGHSET), int32(pid), nil, nil, nil)
_ = rv
_ = rc
_ = rn
// Use PT_BLOCKREQ to read GPRs and PSW in one call
// dbx uses 3 requests: PT_READ_GPR, PT_READ_U, PT_READ_GPRH
// PT_READ_U is required for ptrace to populate PSW in GPR block
//
// IMPORTANT: z/OS ptrace always returns 64-bit ESA/390 format PSW in the Psw field.
// The 128-bit Pswg field is never populated, even with PSWG_Req bit set.
// We convert ESA/390 to z/Architecture format in software (matching DBX behavior).
const (
numRequests = 3
reqSize = int(unsafe.Sizeof(PtraceBlkReqReq{}))
gprSize = int(unsafe.Sizeof(PtraceBlkGpr{}))
uareaSize = int(unsafe.Sizeof(PtraceBlkUar{})) + 6*int(unsafe.Sizeof(PtraceBlkUarOcw{}))
totalSize = int(unsafe.Sizeof(PtraceBlkReq{})) + numRequests*reqSize + gprSize*2 + uareaSize
)
// Allocate buffer for block request
buf := make([]byte, totalSize)
// Setup block request header
blkReq := (*PtraceBlkReq)(unsafe.Pointer(&buf[0]))
blkReq.Numreq = numRequests
// Setup request array
reqOffset := int(unsafe.Sizeof(PtraceBlkReq{}))
reqs := (*[3]PtraceBlkReqReq)(unsafe.Pointer(&buf[reqOffset]))
// Setup GPR block (lower 32 bits)
gprOffset := reqOffset + numRequests*reqSize
gprBlock := (*PtraceBlkGpr)(unsafe.Pointer(&buf[gprOffset]))
// Setup user area block (required for PSW)
uareaOffset := gprOffset + gprSize
uareaBlock := (*PtraceBlkUar)(unsafe.Pointer(&buf[uareaOffset]))
uareaBlock.Num = 6 // Number of control info entries
// Setup user area offset/control words
uareaOcw := (*[6]PtraceBlkUarOcw)(unsafe.Pointer(&buf[uareaOffset+int(unsafe.Sizeof(PtraceBlkUar{}))]))
uareaOcw[0].Ofs = 1025 // program interrupt code
uareaOcw[1].Ofs = 1026 // abend completion code
uareaOcw[2].Ofs = 1027 // abend reason code
uareaOcw[3].Ofs = 1028 // signal code
uareaOcw[4].Ofs = 1029 // instruction length code
uareaOcw[5].Ofs = 1030 // process flags
// Setup high GPR block (upper 32 bits)
gprHighOffset := uareaOffset + uareaSize
gprHighBlock := (*PtraceBlkGpr)(unsafe.Pointer(&buf[gprHighOffset]))
// Configure requests (order matters: GPR, U, GPRH)
// reqdata contains OFFSET from buffer start, not absolute address
// dbx: brr[0].reqdata=(unsigned long)brg; where brg is the offset
reqs[0].Reqtype = PT_READ_GPR
reqs[0].Reqdata = uint32(gprOffset)
reqs[1].Reqtype = PT_READ_U
reqs[1].Reqdata = uint32(uareaOffset)
reqs[2].Reqtype = PT_READ_GPRH
reqs[2].Reqdata = uint32(gprHighOffset)
// Execute block request
rvBlk, rcBlk, rnBlk := Bpx4ptr(int32(PT_BLOCKREQ), int32(pid),
unsafe.Pointer(&buf[0]), unsafe.Pointer(uintptr(totalSize)), nil)
if rvBlk == -1 {
return errnoErr2(Errno(rcBlk), uintptr(rnBlk))
}
// Extract GPRs (combine low and high 32 bits)
for i := 0; i < 16; i++ {
low := uint64(gprBlock.Gpr[i])
high := uint64(gprHighBlock.Gpr[i]) << 32
regsout.Gprs[i] = high | low
}
// Extract PSW from GPR block structure
// z/OS ptrace ALWAYS returns 8-byte ESA/390 format PSW at offset 144.
// The 16-byte PSWG field at offset 152 is NEVER populated (always zero).
// We must convert ESA/390 format to z/Architecture format in software (like DBX does).
//
// ESA/390 PSW format (8 bytes):
// Word 1 (bytes 0-3): Mask with bit 12 = 1 (ECMODE31BIT)
// Word 2 (bytes 4-7): Address with bit 0 = AMODE bit (1=31-bit, 0=64-bit)
//
// z/Architecture PSW format (16 bytes):
// Mask (bytes 0-7): Extended mask with bit 12 = 0, bits 31-32 = EA+BA (AMODE)
// Addr (bytes 8-15): Full 64-bit instruction address
// Read ESA/390 PSW as two 32-bit words
pswWord1 := uint32(gprBlock.Psw[0])<<24 | uint32(gprBlock.Psw[1])<<16 |
uint32(gprBlock.Psw[2])<<8 | uint32(gprBlock.Psw[3])
pswWord2 := uint32(gprBlock.Psw[4])<<24 | uint32(gprBlock.Psw[5])<<16 |
uint32(gprBlock.Psw[6])<<8 | uint32(gprBlock.Psw[7])
// Convert ESA/390 to z/Architecture format (matching DBX psw_ESA390_to_zArchitecture)
const ECMODE31BIT = uint64(0x0000000000080000)
const AMODE31BIT = uint64(0x0000000080000000)
const ADDR_MASK_31 = uint64(0x000000007FFFFFFF)
// Extract AMODE bit from address word (bit 0 of word 2)
amode31 := uint64(pswWord2) & AMODE31BIT
// Mask off AMODE bit from address
addr := uint64(pswWord2) & ADDR_MASK_31
// Convert mask: clear ECMODE31BIT, shift left 32 bits, add AMODE bit
mask := uint64(pswWord1) & ^ECMODE31BIT
mask = (mask << 32) | amode31
regsout.Psw.Mask = mask
regsout.Psw.Addr = addr
// Note: Access registers (Acrs), floating point registers (Fp_regs),
// PER info (Per_info), and other fields would require additional
// PT_READ_* calls. For now, we focus on GPRs and PSW which are
// the most commonly used registers for debugging.
return nil
}
// ptraceWritePSW writes only the PSW address register.
// This is a lower-level function used internally for single-stepping.
// Based on ztrace's write_psw function.
func ptraceWritePSW(pid int, pswAddr uint32) error {
const PTRACE_REG_PSWA = 41
rv, rc, rn := Bpx4ptr(int32(PT_WRITE_GPR), int32(pid),
unsafe.Pointer(uintptr(PTRACE_REG_PSWA)),
unsafe.Pointer(uintptr(pswAddr)),
nil)
if rv == -1 {
return errnoErr2(Errno(rc), uintptr(rn))
}
return nil
}
// PtraceSetRegs sets the general purpose registers in the traced process.
// Uses PT_BLOCKREQ to write GPRs and PSW in a single call, matching dbx implementation.
// DIFFERENCE FROM LINUX: Linux uses PTRACE_SETREGSET with iovec, z/OS uses PT_BLOCKREQ.
// NOTE: The process must be in a stopped state for register writes to succeed.
func PtraceSetRegs(pid int, regs *PtraceRegs) (err error) {
// Use PT_REGHSET to enable writing high GPRs for AMODE 64 programs
rv, rc, rn := Bpx4ptr(int32(PT_REGHSET), int32(pid), nil, nil, nil)
_ = rv
_ = rc
_ = rn
// Use PT_BLOCKREQ to write GPRs and PSW in one call
const (
numRequests = 2
reqSize = int(unsafe.Sizeof(PtraceBlkReqReq{}))
gprSize = int(unsafe.Sizeof(PtraceBlkGpr{}))
totalSize = int(unsafe.Sizeof(PtraceBlkReq{})) + numRequests*reqSize + gprSize*2
)
// Allocate buffer for block request
buf := make([]byte, totalSize)
// Setup block request header
blkReq := (*PtraceBlkReq)(unsafe.Pointer(&buf[0]))
blkReq.Numreq = numRequests
// Setup request array
reqOffset := int(unsafe.Sizeof(PtraceBlkReq{}))
reqs := (*[2]PtraceBlkReqReq)(unsafe.Pointer(&buf[reqOffset]))
// Setup GPR block (lower 32 bits)
gprOffset := reqOffset + numRequests*reqSize
gprBlock := (*PtraceBlkGpr)(unsafe.Pointer(&buf[gprOffset]))
// Setup high GPR block (upper 32 bits)
gprHighOffset := gprOffset + gprSize
gprHighBlock := (*PtraceBlkGpr)(unsafe.Pointer(&buf[gprHighOffset]))
// Mark all GPRs as modified (set all 16 bits)
gprBlock.Writebitflags = 0xFFFF
gprHighBlock.Writebitflags = 0xFFFF
// Split 64-bit GPRs into low and high 32 bits
for i := 0; i < 16; i++ {
gprBlock.Gpr[i] = uint32(regs.Gprs[i] & 0xFFFFFFFF)
gprHighBlock.Gpr[i] = uint32(regs.Gprs[i] >> 32)
}
// Pack PSW for 31-bit targets (current z/OS)
// Upper 32 bits = mask, lower 32 bits = address
gprBlock.Wpsw = 1 // Mark PSW as modified
// Write PSW in old format (8 bytes at offset 144)
// Word 1 (mask) at bytes 0-3, Word 2 (addr) at bytes 4-7
pswMask := uint32(regs.Psw.Mask)
pswAddr := uint32(regs.Psw.Addr)
gprBlock.Psw[0] = byte(pswMask >> 24)
gprBlock.Psw[1] = byte(pswMask >> 16)
gprBlock.Psw[2] = byte(pswMask >> 8)
gprBlock.Psw[3] = byte(pswMask)
gprBlock.Psw[4] = byte(pswAddr >> 24)
gprBlock.Psw[5] = byte(pswAddr >> 16)
gprBlock.Psw[6] = byte(pswAddr >> 8)
gprBlock.Psw[7] = byte(pswAddr)
// Clear extended PSWG (not used for 31-bit targets)
for i := 0; i < 16; i++ {
gprBlock.Pswg[i] = 0
}
// Configure requests
// reqdata contains OFFSET from buffer start, not absolute address
reqs[0].Reqtype = PT_WRITE_GPR
reqs[0].Reqdata = uint32(gprOffset)
reqs[1].Reqtype = PT_WRITE_GPRH
reqs[1].Reqdata = uint32(gprHighOffset)
// Execute block request
rvBlk, rcBlk, rnBlk := Bpx4ptr(int32(PT_BLOCKREQ), int32(pid),
unsafe.Pointer(&buf[0]), unsafe.Pointer(uintptr(totalSize)), nil)
if rvBlk == -1 {
return errnoErr2(Errno(rcBlk), uintptr(rnBlk))
}
return nil
}
// PtraceSetOptions sets ptrace options.
// DIFFERENCE FROM LINUX: z/OS doesn't have PTRACE_SETOPTIONS equivalent.
// Returns ENOSYS to indicate this is not supported on z/OS.
func PtraceSetOptions(pid int, options int) (err error) {
return ENOSYS
}
// PtraceGetEventMsg retrieves a message about the ptrace event.
// DIFFERENCE FROM LINUX: z/OS doesn't have PTRACE_GETEVENTMSG equivalent.
// Returns ENOSYS to indicate this is not supported on z/OS.
func PtraceGetEventMsg(pid int) (msg uint, err error) {
return 0, ENOSYS
}
// PtraceSyscall continues execution and stops at the next syscall entry/exit.
// DIFFERENCE FROM LINUX: z/OS doesn't have PTRACE_SYSCALL equivalent.
// This function falls back to PT_CONTINUE and will NOT stop at syscalls.
//
// To trace syscalls on z/OS, applications must:
// 1. Set breakpoints at BPX syscall entry points (requires knowledge of syscall vector)
// 2. Detect BASR instructions that call into the BPX syscall vector
// 3. Use PT_LDINFO to identify loaded modules and their entry points
func PtraceSyscall(pid int, signal int) (err error) {
// z/OS doesn't have a direct PTRACE_SYSCALL equivalent
// Use PT_CONTINUE as a fallback (won't stop at syscalls)
return ptrace(PT_CONTINUE, pid, 0, uintptr(signal))
}
// Note: PtraceSingleStep is implemented in ptrace_zos_singlestep.go
// with full two-breakpoint emulation for branch instructions.
// PtraceInterrupt interrupts the traced process.
// DIFFERENCE FROM LINUX: z/OS doesn't have PTRACE_INTERRUPT equivalent.
// Returns ENOSYS to indicate this is not supported on z/OS.
func PtraceInterrupt(pid int) (err error) {
return ENOSYS
}
// PtraceSeize attaches to a process without stopping it.
// DIFFERENCE FROM LINUX: z/OS doesn't have PTRACE_SEIZE equivalent.
// Uses PT_ATTACH as a fallback, which will stop the process.
func PtraceSeize(pid int) (err error) {
// z/OS doesn't have PTRACE_SEIZE, use PT_ATTACH
// Note: This WILL stop the process, unlike Linux PTRACE_SEIZE
return ptrace(PT_ATTACH, pid, 0, 0)
}
+425
View File
@@ -0,0 +1,425 @@
// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build zos
package unix
import (
"sync"
)
// Single-stepping emulation for z/OS
//
// z/OS does not have native single-step support like Linux's PTRACE_SINGLESTEP.
// Instead, single-stepping must be emulated using temporary breakpoints.
//
// This file provides a complete implementation of single-step emulation using
// temporary breakpoints at both the sequential next instruction and branch target.
const (
// NO_BRANCH_DEST indicates the instruction is not a branch or cannot branch
NO_BRANCH_DEST = 0xFFFFFFFF
// SVC_144 is the breakpoint instruction (SVC 144 = 0x0A90)
SVC_144 = 0x0A90
// Maximum number of temporary breakpoints (one for next PC, one for branch dest)
MAX_TEMP_BREAKPOINTS = 2
// S/390 branch instruction opcodes
// RR format (Register-Register)
opBCR = 0x07 // Branch on Condition Register
opBALR = 0x05 // Branch and Link Register
opBASR = 0x0D // Branch and Save Register
opBASSM = 0x0C // Branch and Save and Set Mode
opBSM = 0x0B // Branch and Set Mode
opBCTR = 0x06 // Branch on Count Register
// RX format (Register-Index-Storage)
opBC = 0x47 // Branch on Condition
opBAL = 0x45 // Branch and Link
opBCT = 0x46 // Branch on Count
opBAS = 0x4D // Branch and Save
// RS format (Register-Storage)
opBXH = 0x86 // Branch on Index High
opBXLE = 0x87 // Branch on Index Low or Equal
// Extended branch opcodes
opBA7 = 0xA7 // A7x series (BRC, BRAS, BRCT, BRCTG)
opBC0 = 0xC0 // C0x series (BRASL, BRCL)
opBEC = 0xEC // ECxx series (RIEc format: compare-and-branch)
)
// tempBreakpoint represents a temporary breakpoint for single-stepping
type tempBreakpoint struct {
address uint64
originalInsn [8]byte // Save up to 8 bytes (full instruction)
active bool
}
// singleStepState manages temporary breakpoints for a process
type singleStepState struct {
mu sync.Mutex
breakpoints [MAX_TEMP_BREAKPOINTS]tempBreakpoint
}
var (
// Global map of process states (pid -> state)
processStates = make(map[int]*singleStepState)
processStatesMu sync.Mutex
)
// getProcessState returns or creates the single-step state for a process
func getProcessState(pid int) *singleStepState {
processStatesMu.Lock()
defer processStatesMu.Unlock()
state, exists := processStates[pid]
if !exists {
state = &singleStepState{}
processStates[pid] = state
}
return state
}
// cleanupProcessState removes the state for a process (call on detach)
func cleanupProcessState(pid int) {
processStatesMu.Lock()
defer processStatesMu.Unlock()
delete(processStates, pid)
}
// GetInstructionLength returns the length of an S/390 instruction in bytes.
func GetInstructionLength(firstByte byte) int {
switch (firstByte >> 6) & 0x03 {
case 0x00:
return 2
case 0x01, 0x02:
return 4
case 0x03:
return 6
default:
return 2
}
}
// GetNextInstructionAddr returns the address of the next sequential instruction.
func GetNextInstructionAddr(pc uint64, instruction []byte) uint64 {
if len(instruction) < 1 {
return pc + 2
}
insnLen := GetInstructionLength(instruction[0])
return pc + uint64(insnLen)
}
// CalculateBranchDest calculates the destination address for branch instructions.
func CalculateBranchDest(pid int, pc uint64, instruction []byte) uint64 {
if len(instruction) < 6 {
return NO_BRANCH_DEST
}
inst0_15 := uint16(instruction[0])<<8 | uint16(instruction[1])
inst16_31 := uint16(instruction[2])<<8 | uint16(instruction[3])
inst32_47 := uint16(instruction[4])<<8 | uint16(instruction[5])
opcode := instruction[0]
var dest uint64 = NO_BRANCH_DEST
// RR format branches
if opcode == opBCR || opcode == opBALR || opcode == opBASR ||
opcode == opBASSM || opcode == opBSM || opcode == opBCTR {
r2 := int(inst0_15 & 0x0F)
if opcode == opBCR {
mask := int((inst0_15 >> 4) & 0x0F)
if mask == 0 || r2 == 0 {
return NO_BRANCH_DEST
}
} else {
if r2 == 0 {
return NO_BRANCH_DEST
}
}
var regs PtraceRegs
if err := PtraceGetRegs(pid, &regs); err == nil {
if r2 >= 0 && r2 < 16 {
regVal := regs.Gprs[r2]
dest = regVal
}
}
} else if opcode == opBC || opcode == opBAL || opcode == opBCT || opcode == opBAS {
// RX format branches
if opcode == opBC {
mask := int((inst0_15 >> 4) & 0x0F)
if mask == 0 {
return NO_BRANCH_DEST
}
}
index := int(inst0_15 & 0x0F)
base := int((inst16_31 >> 12) & 0x0F)
displacement := uint32(inst16_31 & 0x0FFF)
var baseVal, indexVal uint64
var regs PtraceRegs
if err := PtraceGetRegs(pid, &regs); err == nil {
if base != 0 && base < 16 {
baseVal = regs.Gprs[base]
}
if index != 0 && index < 16 {
indexVal = regs.Gprs[index]
}
}
if base == 0 && index == 0 {
dest = pc + uint64(displacement)
} else {
dest = baseVal + indexVal + uint64(displacement)
}
} else if opcode == opBXH || opcode == opBXLE {
// RS format branches
base := int((inst16_31 >> 12) & 0x0F)
displacement := uint32(inst16_31 & 0x0FFF)
var baseVal uint64
var regs PtraceRegs
if err := PtraceGetRegs(pid, &regs); err == nil {
if base != 0 && base < 16 {
baseVal = regs.Gprs[base]
}
}
dest = baseVal + uint64(displacement)
} else if opcode == opBA7 {
// RI format branches
subOp := int(inst0_15 & 0x0F)
if subOp < 4 || subOp > 7 {
return NO_BRANCH_DEST
}
if subOp == 0x04 {
mask := int((inst0_15 >> 4) & 0x0F)
if mask == 0 {
return NO_BRANCH_DEST
}
}
relDispl := int16(inst16_31)
dest = uint64(int64(pc) + int64(relDispl)*2)
} else if opcode == opBC0 {
// RIL format branches
subOp := int(inst0_15 & 0x0F)
if subOp == 0x04 || subOp == 0x05 {
if subOp == 0x04 {
mask := int((inst0_15 >> 4) & 0x0F)
if mask == 0 {
return NO_BRANCH_DEST
}
}
inst16_47 := (uint32(inst16_31) << 16) | uint32(inst32_47)
relDispl4 := int32(inst16_47)
dest = uint64(int64(pc) + int64(relDispl4)*2)
}
} else if opcode == opBEC {
// RIEc format compare-and-branch
subOp := instruction[4]
isCompareBranch := (subOp >= 0x64 && subOp <= 0x67) ||
(subOp >= 0x76 && subOp <= 0x77) ||
(subOp >= 0x7C && subOp <= 0x7F)
if isCompareBranch {
ri4 := int16(inst16_31)
dest = uint64(int64(pc) + int64(ri4)*2)
}
}
if dest < 0x2000 {
return NO_BRANCH_DEST
}
return dest
}
// setTempBreakpoint sets a temporary breakpoint at the specified address
func (s *singleStepState) setTempBreakpoint(pid int, addr uint64) error {
s.mu.Lock()
defer s.mu.Unlock()
// Find free slot
slot := -1
for i := 0; i < MAX_TEMP_BREAKPOINTS; i++ {
if !s.breakpoints[i].active {
slot = i
break
}
}
if slot == -1 {
return ENOMEM // All slots in use
}
// Read original instruction (up to 8 bytes for safety)
_, err := PtracePeekText(pid, uintptr(addr), s.breakpoints[slot].originalInsn[:])
if err != nil {
return err
}
// Write SVC 144 (0x0A90) as breakpoint
svc144 := []byte{0x0A, 0x90}
_, err = PtracePokeText(pid, uintptr(addr), svc144)
if err != nil {
return err
}
s.breakpoints[slot].address = addr
s.breakpoints[slot].active = true
return nil
}
// removeTempBreakpoints removes all active temporary breakpoints
func (s *singleStepState) removeTempBreakpoints(pid int) error {
s.mu.Lock()
defer s.mu.Unlock()
var lastErr error
for i := 0; i < MAX_TEMP_BREAKPOINTS; i++ {
if !s.breakpoints[i].active {
continue
}
// Restore original instruction (first 2 bytes are enough for SVC 144)
_, err := PtracePokeText(pid, uintptr(s.breakpoints[i].address), s.breakpoints[i].originalInsn[:2])
if err != nil {
lastErr = err
}
s.breakpoints[i].active = false
}
return lastErr
}
// isTempBreakpoint checks if an address is a temporary breakpoint
func (s *singleStepState) isTempBreakpoint(addr uint64) bool {
s.mu.Lock()
defer s.mu.Unlock()
for i := 0; i < MAX_TEMP_BREAKPOINTS; i++ {
if s.breakpoints[i].active && s.breakpoints[i].address == addr {
return true
}
}
return false
}
// PtraceSingleStep executes a single instruction using temporary breakpoints.
//
// This function:
// 1. Reads the current instruction at PSW
// 2. Calculates the next instruction address (sequential)
// 3. If branch instruction, calculates branch destination
// 4. Sets temporary breakpoints at both addresses
// 5. Continues execution until breakpoint hit
// 6. Restores original instructions and adjusts PSW
//
// Returns nil on success, error on failure.
func PtraceSingleStep(pid int) error {
state := getProcessState(pid)
// 1. Get current PSW
var regs PtraceRegs
if err := PtraceGetRegs(pid, &regs); err != nil {
return err
}
pc := regs.Psw.Addr
// 2. Read instruction at PC
insn := make([]byte, 8)
if _, err := PtracePeekText(pid, uintptr(pc), insn); err != nil {
return err
}
// 3. Calculate next instruction addresses
nextPC := GetNextInstructionAddr(pc, insn)
branchDest := CalculateBranchDest(pid, pc, insn)
// 4. Set temporary breakpoints
if err := state.setTempBreakpoint(pid, nextPC); err != nil {
return err
}
// If branch instruction, set breakpoint at branch destination too
if branchDest != NO_BRANCH_DEST && branchDest != nextPC {
if err := state.setTempBreakpoint(pid, branchDest); err != nil {
state.removeTempBreakpoints(pid) // Cleanup first breakpoint
return err
}
}
// 5. Continue execution
if err := PtraceCont(pid, 0); err != nil {
state.removeTempBreakpoints(pid)
return err
}
// 6. Wait for breakpoint hit (retry on EINTR)
var status WaitStatus
for {
_, err := Wait4(pid, &status, 0, nil)
if err == EINTR {
continue // Retry on interrupted system call
}
if err != nil {
state.removeTempBreakpoints(pid)
return err
}
break
}
// Check if process stopped (should be at breakpoint)
if !status.Stopped() {
state.removeTempBreakpoints(pid)
return ECHILD // Process exited or error
}
// 7. Get current PSW (should be at breakpoint + 2)
if err := PtraceGetRegs(pid, &regs); err != nil {
state.removeTempBreakpoints(pid)
return err
}
// 8. Remove temporary breakpoints
if err := state.removeTempBreakpoints(pid); err != nil {
return err
}
// 9. Back up PSW by 2 bytes (to point at original instruction, not SVC 144)
// Use direct PSW write like ztrace does, not full register set
hitAddr := regs.Psw.Addr
if state.isTempBreakpoint(hitAddr - 2) {
if err := ptraceWritePSW(pid, uint32(hitAddr-2)); err != nil {
return err
}
}
return nil
}
// PtraceDetachWithCleanup detaches from a process and cleans up single-step state.
// Use this instead of PtraceDetach when using PtraceSingleStep.
func PtraceDetachWithCleanup(pid int) error {
state := getProcessState(pid)
state.removeTempBreakpoints(pid)
cleanupProcessState(pid)
return PtraceDetach(pid)
}
+1
View File
@@ -369,6 +369,7 @@ func Flistxattr(fd int, dest []byte) (sz int, err error) {
*/
//sys fcntl(fd int, cmd int, arg int) (val int, err error)
//sys fcntlPtr(fd int, cmd int, arg unsafe.Pointer) (val int, err error) = SYS_FCNTL
//sys kill(pid int, signum int, posix int) (err error)
+9
View File
@@ -369,6 +369,15 @@ func Dup3(oldfd, newfd, flags int) error {
//sys ExtattrSetLink(link string, attrnamespace int, attrname string, data uintptr, nbytes int) (ret int, err error)
//sys ExtattrDeleteLink(link string, attrnamespace int, attrname string) (err error)
//sys ExtattrListLink(link string, attrnamespace int, data uintptr, nbytes int) (ret int, err error)
//sys extattrGetFd(fd int, attrnamespace int, attrname string, data unsafe.Pointer, nbytes int) (ret int, err error)
//sys extattrSetFd(fd int, attrnamespace int, attrname string, data unsafe.Pointer, nbytes int) (ret int, err error)
//sys extattrListFd(fd int, attrnamespace int, data unsafe.Pointer, nbytes int) (ret int, err error)
//sys extattrGetFile(file string, attrnamespace int, attrname string, data unsafe.Pointer, nbytes int) (ret int, err error)
//sys extattrSetFile(file string, attrnamespace int, attrname string, data unsafe.Pointer, nbytes int) (ret int, err error)
//sys extattrListFile(file string, attrnamespace int, data unsafe.Pointer, nbytes int) (ret int, err error)
//sys extattrGetLink(link string, attrnamespace int, attrname string, data unsafe.Pointer, nbytes int) (ret int, err error)
//sys extattrSetLink(link string, attrnamespace int, attrname string, data unsafe.Pointer, nbytes int) (ret int, err error)
//sys extattrListLink(link string, attrnamespace int, data unsafe.Pointer, nbytes int) (ret int, err error)
//sys Fadvise(fd int, offset int64, length int64, advice int) (err error) = SYS_POSIX_FADVISE
//sys Faccessat(dirfd int, path string, mode uint32, flags int) (err error)
//sys Fchdir(fd int) (err error)
+20 -1
View File
@@ -1991,7 +1991,24 @@ func FsconfigReconfigure(fd int) (err error) {
return fsconfig(fd, FSCONFIG_CMD_RECONFIGURE, nil, nil, 0)
}
//sys Getdents(fd int, buf []byte) (n int, err error) = SYS_GETDENTS64
//sys getdents(fd int, buf []byte) (n int, err error) = SYS_GETDENTS64
// Getdents reads directory entries from fd into buf.
func Getdents(fd int, buf []byte) (n int, err error) {
// getdents64 stores d_ino with a 64-bit write, which faults on
// strict-alignment targets unless buf is 8-aligned.
if len(buf) > 0 {
if off := int(-uintptr(unsafe.Pointer(&buf[0])) & 7); off > 0 && off < len(buf) {
n, err = getdents(fd, buf[off:])
if n > 0 {
copy(buf, buf[off:off+n])
}
return n, err
}
}
return getdents(fd, buf)
}
//sysnb Getpgid(pid int) (pgid int, err error)
func Getpgrp() (pid int) {
@@ -2033,6 +2050,7 @@ func Getrandom(buf []byte, flags int) (n int, err error) {
//sysnb Kill(pid int, sig syscall.Signal) (err error)
//sys Klogctl(typ int, buf []byte) (n int, err error) = SYS_SYSLOG
//sys Lgetxattr(path string, attr string, dest []byte) (sz int, err error)
//sys Listns(req *NsIdReq, nsIds []uint64, flags uint) (n int, err error)
//sys Listxattr(path string, dest []byte) (sz int, err error)
//sys Llistxattr(path string, dest []byte) (sz int, err error)
//sys Lremovexattr(path string, attr string) (err error)
@@ -2046,6 +2064,7 @@ func Getrandom(buf []byte, flags int) (n int, err error) {
//sys PerfEventOpen(attr *PerfEventAttr, pid int, cpu int, groupFd int, flags int) (fd int, err error)
//sys PivotRoot(newroot string, putold string) (err error) = SYS_PIVOT_ROOT
//sys Prctl(option int, arg2 uintptr, arg3 uintptr, arg4 uintptr, arg5 uintptr) (err error)
//sys PrctlArg5Ptr(option int, arg2 uintptr, arg3 uintptr, arg4 uintptr, arg5 unsafe.Pointer) (err error) = SYS_PRCTL
//sys pselect6(nfd int, r *FdSet, w *FdSet, e *FdSet, timeout *Timespec, sigmask *sigset_argpack) (n int, err error)
//sys read(fd int, p []byte) (n int, err error)
//sys Removexattr(path string, attr string) (err error)
+3 -3
View File
@@ -8,7 +8,7 @@ package unix
//sys Fadvise(fd int, offset int64, length int64, advice int) (err error) = SYS_FADVISE64
//sys Fchown(fd int, uid int, gid int) (err error)
//sys Fstat(fd int, stat *Stat_t) (err error)
//sys Fstat(fd int, stat *Stat_t) (err error) = SYS_FSTAT64
//sys Fstatat(dirfd int, path string, stat *Stat_t, flags int) (err error) = SYS_FSTATAT64
//sys Fstatfs(fd int, buf *Statfs_t) (err error)
//sys Ftruncate(fd int, length int64) (err error)
@@ -19,7 +19,7 @@ package unix
//sysnb Getuid() (uid int)
//sys Lchown(path string, uid int, gid int) (err error)
//sys Listen(s int, n int) (err error)
//sys Lstat(path string, stat *Stat_t) (err error)
//sys Lstat(path string, stat *Stat_t) (err error) = SYS_LSTAT64
//sys Pause() (err error)
//sys pread(fd int, p []byte, offset int64) (n int, err error) = SYS_PREAD64
//sys pwrite(fd int, p []byte, offset int64) (n int, err error) = SYS_PWRITE64
@@ -31,7 +31,7 @@ package unix
//sys setfsuid(uid int) (prev int, err error)
//sys Shutdown(fd int, how int) (err error)
//sys Splice(rfd int, roff *int64, wfd int, woff *int64, len int, flags int) (n int64, err error)
//sys Stat(path string, stat *Stat_t) (err error)
//sys Stat(path string, stat *Stat_t) (err error) = SYS_STAT64
//sys Statfs(path string, buf *Statfs_t) (err error)
//sys SyncFileRange(fd int, off int64, n int64, flags int) (err error)
//sys Truncate(path string, length int64) (err error)
+9
View File
@@ -293,6 +293,15 @@ func Getvfsstat(buf []Statvfs_t, flags int) (n int, err error) {
//sys ExtattrSetLink(link string, attrnamespace int, attrname string, data uintptr, nbytes int) (ret int, err error)
//sys ExtattrDeleteLink(link string, attrnamespace int, attrname string) (err error)
//sys ExtattrListLink(link string, attrnamespace int, data uintptr, nbytes int) (ret int, err error)
//sys extattrGetFd(fd int, attrnamespace int, attrname string, data unsafe.Pointer, nbytes int) (ret int, err error)
//sys extattrSetFd(fd int, attrnamespace int, attrname string, data unsafe.Pointer, nbytes int) (ret int, err error)
//sys extattrListFd(fd int, attrnamespace int, data unsafe.Pointer, nbytes int) (ret int, err error)
//sys extattrGetFile(file string, attrnamespace int, attrname string, data unsafe.Pointer, nbytes int) (ret int, err error)
//sys extattrSetFile(file string, attrnamespace int, attrname string, data unsafe.Pointer, nbytes int) (ret int, err error)
//sys extattrListFile(file string, attrnamespace int, data unsafe.Pointer, nbytes int) (ret int, err error)
//sys extattrGetLink(link string, attrnamespace int, attrname string, data unsafe.Pointer, nbytes int) (ret int, err error)
//sys extattrSetLink(link string, attrnamespace int, attrname string, data unsafe.Pointer, nbytes int) (ret int, err error)
//sys extattrListLink(link string, attrnamespace int, data unsafe.Pointer, nbytes int) (ret int, err error)
//sys Faccessat(dirfd int, path string, mode uint32, flags int) (err error)
//sys Fadvise(fd int, offset int64, length int64, advice int) (err error) = SYS_POSIX_FADVISE
//sys Fchdir(fd int) (err error)
+1 -1
View File
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build (darwin || dragonfly || freebsd || (linux && !ppc64 && !ppc64le) || netbsd || openbsd || solaris) && gc
//go:build (darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris) && gc
package unix
-22
View File
@@ -1,22 +0,0 @@
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build linux && (ppc64le || ppc64) && gc
package unix
import "syscall"
func Syscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err syscall.Errno) {
return syscall.Syscall(trap, a1, a2, a3)
}
func Syscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err syscall.Errno) {
return syscall.Syscall6(trap, a1, a2, a3, a4, a5, a6)
}
func RawSyscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err syscall.Errno) {
return syscall.RawSyscall(trap, a1, a2, a3)
}
func RawSyscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err syscall.Errno) {
return syscall.RawSyscall6(trap, a1, a2, a3, a4, a5, a6)
}
-4
View File
@@ -27,14 +27,10 @@ import (
"unsafe"
)
//go:noescape
func initZosLibVec()
//go:noescape
func GetZosLibVec() uintptr
func init() {
initZosLibVec()
r0, _, _ := CallLeFuncWithPtrReturn(GetZosLibVec()+SYS_____GETENV_A<<4, uintptr(unsafe.Pointer(&([]byte("__ZOS_XSYSTRACE\x00"))[0])))
if r0 != 0 {
n, _, _ := CallLeFuncWithPtrReturn(GetZosLibVec()+SYS___ATOI_A<<4, r0)
+12 -12
View File
@@ -56,7 +56,7 @@ func Getxattr(file string, attr string, dest []byte) (sz int, err error) {
return -1, err
}
return ExtattrGetFile(file, nsid, a, uintptr(d), destsize)
return extattrGetFile(file, nsid, a, d, destsize)
}
func Fgetxattr(fd int, attr string, dest []byte) (sz int, err error) {
@@ -68,7 +68,7 @@ func Fgetxattr(fd int, attr string, dest []byte) (sz int, err error) {
return -1, err
}
return ExtattrGetFd(fd, nsid, a, uintptr(d), destsize)
return extattrGetFd(fd, nsid, a, d, destsize)
}
func Lgetxattr(link string, attr string, dest []byte) (sz int, err error) {
@@ -80,7 +80,7 @@ func Lgetxattr(link string, attr string, dest []byte) (sz int, err error) {
return -1, err
}
return ExtattrGetLink(link, nsid, a, uintptr(d), destsize)
return extattrGetLink(link, nsid, a, d, destsize)
}
// flags are unused on FreeBSD
@@ -97,7 +97,7 @@ func Fsetxattr(fd int, attr string, data []byte, flags int) (err error) {
return
}
_, err = ExtattrSetFd(fd, nsid, a, uintptr(d), datasiz)
_, err = extattrSetFd(fd, nsid, a, d, datasiz)
return
}
@@ -113,7 +113,7 @@ func Setxattr(file string, attr string, data []byte, flags int) (err error) {
return
}
_, err = ExtattrSetFile(file, nsid, a, uintptr(d), datasiz)
_, err = extattrSetFile(file, nsid, a, d, datasiz)
return
}
@@ -129,7 +129,7 @@ func Lsetxattr(link string, attr string, data []byte, flags int) (err error) {
return
}
_, err = ExtattrSetLink(link, nsid, a, uintptr(d), datasiz)
_, err = extattrSetLink(link, nsid, a, d, datasiz)
return
}
@@ -199,9 +199,9 @@ func ListxattrNS(file string, nsid int, dest []byte) (sz int, err error) {
d := initxattrdest(dest, 0)
destsiz := len(dest)
s, e := ExtattrListFile(file, nsid, uintptr(d), destsiz)
s, e := extattrListFile(file, nsid, d, destsiz)
if e != nil {
return 0, err
return 0, e
}
return s, nil
@@ -235,9 +235,9 @@ func FlistxattrNS(fd int, nsid int, dest []byte) (sz int, err error) {
d := initxattrdest(dest, 0)
destsiz := len(dest)
s, e := ExtattrListFd(fd, nsid, uintptr(d), destsiz)
s, e := extattrListFd(fd, nsid, d, destsiz)
if e != nil {
return 0, err
return 0, e
}
return s, nil
@@ -271,9 +271,9 @@ func LlistxattrNS(link string, nsid int, dest []byte) (sz int, err error) {
d := initxattrdest(dest, 0)
destsiz := len(dest)
s, e := ExtattrListLink(link, nsid, uintptr(d), destsiz)
s, e := extattrListLink(link, nsid, d, destsiz)
if e != nil {
return 0, err
return 0, e
}
return s, nil
+6
View File
@@ -1128,6 +1128,7 @@ const (
O_DSYNC = 0x400000
O_EVTONLY = 0x8000
O_EXCL = 0x800
O_EXEC = 0x40000000
O_EXLOCK = 0x20
O_FSYNC = 0x80
O_NDELAY = 0x4
@@ -1138,6 +1139,7 @@ const (
O_POPUP = 0x80000000
O_RDONLY = 0x0
O_RDWR = 0x2
O_SEARCH = 0x40100000
O_SHLOCK = 0x10
O_SYMLINK = 0x200000
O_SYNC = 0x80
@@ -1147,6 +1149,10 @@ const (
PARMRK = 0x8
PARODD = 0x2000
PENDIN = 0x20000000
PRIO_DARWIN_BG = 0x1000
PRIO_DARWIN_NONUI = 0x1001
PRIO_DARWIN_PROCESS = 0x4
PRIO_DARWIN_THREAD = 0x3
PRIO_PGRP = 0x1
PRIO_PROCESS = 0x0
PRIO_USER = 0x2
+6
View File
@@ -1128,6 +1128,7 @@ const (
O_DSYNC = 0x400000
O_EVTONLY = 0x8000
O_EXCL = 0x800
O_EXEC = 0x40000000
O_EXLOCK = 0x20
O_FSYNC = 0x80
O_NDELAY = 0x4
@@ -1138,6 +1139,7 @@ const (
O_POPUP = 0x80000000
O_RDONLY = 0x0
O_RDWR = 0x2
O_SEARCH = 0x40100000
O_SHLOCK = 0x10
O_SYMLINK = 0x200000
O_SYNC = 0x80
@@ -1147,6 +1149,10 @@ const (
PARMRK = 0x8
PARODD = 0x2000
PENDIN = 0x20000000
PRIO_DARWIN_BG = 0x1000
PRIO_DARWIN_NONUI = 0x1001
PRIO_DARWIN_PROCESS = 0x4
PRIO_DARWIN_THREAD = 0x3
PRIO_PGRP = 0x1
PRIO_PROCESS = 0x0
PRIO_USER = 0x2
+32
View File
@@ -752,10 +752,12 @@ const (
CLONE_ARGS_SIZE_VER0 = 0x40
CLONE_ARGS_SIZE_VER1 = 0x50
CLONE_ARGS_SIZE_VER2 = 0x58
CLONE_AUTOREAP = 0x400000000
CLONE_CHILD_CLEARTID = 0x200000
CLONE_CHILD_SETTID = 0x1000000
CLONE_CLEAR_SIGHAND = 0x100000000
CLONE_DETACHED = 0x400000
CLONE_EMPTY_MNTNS = 0x2000000000
CLONE_FILES = 0x400
CLONE_FS = 0x200
CLONE_INTO_CGROUP = 0x200000000
@@ -768,9 +770,11 @@ const (
CLONE_NEWTIME = 0x80
CLONE_NEWUSER = 0x10000000
CLONE_NEWUTS = 0x4000000
CLONE_NNP = 0x800000000
CLONE_PARENT = 0x8000
CLONE_PARENT_SETTID = 0x100000
CLONE_PIDFD = 0x1000
CLONE_PIDFD_AUTOKILL = 0x1000000000
CLONE_PTRACE = 0x2000
CLONE_SETTLS = 0x80000
CLONE_SIGHAND = 0x800
@@ -803,12 +807,15 @@ const (
DEVLINK_GENL_MCGRP_CONFIG_NAME = "config"
DEVLINK_GENL_NAME = "devlink"
DEVLINK_GENL_VERSION = 0x1
DEVLINK_INDEX_BUS_NAME = "devlink_index"
DEVLINK_PORT_FN_CAP_IPSEC_CRYPTO = 0x4
DEVLINK_PORT_FN_CAP_IPSEC_PACKET = 0x8
DEVLINK_PORT_FN_CAP_MIGRATABLE = 0x2
DEVLINK_PORT_FN_CAP_ROCE = 0x1
DEVLINK_RATE_TCS_MAX = 0x8
DEVLINK_RATE_TC_INDEX_MAX = 0x7
DEVLINK_RESOURCE_SCOPE_DEV = 0x1
DEVLINK_RESOURCE_SCOPE_PORT = 0x2
DEVLINK_SB_THRESHOLD_TO_ALPHA_MAX = 0x14
DEVLINK_SUPPORTED_FLASH_OVERWRITE_SECTIONS = 0x3
DEVMEM_MAGIC = 0x454d444d
@@ -1127,6 +1134,7 @@ const (
ETHTOOL_SWOL = 0x6
ETHTOOL_TEST = 0x1a
ETH_P_1588 = 0x88f7
ETH_P_8021AC = 0x8870
ETH_P_8021AD = 0x88a8
ETH_P_8021AH = 0x88e7
ETH_P_8021Q = 0x8100
@@ -1170,6 +1178,7 @@ const (
ETH_P_ETHERCAT = 0x88a4
ETH_P_FCOE = 0x8906
ETH_P_FIP = 0x8914
ETH_P_GRE_OSI = 0xfe
ETH_P_HDLC = 0x19
ETH_P_HSR = 0x892f
ETH_P_IBOE = 0x8915
@@ -1198,6 +1207,7 @@ const (
ETH_P_MXLGSW = 0x88c3
ETH_P_NCSI = 0x88f8
ETH_P_NSH = 0x894f
ETH_P_NXP_NETC = 0xfd3a
ETH_P_PAE = 0x888e
ETH_P_PAUSE = 0x8808
ETH_P_PHONET = 0xf5
@@ -1359,6 +1369,7 @@ const (
FAN_UNLIMITED_MARKS = 0x20
FAN_UNLIMITED_QUEUE = 0x10
FD_CLOEXEC = 0x1
FD_NSFS_ROOT = -0x2713
FD_PIDFS_ROOT = -0x2712
FD_SETSIZE = 0x400
FF0 = 0x0
@@ -1427,6 +1438,8 @@ const (
FS_POLICY_FLAGS_PAD_8 = 0x1
FS_POLICY_FLAGS_PAD_MASK = 0x3
FS_POLICY_FLAGS_VALID = 0x7
FS_SHUTDOWN_FLAGS_LOGFLUSH = 0x1
FS_SHUTDOWN_FLAGS_NOLOGFLUSH = 0x2
FS_VERITY_FL = 0x100000
FS_VERITY_HASH_ALG_SHA256 = 0x1
FS_VERITY_HASH_ALG_SHA512 = 0x2
@@ -1441,6 +1454,7 @@ const (
F_DUPFD_CLOEXEC = 0x406
F_DUPFD_QUERY = 0x403
F_EXLCK = 0x4
F_GETDELEG = 0x40f
F_GETFD = 0x1
F_GETFL = 0x3
F_GETLEASE = 0x401
@@ -1462,6 +1476,7 @@ const (
F_SEAL_SEAL = 0x1
F_SEAL_SHRINK = 0x2
F_SEAL_WRITE = 0x8
F_SETDELEG = 0x410
F_SETFD = 0x2
F_SETFL = 0x4
F_SETLEASE = 0x400
@@ -1632,6 +1647,7 @@ const (
IN_UNMOUNT = 0x2000
IOCTL_MEI_CONNECT_CLIENT = 0xc0104801
IOCTL_MEI_CONNECT_CLIENT_VTAG = 0xc0144804
IPPROTO_AGGFRAG = 0x90
IPPROTO_AH = 0x33
IPPROTO_BEETPH = 0x5e
IPPROTO_COMP = 0x6c
@@ -1913,10 +1929,14 @@ const (
LANDLOCK_ACCESS_FS_REFER = 0x2000
LANDLOCK_ACCESS_FS_REMOVE_DIR = 0x10
LANDLOCK_ACCESS_FS_REMOVE_FILE = 0x20
LANDLOCK_ACCESS_FS_RESOLVE_UNIX = 0x10000
LANDLOCK_ACCESS_FS_TRUNCATE = 0x4000
LANDLOCK_ACCESS_FS_WRITE_FILE = 0x2
LANDLOCK_ACCESS_NET_BIND_TCP = 0x1
LANDLOCK_ACCESS_NET_BIND_UDP = 0x4
LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP = 0x8
LANDLOCK_ACCESS_NET_CONNECT_TCP = 0x2
LANDLOCK_ADD_RULE_QUIET = 0x1
LANDLOCK_CREATE_RULESET_ERRATA = 0x2
LANDLOCK_CREATE_RULESET_VERSION = 0x1
LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON = 0x2
@@ -3408,6 +3428,7 @@ const (
SCHED_FLAG_UTIL_CLAMP = 0x60
SCHED_FLAG_UTIL_CLAMP_MAX = 0x40
SCHED_FLAG_UTIL_CLAMP_MIN = 0x20
SCHED_GETATTR_FLAG_DL_DYNAMIC = 0x1
SCHED_IDLE = 0x5
SCHED_NORMAL = 0x0
SCHED_RESET_ON_FORK = 0x40000000
@@ -3790,6 +3811,14 @@ const (
TCPOPT_TIMESTAMP = 0x8
TCPOPT_TSTAMP_HDR = 0x101080a
TCPOPT_WINDOW = 0x3
TCP_ACCECN_ACE_FAIL_RECV = 0x2
TCP_ACCECN_ACE_FAIL_SEND = 0x1
TCP_ACCECN_OPT_COUNTER_SEEN = 0x2
TCP_ACCECN_OPT_EMPTY_SEEN = 0x1
TCP_ACCECN_OPT_FAIL_RECV = 0x8
TCP_ACCECN_OPT_FAIL_SEEN = 0x3
TCP_ACCECN_OPT_FAIL_SEND = 0x4
TCP_ACCECN_OPT_NOT_SEEN = 0x0
TCP_AO_KEYF_EXCLUDE_OPT = 0x2
TCP_AO_KEYF_IFINDEX = 0x1
TCP_AO_MAXKEYLEN = 0x50
@@ -3804,6 +3833,7 @@ const (
TCP_COOKIE_TRANSACTIONS = 0xf
TCP_CORK = 0x3
TCP_DEFER_ACCEPT = 0x9
TCP_DELACK_MAX_US = 0x2e
TCP_FASTOPEN = 0x17
TCP_FASTOPEN_CONNECT = 0x1e
TCP_FASTOPEN_KEY = 0x21
@@ -3836,6 +3866,8 @@ const (
TCP_REPAIR_OPTIONS = 0x16
TCP_REPAIR_QUEUE = 0x14
TCP_REPAIR_WINDOW = 0x1d
TCP_RTO_MAX_MS = 0x2c
TCP_RTO_MIN_US = 0x2d
TCP_SAVED_SYN = 0x1c
TCP_SAVE_SYN = 0x1b
TCP_SYNCNT = 0x7
+2
View File
@@ -600,6 +600,7 @@ const (
EDQUOT = syscall.Errno(0x7a)
EFSBADCRC = syscall.Errno(0x4a)
EFSCORRUPTED = syscall.Errno(0x75)
EFTYPE = syscall.Errno(0x86)
EHOSTDOWN = syscall.Errno(0x70)
EHOSTUNREACH = syscall.Errno(0x71)
EHWPOISON = syscall.Errno(0x85)
@@ -840,6 +841,7 @@ var errorList = [...]struct {
{131, "ENOTRECOVERABLE", "state not recoverable"},
{132, "ERFKILL", "operation not possible due to RF-kill"},
{133, "EHWPOISON", "memory page has hardware error"},
{134, "EFTYPE", "unknown error 134"},
}
// Signal table
+2
View File
@@ -600,6 +600,7 @@ const (
EDQUOT = syscall.Errno(0x7a)
EFSBADCRC = syscall.Errno(0x4a)
EFSCORRUPTED = syscall.Errno(0x75)
EFTYPE = syscall.Errno(0x86)
EHOSTDOWN = syscall.Errno(0x70)
EHOSTUNREACH = syscall.Errno(0x71)
EHWPOISON = syscall.Errno(0x85)
@@ -840,6 +841,7 @@ var errorList = [...]struct {
{131, "ENOTRECOVERABLE", "state not recoverable"},
{132, "ERFKILL", "operation not possible due to RF-kill"},
{133, "EHWPOISON", "memory page has hardware error"},
{134, "EFTYPE", "unknown error 134"},
}
// Signal table
+2
View File
@@ -605,6 +605,7 @@ const (
EDQUOT = syscall.Errno(0x7a)
EFSBADCRC = syscall.Errno(0x4a)
EFSCORRUPTED = syscall.Errno(0x75)
EFTYPE = syscall.Errno(0x86)
EHOSTDOWN = syscall.Errno(0x70)
EHOSTUNREACH = syscall.Errno(0x71)
EHWPOISON = syscall.Errno(0x85)
@@ -845,6 +846,7 @@ var errorList = [...]struct {
{131, "ENOTRECOVERABLE", "state not recoverable"},
{132, "ERFKILL", "operation not possible due to RF-kill"},
{133, "EHWPOISON", "memory page has hardware error"},
{134, "EFTYPE", "unknown error 134"},
}
// Signal table
+2
View File
@@ -602,6 +602,7 @@ const (
EDQUOT = syscall.Errno(0x7a)
EFSBADCRC = syscall.Errno(0x4a)
EFSCORRUPTED = syscall.Errno(0x75)
EFTYPE = syscall.Errno(0x86)
EHOSTDOWN = syscall.Errno(0x70)
EHOSTUNREACH = syscall.Errno(0x71)
EHWPOISON = syscall.Errno(0x85)
@@ -842,6 +843,7 @@ var errorList = [...]struct {
{131, "ENOTRECOVERABLE", "state not recoverable"},
{132, "ERFKILL", "operation not possible due to RF-kill"},
{133, "EHWPOISON", "memory page has hardware error"},
{134, "EFTYPE", "unknown error 134"},
}
// Signal table
+2
View File
@@ -592,6 +592,7 @@ const (
EDQUOT = syscall.Errno(0x7a)
EFSBADCRC = syscall.Errno(0x4a)
EFSCORRUPTED = syscall.Errno(0x75)
EFTYPE = syscall.Errno(0x86)
EHOSTDOWN = syscall.Errno(0x70)
EHOSTUNREACH = syscall.Errno(0x71)
EHWPOISON = syscall.Errno(0x85)
@@ -832,6 +833,7 @@ var errorList = [...]struct {
{131, "ENOTRECOVERABLE", "state not recoverable"},
{132, "ERFKILL", "operation not possible due to RF-kill"},
{133, "EHWPOISON", "memory page has hardware error"},
{134, "EFTYPE", "unknown error 134"},
}
// Signal table
+2
View File
@@ -601,6 +601,7 @@ const (
EDQUOT = syscall.Errno(0x46d)
EFSBADCRC = syscall.Errno(0x4d)
EFSCORRUPTED = syscall.Errno(0x87)
EFTYPE = syscall.Errno(0xa9)
EHOSTDOWN = syscall.Errno(0x93)
EHOSTUNREACH = syscall.Errno(0x94)
EHWPOISON = syscall.Errno(0xa8)
@@ -845,6 +846,7 @@ var errorList = [...]struct {
{166, "ENOTRECOVERABLE", "state not recoverable"},
{167, "ERFKILL", "operation not possible due to RF-kill"},
{168, "EHWPOISON", "memory page has hardware error"},
{169, "EFTYPE", "unknown error 169"},
{1133, "EDQUOT", "disk quota exceeded"},
}
+2
View File
@@ -601,6 +601,7 @@ const (
EDQUOT = syscall.Errno(0x46d)
EFSBADCRC = syscall.Errno(0x4d)
EFSCORRUPTED = syscall.Errno(0x87)
EFTYPE = syscall.Errno(0xa9)
EHOSTDOWN = syscall.Errno(0x93)
EHOSTUNREACH = syscall.Errno(0x94)
EHWPOISON = syscall.Errno(0xa8)
@@ -845,6 +846,7 @@ var errorList = [...]struct {
{166, "ENOTRECOVERABLE", "state not recoverable"},
{167, "ERFKILL", "operation not possible due to RF-kill"},
{168, "EHWPOISON", "memory page has hardware error"},
{169, "EFTYPE", "unknown error 169"},
{1133, "EDQUOT", "disk quota exceeded"},
}
+2
View File
@@ -601,6 +601,7 @@ const (
EDQUOT = syscall.Errno(0x46d)
EFSBADCRC = syscall.Errno(0x4d)
EFSCORRUPTED = syscall.Errno(0x87)
EFTYPE = syscall.Errno(0xa9)
EHOSTDOWN = syscall.Errno(0x93)
EHOSTUNREACH = syscall.Errno(0x94)
EHWPOISON = syscall.Errno(0xa8)
@@ -845,6 +846,7 @@ var errorList = [...]struct {
{166, "ENOTRECOVERABLE", "state not recoverable"},
{167, "ERFKILL", "operation not possible due to RF-kill"},
{168, "EHWPOISON", "memory page has hardware error"},
{169, "EFTYPE", "unknown error 169"},
{1133, "EDQUOT", "disk quota exceeded"},
}
+2
View File
@@ -601,6 +601,7 @@ const (
EDQUOT = syscall.Errno(0x46d)
EFSBADCRC = syscall.Errno(0x4d)
EFSCORRUPTED = syscall.Errno(0x87)
EFTYPE = syscall.Errno(0xa9)
EHOSTDOWN = syscall.Errno(0x93)
EHOSTUNREACH = syscall.Errno(0x94)
EHWPOISON = syscall.Errno(0xa8)
@@ -845,6 +846,7 @@ var errorList = [...]struct {
{166, "ENOTRECOVERABLE", "state not recoverable"},
{167, "ERFKILL", "operation not possible due to RF-kill"},
{168, "EHWPOISON", "memory page has hardware error"},
{169, "EFTYPE", "unknown error 169"},
{1133, "EDQUOT", "disk quota exceeded"},
}
+2
View File
@@ -657,6 +657,7 @@ const (
EDQUOT = syscall.Errno(0x7a)
EFSBADCRC = syscall.Errno(0x4a)
EFSCORRUPTED = syscall.Errno(0x75)
EFTYPE = syscall.Errno(0x86)
EHOSTDOWN = syscall.Errno(0x70)
EHOSTUNREACH = syscall.Errno(0x71)
EHWPOISON = syscall.Errno(0x85)
@@ -898,6 +899,7 @@ var errorList = [...]struct {
{131, "ENOTRECOVERABLE", "state not recoverable"},
{132, "ERFKILL", "operation not possible due to RF-kill"},
{133, "EHWPOISON", "memory page has hardware error"},
{134, "EFTYPE", "unknown error 134"},
}
// Signal table
+2
View File
@@ -661,6 +661,7 @@ const (
EDQUOT = syscall.Errno(0x7a)
EFSBADCRC = syscall.Errno(0x4a)
EFSCORRUPTED = syscall.Errno(0x75)
EFTYPE = syscall.Errno(0x86)
EHOSTDOWN = syscall.Errno(0x70)
EHOSTUNREACH = syscall.Errno(0x71)
EHWPOISON = syscall.Errno(0x85)
@@ -902,6 +903,7 @@ var errorList = [...]struct {
{131, "ENOTRECOVERABLE", "state not recoverable"},
{132, "ERFKILL", "operation not possible due to RF-kill"},
{133, "EHWPOISON", "memory page has hardware error"},
{134, "EFTYPE", "unknown error 134"},
}
// Signal table
+2
View File
@@ -661,6 +661,7 @@ const (
EDQUOT = syscall.Errno(0x7a)
EFSBADCRC = syscall.Errno(0x4a)
EFSCORRUPTED = syscall.Errno(0x75)
EFTYPE = syscall.Errno(0x86)
EHOSTDOWN = syscall.Errno(0x70)
EHOSTUNREACH = syscall.Errno(0x71)
EHWPOISON = syscall.Errno(0x85)
@@ -902,6 +903,7 @@ var errorList = [...]struct {
{131, "ENOTRECOVERABLE", "state not recoverable"},
{132, "ERFKILL", "operation not possible due to RF-kill"},
{133, "EHWPOISON", "memory page has hardware error"},
{134, "EFTYPE", "unknown error 134"},
}
// Signal table
+2
View File
@@ -602,6 +602,7 @@ const (
EDQUOT = syscall.Errno(0x7a)
EFSBADCRC = syscall.Errno(0x4a)
EFSCORRUPTED = syscall.Errno(0x75)
EFTYPE = syscall.Errno(0x86)
EHOSTDOWN = syscall.Errno(0x70)
EHOSTUNREACH = syscall.Errno(0x71)
EHWPOISON = syscall.Errno(0x85)
@@ -842,6 +843,7 @@ var errorList = [...]struct {
{131, "ENOTRECOVERABLE", "state not recoverable"},
{132, "ERFKILL", "operation not possible due to RF-kill"},
{133, "EHWPOISON", "memory page has hardware error"},
{134, "EFTYPE", "unknown error 134"},
}
// Signal table
+2
View File
@@ -661,6 +661,7 @@ const (
EDQUOT = syscall.Errno(0x7a)
EFSBADCRC = syscall.Errno(0x4a)
EFSCORRUPTED = syscall.Errno(0x75)
EFTYPE = syscall.Errno(0x86)
EHOSTDOWN = syscall.Errno(0x70)
EHOSTUNREACH = syscall.Errno(0x71)
EHWPOISON = syscall.Errno(0x85)
@@ -901,6 +902,7 @@ var errorList = [...]struct {
{131, "ENOTRECOVERABLE", "state not recoverable"},
{132, "ERFKILL", "operation not possible due to RF-kill"},
{133, "EHWPOISON", "memory page has hardware error"},
{134, "EFTYPE", "unknown error 134"},
}
// Signal table
+2
View File
@@ -704,6 +704,7 @@ const (
EDQUOT = syscall.Errno(0x45)
EFSBADCRC = syscall.Errno(0x4c)
EFSCORRUPTED = syscall.Errno(0x75)
EFTYPE = syscall.Errno(0x88)
EHOSTDOWN = syscall.Errno(0x40)
EHOSTUNREACH = syscall.Errno(0x41)
EHWPOISON = syscall.Errno(0x87)
@@ -950,6 +951,7 @@ var errorList = [...]struct {
{133, "ENOTRECOVERABLE", "state not recoverable"},
{134, "ERFKILL", "operation not possible due to RF-kill"},
{135, "EHWPOISON", "memory page has hardware error"},
{136, "EFTYPE", "unknown error 136"},
}
// Signal table
+11
View File
@@ -702,6 +702,17 @@ var libc_fcntl_trampoline_addr uintptr
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func fcntlPtr(fd int, cmd int, arg unsafe.Pointer) (val int, err error) {
r0, _, e1 := syscall_syscall(libc_fcntl_trampoline_addr, uintptr(fd), uintptr(cmd), uintptr(arg))
val = int(r0)
if e1 != 0 {
err = errnoErr(e1)
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func kill(pid int, signum int, posix int) (err error) {
_, _, e1 := syscall_syscall(libc_kill_trampoline_addr, uintptr(pid), uintptr(signum), uintptr(posix))
if e1 != 0 {
+11
View File
@@ -702,6 +702,17 @@ var libc_fcntl_trampoline_addr uintptr
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func fcntlPtr(fd int, cmd int, arg unsafe.Pointer) (val int, err error) {
r0, _, e1 := syscall_syscall(libc_fcntl_trampoline_addr, uintptr(fd), uintptr(cmd), uintptr(arg))
val = int(r0)
if e1 != 0 {
err = errnoErr(e1)
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func kill(pid int, signum int, posix int) (err error) {
_, _, e1 := syscall_syscall(libc_kill_trampoline_addr, uintptr(pid), uintptr(signum), uintptr(posix))
if e1 != 0 {
+159
View File
@@ -825,6 +825,165 @@ func ExtattrListLink(link string, attrnamespace int, data uintptr, nbytes int) (
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func extattrGetFd(fd int, attrnamespace int, attrname string, data unsafe.Pointer, nbytes int) (ret int, err error) {
var _p0 *byte
_p0, err = BytePtrFromString(attrname)
if err != nil {
return
}
r0, _, e1 := Syscall6(SYS_EXTATTR_GET_FD, uintptr(fd), uintptr(attrnamespace), uintptr(unsafe.Pointer(_p0)), uintptr(data), uintptr(nbytes), 0)
ret = int(r0)
if e1 != 0 {
err = errnoErr(e1)
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func extattrSetFd(fd int, attrnamespace int, attrname string, data unsafe.Pointer, nbytes int) (ret int, err error) {
var _p0 *byte
_p0, err = BytePtrFromString(attrname)
if err != nil {
return
}
r0, _, e1 := Syscall6(SYS_EXTATTR_SET_FD, uintptr(fd), uintptr(attrnamespace), uintptr(unsafe.Pointer(_p0)), uintptr(data), uintptr(nbytes), 0)
ret = int(r0)
if e1 != 0 {
err = errnoErr(e1)
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func extattrListFd(fd int, attrnamespace int, data unsafe.Pointer, nbytes int) (ret int, err error) {
r0, _, e1 := Syscall6(SYS_EXTATTR_LIST_FD, uintptr(fd), uintptr(attrnamespace), uintptr(data), uintptr(nbytes), 0, 0)
ret = int(r0)
if e1 != 0 {
err = errnoErr(e1)
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func extattrGetFile(file string, attrnamespace int, attrname string, data unsafe.Pointer, nbytes int) (ret int, err error) {
var _p0 *byte
_p0, err = BytePtrFromString(file)
if err != nil {
return
}
var _p1 *byte
_p1, err = BytePtrFromString(attrname)
if err != nil {
return
}
r0, _, e1 := Syscall6(SYS_EXTATTR_GET_FILE, uintptr(unsafe.Pointer(_p0)), uintptr(attrnamespace), uintptr(unsafe.Pointer(_p1)), uintptr(data), uintptr(nbytes), 0)
ret = int(r0)
if e1 != 0 {
err = errnoErr(e1)
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func extattrSetFile(file string, attrnamespace int, attrname string, data unsafe.Pointer, nbytes int) (ret int, err error) {
var _p0 *byte
_p0, err = BytePtrFromString(file)
if err != nil {
return
}
var _p1 *byte
_p1, err = BytePtrFromString(attrname)
if err != nil {
return
}
r0, _, e1 := Syscall6(SYS_EXTATTR_SET_FILE, uintptr(unsafe.Pointer(_p0)), uintptr(attrnamespace), uintptr(unsafe.Pointer(_p1)), uintptr(data), uintptr(nbytes), 0)
ret = int(r0)
if e1 != 0 {
err = errnoErr(e1)
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func extattrListFile(file string, attrnamespace int, data unsafe.Pointer, nbytes int) (ret int, err error) {
var _p0 *byte
_p0, err = BytePtrFromString(file)
if err != nil {
return
}
r0, _, e1 := Syscall6(SYS_EXTATTR_LIST_FILE, uintptr(unsafe.Pointer(_p0)), uintptr(attrnamespace), uintptr(data), uintptr(nbytes), 0, 0)
ret = int(r0)
if e1 != 0 {
err = errnoErr(e1)
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func extattrGetLink(link string, attrnamespace int, attrname string, data unsafe.Pointer, nbytes int) (ret int, err error) {
var _p0 *byte
_p0, err = BytePtrFromString(link)
if err != nil {
return
}
var _p1 *byte
_p1, err = BytePtrFromString(attrname)
if err != nil {
return
}
r0, _, e1 := Syscall6(SYS_EXTATTR_GET_LINK, uintptr(unsafe.Pointer(_p0)), uintptr(attrnamespace), uintptr(unsafe.Pointer(_p1)), uintptr(data), uintptr(nbytes), 0)
ret = int(r0)
if e1 != 0 {
err = errnoErr(e1)
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func extattrSetLink(link string, attrnamespace int, attrname string, data unsafe.Pointer, nbytes int) (ret int, err error) {
var _p0 *byte
_p0, err = BytePtrFromString(link)
if err != nil {
return
}
var _p1 *byte
_p1, err = BytePtrFromString(attrname)
if err != nil {
return
}
r0, _, e1 := Syscall6(SYS_EXTATTR_SET_LINK, uintptr(unsafe.Pointer(_p0)), uintptr(attrnamespace), uintptr(unsafe.Pointer(_p1)), uintptr(data), uintptr(nbytes), 0)
ret = int(r0)
if e1 != 0 {
err = errnoErr(e1)
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func extattrListLink(link string, attrnamespace int, data unsafe.Pointer, nbytes int) (ret int, err error) {
var _p0 *byte
_p0, err = BytePtrFromString(link)
if err != nil {
return
}
r0, _, e1 := Syscall6(SYS_EXTATTR_LIST_LINK, uintptr(unsafe.Pointer(_p0)), uintptr(attrnamespace), uintptr(data), uintptr(nbytes), 0, 0)
ret = int(r0)
if e1 != 0 {
err = errnoErr(e1)
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func Fadvise(fd int, offset int64, length int64, advice int) (err error) {
_, _, e1 := Syscall6(SYS_POSIX_FADVISE, uintptr(fd), uintptr(offset), uintptr(offset>>32), uintptr(length), uintptr(length>>32), uintptr(advice))
if e1 != 0 {

Some files were not shown because too many files have changed in this diff Show More