diff --git a/LICENSES/vendor/github.com/pandatix/go-abnf/LICENSE b/LICENSES/vendor/github.com/pandatix/go-abnf/LICENSE new file mode 100644 index 00000000000..3c804b88bcf --- /dev/null +++ b/LICENSES/vendor/github.com/pandatix/go-abnf/LICENSE @@ -0,0 +1,25 @@ += vendor/github.com/pandatix/go-abnf licensed under: = + +MIT License + +Copyright (c) 2024 Lucas TESSON - PandatiX + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + += vendor/github.com/pandatix/go-abnf/LICENSE d3af9189e0a449a8402ddf72567f0748 diff --git a/go.mod b/go.mod index 8ec0764689f..8b513344ab2 100644 --- a/go.mod +++ b/go.mod @@ -48,7 +48,7 @@ require ( github.com/opencontainers/selinux v1.15.1 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 github.com/prometheus/client_model v0.6.3 - github.com/prometheus/common v0.72.0 + github.com/prometheus/common v0.73.1 github.com/robfig/cron/v3 v3.0.1 github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 @@ -184,6 +184,7 @@ require ( github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect github.com/opencontainers/runtime-spec v1.3.0 // indirect + github.com/pandatix/go-abnf v0.5.5 // indirect github.com/peterbourgon/diskv/v3 v3.0.1 // indirect github.com/prometheus/client_golang v1.25.0 // indirect github.com/prometheus/procfs v0.22.0 // indirect diff --git a/go.sum b/go.sum index 5f9301343b9..125bfeee952 100644 --- a/go.sum +++ b/go.sum @@ -245,6 +245,8 @@ github.com/opencontainers/runtime-spec v1.3.0 h1:YZupQUdctfhpZy3TM39nN9Ika5CBWT5 github.com/opencontainers/runtime-spec v1.3.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/selinux v1.15.1 h1:ERxeh5caJvCzNAKdI8WQbJmB1LDTn4BuaAg8wihLBpA= github.com/opencontainers/selinux v1.15.1/go.mod h1:LenyElirjUHszfxrjuFqC85HIeXZKumHcKMQtnaDlQQ= +github.com/pandatix/go-abnf v0.5.5 h1:EXex5KkRQx4Dm6GnpAcQnIMBs9KiK0uf8p/wq+4t9PM= +github.com/pandatix/go-abnf v0.5.5/go.mod h1:AE0KD3/VNoCjGzc1gqetRKshbI4SYErXQ/8kPenR5mc= github.com/peterbourgon/diskv/v3 v3.0.1 h1:x06SQA46+PKIUftmEujdwSEpIx8kR+M9eLYsUxeYveU= github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= @@ -255,8 +257,8 @@ github.com/prometheus/client_golang v1.25.0 h1:cDtW1TcFsMi1RJc8khwCKB4VhzwV04aKq github.com/prometheus/client_golang v1.25.0/go.mod h1:j2X/4/kaQHLPnmyEU8zjmkndo1T317VpCIQEml8LTKk= github.com/prometheus/client_model v0.6.3 h1:O0jaTVAYNxTHYInEPFJt5I3+sN8zqBtVMPTB1qyxiEo= github.com/prometheus/client_model v0.6.3/go.mod h1:gpN5P9S7Rr6Yr92PiQ+Ixvhf6JZEkF1dnxsYL2aPBEM= -github.com/prometheus/common v0.72.0 h1:tAYsE+sPJxIncDAobm4H5aQjmox9ZxEIIqPbiffa8G4= -github.com/prometheus/common v0.72.0/go.mod h1:77NWqAQ2tXT7BIK40qjJdw5Acrsrg1TlHAnsQi3i6mk= +github.com/prometheus/common v0.73.1 h1:SZWh/j4AvhCdhX8jvYSnxvPxhWlC9QZkjJ+tOrEr7kU= +github.com/prometheus/common v0.73.1/go.mod h1:I9aA/Ukz9s1olOobh8KtCkU7B1mQPQsB9UvQPsUQ1Dk= github.com/prometheus/procfs v0.22.0 h1:6q9+/JL9IKAPbCmBrv9n5O5Ty3NKnciV5X7YGw0oics= github.com/prometheus/procfs v0.22.0/go.mod h1:CvmFr/GVhIjIvWJZW3tgkODBQMRIf0EyWMQLHCHab58= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= diff --git a/hack/tools/instrumentation/go.mod b/hack/tools/instrumentation/go.mod index ef1e052628a..aef8b747143 100644 --- a/hack/tools/instrumentation/go.mod +++ b/hack/tools/instrumentation/go.mod @@ -17,8 +17,9 @@ require ( github.com/go-logr/logr v1.4.4 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pandatix/go-abnf v0.5.5 // indirect github.com/prometheus/client_model v0.6.3 // indirect - github.com/prometheus/common v0.72.0 // indirect + github.com/prometheus/common v0.73.1 // indirect github.com/prometheus/procfs v0.22.0 // indirect go.opentelemetry.io/otel v1.47.0 // indirect go.opentelemetry.io/otel/trace v1.47.0 // indirect diff --git a/hack/tools/instrumentation/go.sum b/hack/tools/instrumentation/go.sum index e42ab754429..ea1149a5cc0 100644 --- a/hack/tools/instrumentation/go.sum +++ b/hack/tools/instrumentation/go.sum @@ -14,12 +14,14 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/pandatix/go-abnf v0.5.5 h1:EXex5KkRQx4Dm6GnpAcQnIMBs9KiK0uf8p/wq+4t9PM= +github.com/pandatix/go-abnf v0.5.5/go.mod h1:AE0KD3/VNoCjGzc1gqetRKshbI4SYErXQ/8kPenR5mc= github.com/prometheus/client_golang v1.25.0 h1:cDtW1TcFsMi1RJc8khwCKB4VhzwV04aKqbYtNOXuiK0= github.com/prometheus/client_golang v1.25.0/go.mod h1:j2X/4/kaQHLPnmyEU8zjmkndo1T317VpCIQEml8LTKk= github.com/prometheus/client_model v0.6.3 h1:O0jaTVAYNxTHYInEPFJt5I3+sN8zqBtVMPTB1qyxiEo= github.com/prometheus/client_model v0.6.3/go.mod h1:gpN5P9S7Rr6Yr92PiQ+Ixvhf6JZEkF1dnxsYL2aPBEM= -github.com/prometheus/common v0.72.0 h1:tAYsE+sPJxIncDAobm4H5aQjmox9ZxEIIqPbiffa8G4= -github.com/prometheus/common v0.72.0/go.mod h1:77NWqAQ2tXT7BIK40qjJdw5Acrsrg1TlHAnsQi3i6mk= +github.com/prometheus/common v0.73.1 h1:SZWh/j4AvhCdhX8jvYSnxvPxhWlC9QZkjJ+tOrEr7kU= +github.com/prometheus/common v0.73.1/go.mod h1:I9aA/Ukz9s1olOobh8KtCkU7B1mQPQsB9UvQPsUQ1Dk= github.com/prometheus/procfs v0.22.0 h1:6q9+/JL9IKAPbCmBrv9n5O5Ty3NKnciV5X7YGw0oics= github.com/prometheus/procfs v0.22.0/go.mod h1:CvmFr/GVhIjIvWJZW3tgkODBQMRIf0EyWMQLHCHab58= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= diff --git a/staging/src/k8s.io/apiextensions-apiserver/go.mod b/staging/src/k8s.io/apiextensions-apiserver/go.mod index 99a40525b34..fadb4167d91 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/go.mod +++ b/staging/src/k8s.io/apiextensions-apiserver/go.mod @@ -74,10 +74,11 @@ require ( github.com/moby/spdystream v0.5.1 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect + github.com/pandatix/go-abnf v0.5.5 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.25.0 // indirect github.com/prometheus/client_model v0.6.3 // indirect - github.com/prometheus/common v0.72.0 // indirect + github.com/prometheus/common v0.73.1 // indirect github.com/prometheus/procfs v0.22.0 // indirect github.com/sirupsen/logrus v1.10.2 // indirect github.com/soheilhy/cmux v0.1.5 // indirect diff --git a/staging/src/k8s.io/apiextensions-apiserver/go.sum b/staging/src/k8s.io/apiextensions-apiserver/go.sum index cc07b21c809..079f67fcaf1 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/go.sum +++ b/staging/src/k8s.io/apiextensions-apiserver/go.sum @@ -127,6 +127,8 @@ github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/onsi/gomega v1.44.0 h1:eAiGl3Pw5jz5GQdDff0BcxYpAX1JxW8xD7mFUuwNfZQ= github.com/onsi/gomega v1.44.0/go.mod h1:e/C2HwaZ1DhvjzXXuFhcR7hY7Sh9pl7MmoWKEjzwcdA= +github.com/pandatix/go-abnf v0.5.5 h1:EXex5KkRQx4Dm6GnpAcQnIMBs9KiK0uf8p/wq+4t9PM= +github.com/pandatix/go-abnf v0.5.5/go.mod h1:AE0KD3/VNoCjGzc1gqetRKshbI4SYErXQ/8kPenR5mc= github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -136,8 +138,8 @@ github.com/prometheus/client_golang v1.25.0 h1:cDtW1TcFsMi1RJc8khwCKB4VhzwV04aKq github.com/prometheus/client_golang v1.25.0/go.mod h1:j2X/4/kaQHLPnmyEU8zjmkndo1T317VpCIQEml8LTKk= github.com/prometheus/client_model v0.6.3 h1:O0jaTVAYNxTHYInEPFJt5I3+sN8zqBtVMPTB1qyxiEo= github.com/prometheus/client_model v0.6.3/go.mod h1:gpN5P9S7Rr6Yr92PiQ+Ixvhf6JZEkF1dnxsYL2aPBEM= -github.com/prometheus/common v0.72.0 h1:tAYsE+sPJxIncDAobm4H5aQjmox9ZxEIIqPbiffa8G4= -github.com/prometheus/common v0.72.0/go.mod h1:77NWqAQ2tXT7BIK40qjJdw5Acrsrg1TlHAnsQi3i6mk= +github.com/prometheus/common v0.73.1 h1:SZWh/j4AvhCdhX8jvYSnxvPxhWlC9QZkjJ+tOrEr7kU= +github.com/prometheus/common v0.73.1/go.mod h1:I9aA/Ukz9s1olOobh8KtCkU7B1mQPQsB9UvQPsUQ1Dk= github.com/prometheus/procfs v0.22.0 h1:6q9+/JL9IKAPbCmBrv9n5O5Ty3NKnciV5X7YGw0oics= github.com/prometheus/procfs v0.22.0/go.mod h1:CvmFr/GVhIjIvWJZW3tgkODBQMRIf0EyWMQLHCHab58= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= diff --git a/staging/src/k8s.io/apiserver/go.mod b/staging/src/k8s.io/apiserver/go.mod index fd76dfe7e94..fc02dc9fff5 100644 --- a/staging/src/k8s.io/apiserver/go.mod +++ b/staging/src/k8s.io/apiserver/go.mod @@ -90,10 +90,11 @@ require ( github.com/jonboulle/clockwork v0.5.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/moby/spdystream v0.5.1 // indirect + github.com/pandatix/go-abnf v0.5.5 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.25.0 // indirect github.com/prometheus/client_model v0.6.3 // indirect - github.com/prometheus/common v0.72.0 // indirect + github.com/prometheus/common v0.73.1 // indirect github.com/prometheus/procfs v0.22.0 // indirect github.com/sirupsen/logrus v1.10.2 // indirect github.com/soheilhy/cmux v0.1.5 // indirect diff --git a/staging/src/k8s.io/apiserver/go.sum b/staging/src/k8s.io/apiserver/go.sum index 27ae2a1bbb6..1cb309d1f94 100644 --- a/staging/src/k8s.io/apiserver/go.sum +++ b/staging/src/k8s.io/apiserver/go.sum @@ -128,6 +128,8 @@ github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/onsi/gomega v1.44.0 h1:eAiGl3Pw5jz5GQdDff0BcxYpAX1JxW8xD7mFUuwNfZQ= github.com/onsi/gomega v1.44.0/go.mod h1:e/C2HwaZ1DhvjzXXuFhcR7hY7Sh9pl7MmoWKEjzwcdA= +github.com/pandatix/go-abnf v0.5.5 h1:EXex5KkRQx4Dm6GnpAcQnIMBs9KiK0uf8p/wq+4t9PM= +github.com/pandatix/go-abnf v0.5.5/go.mod h1:AE0KD3/VNoCjGzc1gqetRKshbI4SYErXQ/8kPenR5mc= github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -137,8 +139,8 @@ github.com/prometheus/client_golang v1.25.0 h1:cDtW1TcFsMi1RJc8khwCKB4VhzwV04aKq github.com/prometheus/client_golang v1.25.0/go.mod h1:j2X/4/kaQHLPnmyEU8zjmkndo1T317VpCIQEml8LTKk= github.com/prometheus/client_model v0.6.3 h1:O0jaTVAYNxTHYInEPFJt5I3+sN8zqBtVMPTB1qyxiEo= github.com/prometheus/client_model v0.6.3/go.mod h1:gpN5P9S7Rr6Yr92PiQ+Ixvhf6JZEkF1dnxsYL2aPBEM= -github.com/prometheus/common v0.72.0 h1:tAYsE+sPJxIncDAobm4H5aQjmox9ZxEIIqPbiffa8G4= -github.com/prometheus/common v0.72.0/go.mod h1:77NWqAQ2tXT7BIK40qjJdw5Acrsrg1TlHAnsQi3i6mk= +github.com/prometheus/common v0.73.1 h1:SZWh/j4AvhCdhX8jvYSnxvPxhWlC9QZkjJ+tOrEr7kU= +github.com/prometheus/common v0.73.1/go.mod h1:I9aA/Ukz9s1olOobh8KtCkU7B1mQPQsB9UvQPsUQ1Dk= github.com/prometheus/procfs v0.22.0 h1:6q9+/JL9IKAPbCmBrv9n5O5Ty3NKnciV5X7YGw0oics= github.com/prometheus/procfs v0.22.0/go.mod h1:CvmFr/GVhIjIvWJZW3tgkODBQMRIf0EyWMQLHCHab58= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= diff --git a/staging/src/k8s.io/cloud-provider/go.mod b/staging/src/k8s.io/cloud-provider/go.mod index d472972a794..f81e148e83f 100644 --- a/staging/src/k8s.io/cloud-provider/go.mod +++ b/staging/src/k8s.io/cloud-provider/go.mod @@ -58,10 +58,11 @@ require ( github.com/kylelemons/godebug v1.1.0 // indirect github.com/moby/term v0.5.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pandatix/go-abnf v0.5.5 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.25.0 // indirect github.com/prometheus/client_model v0.6.3 // indirect - github.com/prometheus/common v0.72.0 // indirect + github.com/prometheus/common v0.73.1 // indirect github.com/prometheus/procfs v0.22.0 // indirect github.com/x448/float16 v0.8.4 // indirect go.etcd.io/etcd/api/v3 v3.7.3 // indirect diff --git a/staging/src/k8s.io/cloud-provider/go.sum b/staging/src/k8s.io/cloud-provider/go.sum index 52917106124..912a5bbad63 100644 --- a/staging/src/k8s.io/cloud-provider/go.sum +++ b/staging/src/k8s.io/cloud-provider/go.sum @@ -120,6 +120,8 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRW github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/onsi/gomega v1.44.0 h1:eAiGl3Pw5jz5GQdDff0BcxYpAX1JxW8xD7mFUuwNfZQ= github.com/onsi/gomega v1.44.0/go.mod h1:e/C2HwaZ1DhvjzXXuFhcR7hY7Sh9pl7MmoWKEjzwcdA= +github.com/pandatix/go-abnf v0.5.5 h1:EXex5KkRQx4Dm6GnpAcQnIMBs9KiK0uf8p/wq+4t9PM= +github.com/pandatix/go-abnf v0.5.5/go.mod h1:AE0KD3/VNoCjGzc1gqetRKshbI4SYErXQ/8kPenR5mc= github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= @@ -128,8 +130,8 @@ github.com/prometheus/client_golang v1.25.0 h1:cDtW1TcFsMi1RJc8khwCKB4VhzwV04aKq github.com/prometheus/client_golang v1.25.0/go.mod h1:j2X/4/kaQHLPnmyEU8zjmkndo1T317VpCIQEml8LTKk= github.com/prometheus/client_model v0.6.3 h1:O0jaTVAYNxTHYInEPFJt5I3+sN8zqBtVMPTB1qyxiEo= github.com/prometheus/client_model v0.6.3/go.mod h1:gpN5P9S7Rr6Yr92PiQ+Ixvhf6JZEkF1dnxsYL2aPBEM= -github.com/prometheus/common v0.72.0 h1:tAYsE+sPJxIncDAobm4H5aQjmox9ZxEIIqPbiffa8G4= -github.com/prometheus/common v0.72.0/go.mod h1:77NWqAQ2tXT7BIK40qjJdw5Acrsrg1TlHAnsQi3i6mk= +github.com/prometheus/common v0.73.1 h1:SZWh/j4AvhCdhX8jvYSnxvPxhWlC9QZkjJ+tOrEr7kU= +github.com/prometheus/common v0.73.1/go.mod h1:I9aA/Ukz9s1olOobh8KtCkU7B1mQPQsB9UvQPsUQ1Dk= github.com/prometheus/procfs v0.22.0 h1:6q9+/JL9IKAPbCmBrv9n5O5Ty3NKnciV5X7YGw0oics= github.com/prometheus/procfs v0.22.0/go.mod h1:CvmFr/GVhIjIvWJZW3tgkODBQMRIf0EyWMQLHCHab58= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= diff --git a/staging/src/k8s.io/component-base/go.mod b/staging/src/k8s.io/component-base/go.mod index c0949cbf116..43ed08365ec 100644 --- a/staging/src/k8s.io/component-base/go.mod +++ b/staging/src/k8s.io/component-base/go.mod @@ -14,7 +14,7 @@ require ( github.com/moby/term v0.5.2 github.com/prometheus/client_golang v1.25.0 github.com/prometheus/client_model v0.6.3 - github.com/prometheus/common v0.72.0 + github.com/prometheus/common v0.73.1 github.com/prometheus/procfs v0.22.0 github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 @@ -56,6 +56,7 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pandatix/go-abnf v0.5.5 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect diff --git a/staging/src/k8s.io/component-base/go.sum b/staging/src/k8s.io/component-base/go.sum index f3247682e65..63c132156d9 100644 --- a/staging/src/k8s.io/component-base/go.sum +++ b/staging/src/k8s.io/component-base/go.sum @@ -91,6 +91,8 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRW github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/onsi/gomega v1.44.0 h1:eAiGl3Pw5jz5GQdDff0BcxYpAX1JxW8xD7mFUuwNfZQ= github.com/onsi/gomega v1.44.0/go.mod h1:e/C2HwaZ1DhvjzXXuFhcR7hY7Sh9pl7MmoWKEjzwcdA= +github.com/pandatix/go-abnf v0.5.5 h1:EXex5KkRQx4Dm6GnpAcQnIMBs9KiK0uf8p/wq+4t9PM= +github.com/pandatix/go-abnf v0.5.5/go.mod h1:AE0KD3/VNoCjGzc1gqetRKshbI4SYErXQ/8kPenR5mc= github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= @@ -99,8 +101,8 @@ github.com/prometheus/client_golang v1.25.0 h1:cDtW1TcFsMi1RJc8khwCKB4VhzwV04aKq github.com/prometheus/client_golang v1.25.0/go.mod h1:j2X/4/kaQHLPnmyEU8zjmkndo1T317VpCIQEml8LTKk= github.com/prometheus/client_model v0.6.3 h1:O0jaTVAYNxTHYInEPFJt5I3+sN8zqBtVMPTB1qyxiEo= github.com/prometheus/client_model v0.6.3/go.mod h1:gpN5P9S7Rr6Yr92PiQ+Ixvhf6JZEkF1dnxsYL2aPBEM= -github.com/prometheus/common v0.72.0 h1:tAYsE+sPJxIncDAobm4H5aQjmox9ZxEIIqPbiffa8G4= -github.com/prometheus/common v0.72.0/go.mod h1:77NWqAQ2tXT7BIK40qjJdw5Acrsrg1TlHAnsQi3i6mk= +github.com/prometheus/common v0.73.1 h1:SZWh/j4AvhCdhX8jvYSnxvPxhWlC9QZkjJ+tOrEr7kU= +github.com/prometheus/common v0.73.1/go.mod h1:I9aA/Ukz9s1olOobh8KtCkU7B1mQPQsB9UvQPsUQ1Dk= github.com/prometheus/procfs v0.22.0 h1:6q9+/JL9IKAPbCmBrv9n5O5Ty3NKnciV5X7YGw0oics= github.com/prometheus/procfs v0.22.0/go.mod h1:CvmFr/GVhIjIvWJZW3tgkODBQMRIf0EyWMQLHCHab58= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= diff --git a/staging/src/k8s.io/controller-manager/go.mod b/staging/src/k8s.io/controller-manager/go.mod index 4b3857121dd..c06d46ca81b 100644 --- a/staging/src/k8s.io/controller-manager/go.mod +++ b/staging/src/k8s.io/controller-manager/go.mod @@ -51,10 +51,11 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pandatix/go-abnf v0.5.5 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.25.0 // indirect github.com/prometheus/client_model v0.6.3 // indirect - github.com/prometheus/common v0.72.0 // indirect + github.com/prometheus/common v0.73.1 // indirect github.com/prometheus/procfs v0.22.0 // indirect github.com/spf13/cobra v1.10.2 // indirect github.com/x448/float16 v0.8.4 // indirect diff --git a/staging/src/k8s.io/controller-manager/go.sum b/staging/src/k8s.io/controller-manager/go.sum index dd71958139d..b91a0c331c5 100644 --- a/staging/src/k8s.io/controller-manager/go.sum +++ b/staging/src/k8s.io/controller-manager/go.sum @@ -116,6 +116,8 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRW github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/onsi/gomega v1.44.0 h1:eAiGl3Pw5jz5GQdDff0BcxYpAX1JxW8xD7mFUuwNfZQ= github.com/onsi/gomega v1.44.0/go.mod h1:e/C2HwaZ1DhvjzXXuFhcR7hY7Sh9pl7MmoWKEjzwcdA= +github.com/pandatix/go-abnf v0.5.5 h1:EXex5KkRQx4Dm6GnpAcQnIMBs9KiK0uf8p/wq+4t9PM= +github.com/pandatix/go-abnf v0.5.5/go.mod h1:AE0KD3/VNoCjGzc1gqetRKshbI4SYErXQ/8kPenR5mc= github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= @@ -124,8 +126,8 @@ github.com/prometheus/client_golang v1.25.0 h1:cDtW1TcFsMi1RJc8khwCKB4VhzwV04aKq github.com/prometheus/client_golang v1.25.0/go.mod h1:j2X/4/kaQHLPnmyEU8zjmkndo1T317VpCIQEml8LTKk= github.com/prometheus/client_model v0.6.3 h1:O0jaTVAYNxTHYInEPFJt5I3+sN8zqBtVMPTB1qyxiEo= github.com/prometheus/client_model v0.6.3/go.mod h1:gpN5P9S7Rr6Yr92PiQ+Ixvhf6JZEkF1dnxsYL2aPBEM= -github.com/prometheus/common v0.72.0 h1:tAYsE+sPJxIncDAobm4H5aQjmox9ZxEIIqPbiffa8G4= -github.com/prometheus/common v0.72.0/go.mod h1:77NWqAQ2tXT7BIK40qjJdw5Acrsrg1TlHAnsQi3i6mk= +github.com/prometheus/common v0.73.1 h1:SZWh/j4AvhCdhX8jvYSnxvPxhWlC9QZkjJ+tOrEr7kU= +github.com/prometheus/common v0.73.1/go.mod h1:I9aA/Ukz9s1olOobh8KtCkU7B1mQPQsB9UvQPsUQ1Dk= github.com/prometheus/procfs v0.22.0 h1:6q9+/JL9IKAPbCmBrv9n5O5Ty3NKnciV5X7YGw0oics= github.com/prometheus/procfs v0.22.0/go.mod h1:CvmFr/GVhIjIvWJZW3tgkODBQMRIf0EyWMQLHCHab58= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= diff --git a/staging/src/k8s.io/cri-client/go.sum b/staging/src/k8s.io/cri-client/go.sum index e5e3d193f10..ec7407b903f 100644 --- a/staging/src/k8s.io/cri-client/go.sum +++ b/staging/src/k8s.io/cri-client/go.sum @@ -46,11 +46,12 @@ github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3x github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/pandatix/go-abnf v0.5.5/go.mod h1:AE0KD3/VNoCjGzc1gqetRKshbI4SYErXQ/8kPenR5mc= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.25.0/go.mod h1:j2X/4/kaQHLPnmyEU8zjmkndo1T317VpCIQEml8LTKk= github.com/prometheus/client_model v0.6.3/go.mod h1:gpN5P9S7Rr6Yr92PiQ+Ixvhf6JZEkF1dnxsYL2aPBEM= -github.com/prometheus/common v0.72.0/go.mod h1:77NWqAQ2tXT7BIK40qjJdw5Acrsrg1TlHAnsQi3i6mk= +github.com/prometheus/common v0.73.1/go.mod h1:I9aA/Ukz9s1olOobh8KtCkU7B1mQPQsB9UvQPsUQ1Dk= github.com/prometheus/procfs v0.22.0/go.mod h1:CvmFr/GVhIjIvWJZW3tgkODBQMRIf0EyWMQLHCHab58= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= diff --git a/staging/src/k8s.io/dynamic-resource-allocation/go.mod b/staging/src/k8s.io/dynamic-resource-allocation/go.mod index 41f71022261..ed3e1937576 100644 --- a/staging/src/k8s.io/dynamic-resource-allocation/go.mod +++ b/staging/src/k8s.io/dynamic-resource-allocation/go.mod @@ -46,10 +46,11 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pandatix/go-abnf v0.5.5 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.25.0 // indirect github.com/prometheus/client_model v0.6.3 // indirect - github.com/prometheus/common v0.72.0 // indirect + github.com/prometheus/common v0.73.1 // indirect github.com/prometheus/procfs v0.22.0 // indirect github.com/spf13/cobra v1.10.2 // indirect github.com/spf13/pflag v1.0.10 // indirect diff --git a/staging/src/k8s.io/dynamic-resource-allocation/go.sum b/staging/src/k8s.io/dynamic-resource-allocation/go.sum index a76b4868190..bdc8fea5b08 100644 --- a/staging/src/k8s.io/dynamic-resource-allocation/go.sum +++ b/staging/src/k8s.io/dynamic-resource-allocation/go.sum @@ -93,6 +93,8 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRW github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/onsi/gomega v1.44.0 h1:eAiGl3Pw5jz5GQdDff0BcxYpAX1JxW8xD7mFUuwNfZQ= github.com/onsi/gomega v1.44.0/go.mod h1:e/C2HwaZ1DhvjzXXuFhcR7hY7Sh9pl7MmoWKEjzwcdA= +github.com/pandatix/go-abnf v0.5.5 h1:EXex5KkRQx4Dm6GnpAcQnIMBs9KiK0uf8p/wq+4t9PM= +github.com/pandatix/go-abnf v0.5.5/go.mod h1:AE0KD3/VNoCjGzc1gqetRKshbI4SYErXQ/8kPenR5mc= github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= @@ -101,8 +103,8 @@ github.com/prometheus/client_golang v1.25.0 h1:cDtW1TcFsMi1RJc8khwCKB4VhzwV04aKq github.com/prometheus/client_golang v1.25.0/go.mod h1:j2X/4/kaQHLPnmyEU8zjmkndo1T317VpCIQEml8LTKk= github.com/prometheus/client_model v0.6.3 h1:O0jaTVAYNxTHYInEPFJt5I3+sN8zqBtVMPTB1qyxiEo= github.com/prometheus/client_model v0.6.3/go.mod h1:gpN5P9S7Rr6Yr92PiQ+Ixvhf6JZEkF1dnxsYL2aPBEM= -github.com/prometheus/common v0.72.0 h1:tAYsE+sPJxIncDAobm4H5aQjmox9ZxEIIqPbiffa8G4= -github.com/prometheus/common v0.72.0/go.mod h1:77NWqAQ2tXT7BIK40qjJdw5Acrsrg1TlHAnsQi3i6mk= +github.com/prometheus/common v0.73.1 h1:SZWh/j4AvhCdhX8jvYSnxvPxhWlC9QZkjJ+tOrEr7kU= +github.com/prometheus/common v0.73.1/go.mod h1:I9aA/Ukz9s1olOobh8KtCkU7B1mQPQsB9UvQPsUQ1Dk= github.com/prometheus/procfs v0.22.0 h1:6q9+/JL9IKAPbCmBrv9n5O5Ty3NKnciV5X7YGw0oics= github.com/prometheus/procfs v0.22.0/go.mod h1:CvmFr/GVhIjIvWJZW3tgkODBQMRIf0EyWMQLHCHab58= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= diff --git a/staging/src/k8s.io/endpointslice/go.mod b/staging/src/k8s.io/endpointslice/go.mod index 5d87a82a678..721e051a2ae 100644 --- a/staging/src/k8s.io/endpointslice/go.mod +++ b/staging/src/k8s.io/endpointslice/go.mod @@ -34,10 +34,11 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pandatix/go-abnf v0.5.5 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.25.0 // indirect github.com/prometheus/client_model v0.6.3 // indirect - github.com/prometheus/common v0.72.0 // indirect + github.com/prometheus/common v0.73.1 // indirect github.com/prometheus/procfs v0.22.0 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/x448/float16 v0.8.4 // indirect diff --git a/staging/src/k8s.io/endpointslice/go.sum b/staging/src/k8s.io/endpointslice/go.sum index db37fcc4bd5..85549f304ce 100644 --- a/staging/src/k8s.io/endpointslice/go.sum +++ b/staging/src/k8s.io/endpointslice/go.sum @@ -66,6 +66,8 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRW github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/onsi/gomega v1.44.0 h1:eAiGl3Pw5jz5GQdDff0BcxYpAX1JxW8xD7mFUuwNfZQ= github.com/onsi/gomega v1.44.0/go.mod h1:e/C2HwaZ1DhvjzXXuFhcR7hY7Sh9pl7MmoWKEjzwcdA= +github.com/pandatix/go-abnf v0.5.5 h1:EXex5KkRQx4Dm6GnpAcQnIMBs9KiK0uf8p/wq+4t9PM= +github.com/pandatix/go-abnf v0.5.5/go.mod h1:AE0KD3/VNoCjGzc1gqetRKshbI4SYErXQ/8kPenR5mc= github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -73,8 +75,8 @@ github.com/prometheus/client_golang v1.25.0 h1:cDtW1TcFsMi1RJc8khwCKB4VhzwV04aKq github.com/prometheus/client_golang v1.25.0/go.mod h1:j2X/4/kaQHLPnmyEU8zjmkndo1T317VpCIQEml8LTKk= github.com/prometheus/client_model v0.6.3 h1:O0jaTVAYNxTHYInEPFJt5I3+sN8zqBtVMPTB1qyxiEo= github.com/prometheus/client_model v0.6.3/go.mod h1:gpN5P9S7Rr6Yr92PiQ+Ixvhf6JZEkF1dnxsYL2aPBEM= -github.com/prometheus/common v0.72.0 h1:tAYsE+sPJxIncDAobm4H5aQjmox9ZxEIIqPbiffa8G4= -github.com/prometheus/common v0.72.0/go.mod h1:77NWqAQ2tXT7BIK40qjJdw5Acrsrg1TlHAnsQi3i6mk= +github.com/prometheus/common v0.73.1 h1:SZWh/j4AvhCdhX8jvYSnxvPxhWlC9QZkjJ+tOrEr7kU= +github.com/prometheus/common v0.73.1/go.mod h1:I9aA/Ukz9s1olOobh8KtCkU7B1mQPQsB9UvQPsUQ1Dk= github.com/prometheus/procfs v0.22.0 h1:6q9+/JL9IKAPbCmBrv9n5O5Ty3NKnciV5X7YGw0oics= github.com/prometheus/procfs v0.22.0/go.mod h1:CvmFr/GVhIjIvWJZW3tgkODBQMRIf0EyWMQLHCHab58= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= diff --git a/staging/src/k8s.io/kube-aggregator/go.mod b/staging/src/k8s.io/kube-aggregator/go.mod index 17a66bcaef5..c988f49a65e 100644 --- a/staging/src/k8s.io/kube-aggregator/go.mod +++ b/staging/src/k8s.io/kube-aggregator/go.mod @@ -64,10 +64,11 @@ require ( github.com/moby/spdystream v0.5.1 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect + github.com/pandatix/go-abnf v0.5.5 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.25.0 // indirect github.com/prometheus/client_model v0.6.3 // indirect - github.com/prometheus/common v0.72.0 // indirect + github.com/prometheus/common v0.73.1 // indirect github.com/prometheus/procfs v0.22.0 // indirect github.com/x448/float16 v0.8.4 // indirect go.etcd.io/etcd/api/v3 v3.7.3 // indirect diff --git a/staging/src/k8s.io/kube-aggregator/go.sum b/staging/src/k8s.io/kube-aggregator/go.sum index 60582e26fdf..c1ca619a86e 100644 --- a/staging/src/k8s.io/kube-aggregator/go.sum +++ b/staging/src/k8s.io/kube-aggregator/go.sum @@ -120,6 +120,8 @@ github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/onsi/gomega v1.44.0 h1:eAiGl3Pw5jz5GQdDff0BcxYpAX1JxW8xD7mFUuwNfZQ= github.com/onsi/gomega v1.44.0/go.mod h1:e/C2HwaZ1DhvjzXXuFhcR7hY7Sh9pl7MmoWKEjzwcdA= +github.com/pandatix/go-abnf v0.5.5 h1:EXex5KkRQx4Dm6GnpAcQnIMBs9KiK0uf8p/wq+4t9PM= +github.com/pandatix/go-abnf v0.5.5/go.mod h1:AE0KD3/VNoCjGzc1gqetRKshbI4SYErXQ/8kPenR5mc= github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= @@ -128,8 +130,8 @@ github.com/prometheus/client_golang v1.25.0 h1:cDtW1TcFsMi1RJc8khwCKB4VhzwV04aKq github.com/prometheus/client_golang v1.25.0/go.mod h1:j2X/4/kaQHLPnmyEU8zjmkndo1T317VpCIQEml8LTKk= github.com/prometheus/client_model v0.6.3 h1:O0jaTVAYNxTHYInEPFJt5I3+sN8zqBtVMPTB1qyxiEo= github.com/prometheus/client_model v0.6.3/go.mod h1:gpN5P9S7Rr6Yr92PiQ+Ixvhf6JZEkF1dnxsYL2aPBEM= -github.com/prometheus/common v0.72.0 h1:tAYsE+sPJxIncDAobm4H5aQjmox9ZxEIIqPbiffa8G4= -github.com/prometheus/common v0.72.0/go.mod h1:77NWqAQ2tXT7BIK40qjJdw5Acrsrg1TlHAnsQi3i6mk= +github.com/prometheus/common v0.73.1 h1:SZWh/j4AvhCdhX8jvYSnxvPxhWlC9QZkjJ+tOrEr7kU= +github.com/prometheus/common v0.73.1/go.mod h1:I9aA/Ukz9s1olOobh8KtCkU7B1mQPQsB9UvQPsUQ1Dk= github.com/prometheus/procfs v0.22.0 h1:6q9+/JL9IKAPbCmBrv9n5O5Ty3NKnciV5X7YGw0oics= github.com/prometheus/procfs v0.22.0/go.mod h1:CvmFr/GVhIjIvWJZW3tgkODBQMRIf0EyWMQLHCHab58= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= diff --git a/staging/src/k8s.io/kube-controller-manager/go.sum b/staging/src/k8s.io/kube-controller-manager/go.sum index 06ade90c02e..a14212d1f92 100644 --- a/staging/src/k8s.io/kube-controller-manager/go.sum +++ b/staging/src/k8s.io/kube-controller-manager/go.sum @@ -41,11 +41,12 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/onsi/gomega v1.44.0 h1:eAiGl3Pw5jz5GQdDff0BcxYpAX1JxW8xD7mFUuwNfZQ= github.com/onsi/gomega v1.44.0/go.mod h1:e/C2HwaZ1DhvjzXXuFhcR7hY7Sh9pl7MmoWKEjzwcdA= +github.com/pandatix/go-abnf v0.5.5/go.mod h1:AE0KD3/VNoCjGzc1gqetRKshbI4SYErXQ/8kPenR5mc= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.25.0/go.mod h1:j2X/4/kaQHLPnmyEU8zjmkndo1T317VpCIQEml8LTKk= github.com/prometheus/client_model v0.6.3/go.mod h1:gpN5P9S7Rr6Yr92PiQ+Ixvhf6JZEkF1dnxsYL2aPBEM= -github.com/prometheus/common v0.72.0/go.mod h1:77NWqAQ2tXT7BIK40qjJdw5Acrsrg1TlHAnsQi3i6mk= +github.com/prometheus/common v0.73.1/go.mod h1:I9aA/Ukz9s1olOobh8KtCkU7B1mQPQsB9UvQPsUQ1Dk= github.com/prometheus/procfs v0.22.0/go.mod h1:CvmFr/GVhIjIvWJZW3tgkODBQMRIf0EyWMQLHCHab58= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= diff --git a/staging/src/k8s.io/kube-proxy/go.sum b/staging/src/k8s.io/kube-proxy/go.sum index 19d5077f329..8fe1aa56c03 100644 --- a/staging/src/k8s.io/kube-proxy/go.sum +++ b/staging/src/k8s.io/kube-proxy/go.sum @@ -38,14 +38,16 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/onsi/gomega v1.44.0 h1:eAiGl3Pw5jz5GQdDff0BcxYpAX1JxW8xD7mFUuwNfZQ= github.com/onsi/gomega v1.44.0/go.mod h1:e/C2HwaZ1DhvjzXXuFhcR7hY7Sh9pl7MmoWKEjzwcdA= +github.com/pandatix/go-abnf v0.5.5 h1:EXex5KkRQx4Dm6GnpAcQnIMBs9KiK0uf8p/wq+4t9PM= +github.com/pandatix/go-abnf v0.5.5/go.mod h1:AE0KD3/VNoCjGzc1gqetRKshbI4SYErXQ/8kPenR5mc= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.25.0 h1:cDtW1TcFsMi1RJc8khwCKB4VhzwV04aKqbYtNOXuiK0= github.com/prometheus/client_golang v1.25.0/go.mod h1:j2X/4/kaQHLPnmyEU8zjmkndo1T317VpCIQEml8LTKk= github.com/prometheus/client_model v0.6.3 h1:O0jaTVAYNxTHYInEPFJt5I3+sN8zqBtVMPTB1qyxiEo= github.com/prometheus/client_model v0.6.3/go.mod h1:gpN5P9S7Rr6Yr92PiQ+Ixvhf6JZEkF1dnxsYL2aPBEM= -github.com/prometheus/common v0.72.0 h1:tAYsE+sPJxIncDAobm4H5aQjmox9ZxEIIqPbiffa8G4= -github.com/prometheus/common v0.72.0/go.mod h1:77NWqAQ2tXT7BIK40qjJdw5Acrsrg1TlHAnsQi3i6mk= +github.com/prometheus/common v0.73.1 h1:SZWh/j4AvhCdhX8jvYSnxvPxhWlC9QZkjJ+tOrEr7kU= +github.com/prometheus/common v0.73.1/go.mod h1:I9aA/Ukz9s1olOobh8KtCkU7B1mQPQsB9UvQPsUQ1Dk= github.com/prometheus/procfs v0.22.0 h1:6q9+/JL9IKAPbCmBrv9n5O5Ty3NKnciV5X7YGw0oics= github.com/prometheus/procfs v0.22.0/go.mod h1:CvmFr/GVhIjIvWJZW3tgkODBQMRIf0EyWMQLHCHab58= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= diff --git a/staging/src/k8s.io/kube-scheduler/go.sum b/staging/src/k8s.io/kube-scheduler/go.sum index ae5508bb3d3..694674163c1 100644 --- a/staging/src/k8s.io/kube-scheduler/go.sum +++ b/staging/src/k8s.io/kube-scheduler/go.sum @@ -55,12 +55,13 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/onsi/gomega v1.44.0 h1:eAiGl3Pw5jz5GQdDff0BcxYpAX1JxW8xD7mFUuwNfZQ= github.com/onsi/gomega v1.44.0/go.mod h1:e/C2HwaZ1DhvjzXXuFhcR7hY7Sh9pl7MmoWKEjzwcdA= +github.com/pandatix/go-abnf v0.5.5/go.mod h1:AE0KD3/VNoCjGzc1gqetRKshbI4SYErXQ/8kPenR5mc= github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.25.0/go.mod h1:j2X/4/kaQHLPnmyEU8zjmkndo1T317VpCIQEml8LTKk= github.com/prometheus/client_model v0.6.3/go.mod h1:gpN5P9S7Rr6Yr92PiQ+Ixvhf6JZEkF1dnxsYL2aPBEM= -github.com/prometheus/common v0.72.0/go.mod h1:77NWqAQ2tXT7BIK40qjJdw5Acrsrg1TlHAnsQi3i6mk= +github.com/prometheus/common v0.73.1/go.mod h1:I9aA/Ukz9s1olOobh8KtCkU7B1mQPQsB9UvQPsUQ1Dk= github.com/prometheus/procfs v0.22.0/go.mod h1:CvmFr/GVhIjIvWJZW3tgkODBQMRIf0EyWMQLHCHab58= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= diff --git a/staging/src/k8s.io/kubectl/go.sum b/staging/src/k8s.io/kubectl/go.sum index 79e70e4ff0d..e05c391875d 100644 --- a/staging/src/k8s.io/kubectl/go.sum +++ b/staging/src/k8s.io/kubectl/go.sum @@ -118,6 +118,7 @@ github.com/onsi/gomega v1.44.0 h1:eAiGl3Pw5jz5GQdDff0BcxYpAX1JxW8xD7mFUuwNfZQ= github.com/onsi/gomega v1.44.0/go.mod h1:e/C2HwaZ1DhvjzXXuFhcR7hY7Sh9pl7MmoWKEjzwcdA= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/pandatix/go-abnf v0.5.5/go.mod h1:AE0KD3/VNoCjGzc1gqetRKshbI4SYErXQ/8kPenR5mc= github.com/peterbourgon/diskv/v3 v3.0.1 h1:x06SQA46+PKIUftmEujdwSEpIx8kR+M9eLYsUxeYveU= github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -125,7 +126,7 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.25.0/go.mod h1:j2X/4/kaQHLPnmyEU8zjmkndo1T317VpCIQEml8LTKk= github.com/prometheus/client_model v0.6.3/go.mod h1:gpN5P9S7Rr6Yr92PiQ+Ixvhf6JZEkF1dnxsYL2aPBEM= -github.com/prometheus/common v0.72.0/go.mod h1:77NWqAQ2tXT7BIK40qjJdw5Acrsrg1TlHAnsQi3i6mk= +github.com/prometheus/common v0.73.1/go.mod h1:I9aA/Ukz9s1olOobh8KtCkU7B1mQPQsB9UvQPsUQ1Dk= github.com/prometheus/procfs v0.22.0/go.mod h1:CvmFr/GVhIjIvWJZW3tgkODBQMRIf0EyWMQLHCHab58= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= diff --git a/staging/src/k8s.io/kubelet/go.sum b/staging/src/k8s.io/kubelet/go.sum index 08b0b7e4628..c08ec66ca08 100644 --- a/staging/src/k8s.io/kubelet/go.sum +++ b/staging/src/k8s.io/kubelet/go.sum @@ -50,6 +50,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/onsi/gomega v1.44.0 h1:eAiGl3Pw5jz5GQdDff0BcxYpAX1JxW8xD7mFUuwNfZQ= github.com/onsi/gomega v1.44.0/go.mod h1:e/C2HwaZ1DhvjzXXuFhcR7hY7Sh9pl7MmoWKEjzwcdA= +github.com/pandatix/go-abnf v0.5.5 h1:EXex5KkRQx4Dm6GnpAcQnIMBs9KiK0uf8p/wq+4t9PM= +github.com/pandatix/go-abnf v0.5.5/go.mod h1:AE0KD3/VNoCjGzc1gqetRKshbI4SYErXQ/8kPenR5mc= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -57,8 +59,8 @@ github.com/prometheus/client_golang v1.25.0 h1:cDtW1TcFsMi1RJc8khwCKB4VhzwV04aKq github.com/prometheus/client_golang v1.25.0/go.mod h1:j2X/4/kaQHLPnmyEU8zjmkndo1T317VpCIQEml8LTKk= github.com/prometheus/client_model v0.6.3 h1:O0jaTVAYNxTHYInEPFJt5I3+sN8zqBtVMPTB1qyxiEo= github.com/prometheus/client_model v0.6.3/go.mod h1:gpN5P9S7Rr6Yr92PiQ+Ixvhf6JZEkF1dnxsYL2aPBEM= -github.com/prometheus/common v0.72.0 h1:tAYsE+sPJxIncDAobm4H5aQjmox9ZxEIIqPbiffa8G4= -github.com/prometheus/common v0.72.0/go.mod h1:77NWqAQ2tXT7BIK40qjJdw5Acrsrg1TlHAnsQi3i6mk= +github.com/prometheus/common v0.73.1 h1:SZWh/j4AvhCdhX8jvYSnxvPxhWlC9QZkjJ+tOrEr7kU= +github.com/prometheus/common v0.73.1/go.mod h1:I9aA/Ukz9s1olOobh8KtCkU7B1mQPQsB9UvQPsUQ1Dk= github.com/prometheus/procfs v0.22.0 h1:6q9+/JL9IKAPbCmBrv9n5O5Ty3NKnciV5X7YGw0oics= github.com/prometheus/procfs v0.22.0/go.mod h1:CvmFr/GVhIjIvWJZW3tgkODBQMRIf0EyWMQLHCHab58= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= diff --git a/staging/src/k8s.io/pod-security-admission/go.mod b/staging/src/k8s.io/pod-security-admission/go.mod index bdc7850a337..7f17dbffbda 100644 --- a/staging/src/k8s.io/pod-security-admission/go.mod +++ b/staging/src/k8s.io/pod-security-admission/go.mod @@ -54,10 +54,11 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pandatix/go-abnf v0.5.5 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.25.0 // indirect github.com/prometheus/client_model v0.6.3 // indirect - github.com/prometheus/common v0.72.0 // indirect + github.com/prometheus/common v0.73.1 // indirect github.com/prometheus/procfs v0.22.0 // indirect github.com/x448/float16 v0.8.4 // indirect go.etcd.io/etcd/api/v3 v3.7.3 // indirect diff --git a/staging/src/k8s.io/pod-security-admission/go.sum b/staging/src/k8s.io/pod-security-admission/go.sum index dd71958139d..b91a0c331c5 100644 --- a/staging/src/k8s.io/pod-security-admission/go.sum +++ b/staging/src/k8s.io/pod-security-admission/go.sum @@ -116,6 +116,8 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRW github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/onsi/gomega v1.44.0 h1:eAiGl3Pw5jz5GQdDff0BcxYpAX1JxW8xD7mFUuwNfZQ= github.com/onsi/gomega v1.44.0/go.mod h1:e/C2HwaZ1DhvjzXXuFhcR7hY7Sh9pl7MmoWKEjzwcdA= +github.com/pandatix/go-abnf v0.5.5 h1:EXex5KkRQx4Dm6GnpAcQnIMBs9KiK0uf8p/wq+4t9PM= +github.com/pandatix/go-abnf v0.5.5/go.mod h1:AE0KD3/VNoCjGzc1gqetRKshbI4SYErXQ/8kPenR5mc= github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= @@ -124,8 +126,8 @@ github.com/prometheus/client_golang v1.25.0 h1:cDtW1TcFsMi1RJc8khwCKB4VhzwV04aKq github.com/prometheus/client_golang v1.25.0/go.mod h1:j2X/4/kaQHLPnmyEU8zjmkndo1T317VpCIQEml8LTKk= github.com/prometheus/client_model v0.6.3 h1:O0jaTVAYNxTHYInEPFJt5I3+sN8zqBtVMPTB1qyxiEo= github.com/prometheus/client_model v0.6.3/go.mod h1:gpN5P9S7Rr6Yr92PiQ+Ixvhf6JZEkF1dnxsYL2aPBEM= -github.com/prometheus/common v0.72.0 h1:tAYsE+sPJxIncDAobm4H5aQjmox9ZxEIIqPbiffa8G4= -github.com/prometheus/common v0.72.0/go.mod h1:77NWqAQ2tXT7BIK40qjJdw5Acrsrg1TlHAnsQi3i6mk= +github.com/prometheus/common v0.73.1 h1:SZWh/j4AvhCdhX8jvYSnxvPxhWlC9QZkjJ+tOrEr7kU= +github.com/prometheus/common v0.73.1/go.mod h1:I9aA/Ukz9s1olOobh8KtCkU7B1mQPQsB9UvQPsUQ1Dk= github.com/prometheus/procfs v0.22.0 h1:6q9+/JL9IKAPbCmBrv9n5O5Ty3NKnciV5X7YGw0oics= github.com/prometheus/procfs v0.22.0/go.mod h1:CvmFr/GVhIjIvWJZW3tgkODBQMRIf0EyWMQLHCHab58= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= diff --git a/staging/src/k8s.io/sample-apiserver/go.mod b/staging/src/k8s.io/sample-apiserver/go.mod index 5286fdeb3ec..36c24c0ec70 100644 --- a/staging/src/k8s.io/sample-apiserver/go.mod +++ b/staging/src/k8s.io/sample-apiserver/go.mod @@ -53,10 +53,11 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pandatix/go-abnf v0.5.5 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.25.0 // indirect github.com/prometheus/client_model v0.6.3 // indirect - github.com/prometheus/common v0.72.0 // indirect + github.com/prometheus/common v0.73.1 // indirect github.com/prometheus/procfs v0.22.0 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/x448/float16 v0.8.4 // indirect diff --git a/staging/src/k8s.io/sample-apiserver/go.sum b/staging/src/k8s.io/sample-apiserver/go.sum index 5d3ba231466..4ec81ee9e9c 100644 --- a/staging/src/k8s.io/sample-apiserver/go.sum +++ b/staging/src/k8s.io/sample-apiserver/go.sum @@ -117,6 +117,8 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRW github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/onsi/gomega v1.44.0 h1:eAiGl3Pw5jz5GQdDff0BcxYpAX1JxW8xD7mFUuwNfZQ= github.com/onsi/gomega v1.44.0/go.mod h1:e/C2HwaZ1DhvjzXXuFhcR7hY7Sh9pl7MmoWKEjzwcdA= +github.com/pandatix/go-abnf v0.5.5 h1:EXex5KkRQx4Dm6GnpAcQnIMBs9KiK0uf8p/wq+4t9PM= +github.com/pandatix/go-abnf v0.5.5/go.mod h1:AE0KD3/VNoCjGzc1gqetRKshbI4SYErXQ/8kPenR5mc= github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= @@ -125,8 +127,8 @@ github.com/prometheus/client_golang v1.25.0 h1:cDtW1TcFsMi1RJc8khwCKB4VhzwV04aKq github.com/prometheus/client_golang v1.25.0/go.mod h1:j2X/4/kaQHLPnmyEU8zjmkndo1T317VpCIQEml8LTKk= github.com/prometheus/client_model v0.6.3 h1:O0jaTVAYNxTHYInEPFJt5I3+sN8zqBtVMPTB1qyxiEo= github.com/prometheus/client_model v0.6.3/go.mod h1:gpN5P9S7Rr6Yr92PiQ+Ixvhf6JZEkF1dnxsYL2aPBEM= -github.com/prometheus/common v0.72.0 h1:tAYsE+sPJxIncDAobm4H5aQjmox9ZxEIIqPbiffa8G4= -github.com/prometheus/common v0.72.0/go.mod h1:77NWqAQ2tXT7BIK40qjJdw5Acrsrg1TlHAnsQi3i6mk= +github.com/prometheus/common v0.73.1 h1:SZWh/j4AvhCdhX8jvYSnxvPxhWlC9QZkjJ+tOrEr7kU= +github.com/prometheus/common v0.73.1/go.mod h1:I9aA/Ukz9s1olOobh8KtCkU7B1mQPQsB9UvQPsUQ1Dk= github.com/prometheus/procfs v0.22.0 h1:6q9+/JL9IKAPbCmBrv9n5O5Ty3NKnciV5X7YGw0oics= github.com/prometheus/procfs v0.22.0/go.mod h1:CvmFr/GVhIjIvWJZW3tgkODBQMRIf0EyWMQLHCHab58= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= diff --git a/vendor/github.com/pandatix/go-abnf/.gitignore b/vendor/github.com/pandatix/go-abnf/.gitignore new file mode 100644 index 00000000000..49262209720 --- /dev/null +++ b/vendor/github.com/pandatix/go-abnf/.gitignore @@ -0,0 +1,17 @@ +# IDE files +.vscode/* + +# Benchmark files +*.pprof +*.golden + +# Fuzzing files +testdata/fuzz/* + +# Generated files from pap +*.mmd +*.svg + +# Documents +*.pdf +*.bkp diff --git a/vendor/github.com/pandatix/go-abnf/.goreleaser.yaml b/vendor/github.com/pandatix/go-abnf/.goreleaser.yaml new file mode 100644 index 00000000000..abd2c454223 --- /dev/null +++ b/vendor/github.com/pandatix/go-abnf/.goreleaser.yaml @@ -0,0 +1,37 @@ +before: + hooks: + - go mod tidy + +gomod: + proxy: true + +builds: + - id: pap + main: ./cmd/pap + binary: pap + env: + - CGO_ENABLED=0 + goos: + - linux + goarch: + - amd64 + +archives: + - id: pap + builds: + - pap + +changelog: + sort: asc + filters: + exclude: + - '^docs:' + - '^test:' + +source: + enabled: true + +checksum: {} + +sboms: + - artifacts: source diff --git a/vendor/github.com/pandatix/go-abnf/CODE_OF_CONDUCT.md b/vendor/github.com/pandatix/go-abnf/CODE_OF_CONDUCT.md new file mode 100644 index 00000000000..516c72dc8b5 --- /dev/null +++ b/vendor/github.com/pandatix/go-abnf/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders 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, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +lucastesson@protonmail.com. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/vendor/github.com/pandatix/go-abnf/CONTRIBUTING.md b/vendor/github.com/pandatix/go-abnf/CONTRIBUTING.md new file mode 100644 index 00000000000..677046c7798 --- /dev/null +++ b/vendor/github.com/pandatix/go-abnf/CONTRIBUTING.md @@ -0,0 +1,16 @@ +# Contributing + +Open-Source means we welcome all changes and ideas any time of day and nights, through issues and pull requests. + +## How to contribute + +As the community around this project is small enough, we set the following contributing guidelines: + - if necessary, [open an issue](https://github.com/pandatix/go-abnf/issues/new) first + - fork the repository + - clone, commit, and when feeling confident enough [create a pull request](https://github.com/pandatix/go-abnf/compare) with the issue(s) it solves. + - check your notifications for comments, suggestions, and maybe an approval + +## Quality + +This implementation focuses on the IETF RFCs, security and performances. +We expect your contribution to provide a minimum quality with unitary tests when applicable. diff --git a/vendor/github.com/pandatix/go-abnf/LICENSE b/vendor/github.com/pandatix/go-abnf/LICENSE new file mode 100644 index 00000000000..89527daea5a --- /dev/null +++ b/vendor/github.com/pandatix/go-abnf/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Lucas TESSON - PandatiX + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/pandatix/go-abnf/README.md b/vendor/github.com/pandatix/go-abnf/README.md new file mode 100644 index 00000000000..902f0812720 --- /dev/null +++ b/vendor/github.com/pandatix/go-abnf/README.md @@ -0,0 +1,156 @@ +
+

Go-ABNF

+ reference + go report + Coverage Status +
+ License + CI + CodeQL +
+ OpenSSF Scoreboard + OpenSSF Best Practices Summary +
+ +A dependency-free Go module for Augmented Backus-Naur Form (ABNF). It parses an ABNF grammar into a data structure and then lets you *recognise*, *parse*, *generate*, *compile to regex*, *visualise*, and even *generate a standalone Go parser* from it. It implements [RFC 5234](https://www.rfc-editor.org/rfc/rfc5234) and [RFC 7405](https://www.rfc-editor.org/rfc/rfc7405), with Errata [2968](https://errata.rfc-editor.org/eid2968/) and [3076](https://errata.rfc-editor.org/eid3076/). + +> [!NOTE] +> **Intended use.** Go-ABNF is built first and foremost as a **testing oracle**. +> Because it can turn one grammar into several independent representations (recognizer, parse forest, regex, generator, transition graph), it is well suited to **differential testing and fuzzing**: generate inputs from a grammar, feed them to your own implementation, and cross-check the verdicts against this library - or cross-check the library's own views against each other. It favors correctness and generality (ambiguous and left-recursive grammars, Unicode) over being the fastest production parser. + +## Capabilities + +- Parse ABNF into a manipulable `*Grammar` (with cycle / DAG detection). +- Recognize input against a grammar - ambiguous and left-recursive grammars included. +- Build a full **parse forest** (SPPF) or **binary-subtree set** (BSR): count trees, detect ambiguity, extract a tree. +- Compile a grammar to a **regular expression** (precedence-aware, size-bounded). +- **Generate** inputs (random walk), a minimal covering test set, or structured ASTs - for fuzzing. +- **Visualize** a grammar as a transition graph (Mermaid). +- **Generate a standalone, specialized Go parser** from a grammar (`go generate`). +- Unicode code points and 64-bit numeric values; bounded against DoS. + +```mermaid +flowchart TD + A["ABNF text"] -->|ParseABNF| G["*Grammar"] + G -->|IsValid| R["recognise
(valid?)"] + G -->|ParseForest / ParseBSR| F["parse forest
(trees, ambiguity)"] + G -->|Regex| X["regular expression"] + G -->|Generate / ASTGenerator| S["sample inputs"] + G -->|TransitionGraph| T["graph (Mermaid)"] + G -->|GenerateGoParser| C["standalone Go parser"] + S -. feed .-> SUT["your implementation"] + R -. cross-check .- F + F -. cross-check .- X +``` + +## How it works + +ABNF is *self-describing*: its own syntax is written in ABNF. Go-ABNF bootstraps from a handwritten parse/evaluation of that meta-grammar, after which any valid grammar parsed by the library can in turn be used to parse new input - so the engine is generic, not specialized to ABNF. + +
+ +
+ +Two parse paths exist by design: +- **Recognition** (`Grammar.IsValid`) answers *does this input match?* by tracking the set of reachable input positions. It never materializes a tree, so it is cheap and is the reference verdict for the other paths. +- **Parsing** builds a derivation representation using a **GLL** engine [1], which handles arbitrary context-free grammars including ambiguity and (left/right/hidden) recursion. Two representations are available over the same engine: a **Shared Packed Parse Forest** [2] (`ParseForest`) and a **Binary Subtree Set** [3] (`ParseBSR`). Both expose `Valid`, `NumTrees`, `Ambiguous` and `Tree`. + +Repetition (`Min*Max element`) is handled *natively*: it lowers to a single self-recursive nonterminal and the bound is enforced by a counter carried in parser state, rather than unrolled - so a grammar like `0*9999999999 "x"` costs $O(1)$ to set up instead of exhausting memory. + +The generated-parser work targets BSR representation [3], in the spirit of [GoGLL](https://github.com/goccmack/gogll). + +References: +- [1] _E. Scott, A. Johnstone. "GLL Parsing," in Electronic Notes in Theoretical Computer Science, vol. 253, no. 7, pp. 177-189, 2010._ +- [2] _E. Scott, A. Johnstone. "GLL parse-tree generation," in Science of Computer Programming, vol. 78, no. 10, pp. 1828-1844, 2013._ +- [3] _E. Scott, A. Johnstone, L. van Binsbergen. "Derivation representation using binary subtree sets," in Science of Computer Programming, vol. 175, pp. 63-84, 2019._ + +## Usage + +Parse a grammar, then recognize or parse input: + +```go +g, err := goabnf.ParseABNF(grammarBytes) // []byte of ABNF, CRLF line endings +if err != nil { /* ... */ } + +ok, _ := g.IsValid("rule", input) // boolean recognizer + +f, _ := goabnf.ParseForest(input, g, "rule") +fmt.Println(f.Valid(), f.NumTrees(), f.Ambiguous()) + +bf, _ := goabnf.ParseBSR(input, g, "rule") // same answers, BSR representation +``` + +Compile to a regular expression, or render a transition graph: + +```go +re, _ := g.Regex("rule", goabnf.WithMaxRegexLen(4096)) + +tg, _ := g.TransitionGraph("rule") +fmt.Println(tg.ToMermaid()) +``` + +### As a fuzzing oracle + +Generate inputs from a grammar to drive Go's fuzzer, even when the input format is too specific for the engine to discover on its own: +```go +func FuzzMyParser(f *testing.F) { + g, err := goabnf.ParseABNF(myGrammar) + if err != nil { f.Fatal(err) } + + f.Fuzz(func(t *testing.T, seed int64) { + input, _ := g.Generate(seed, "rule", + goabnf.WithRepMax(15), // cap repetitions + goabnf.WithThreshold(1024), // cap generated size + ) + MyParser(input) // must not panic; optionally cross-check verdicts + }) +} +``` + +More fuzzing helpers are [documented here](examples/fuzzing/). + +### As a differential oracle + +The recognizer and the parse forest are independent code paths over the same grammar; agreement between them (and with your own implementation) is a strong correctness signal: +```go +ok, _ := g.IsValid("rule", input) +f, _ := goabnf.ParseForest(input, g, "rule") +if ok != f.Valid() { + t.Fatalf("oracle disagreement on %q", input) // a bug in grammar or engine +} +``` + +## Code generation + +Generate a standalone, specialized parser for a grammar - a jump-table GLL/BSR parser with inlined terminals, depending only on the standard library: +```go +src, _ := goabnf.GenerateGoParser(g, "rule", "myparser") +// or, straight from ABNF source: +src, _ = goabnf.GenerateGoParserFromABNF(grammarBytes, "rule", "myparser") +``` + +Wire it into `go generate` with the `cmd/abnf-gen` command: + +```go +//go:generate go run github.com/pandatix/go-abnf/cmd/abnf-gen -in grammar.abnf -root rule -pkg myparser -out parser_gen.go +``` + +The generated file exposes `Parse`, `Result` (`Valid`/`NumTrees`/`Ambiguous`/ `Tree`) and `ParseTree`. Its identifiers are package-scoped, so generate **one parser per package** (e.g. a dedicated `internal/myparser/`). The generated parser is faithful to the grammar you give it: feeding the *raw* RFC 5234 ABNF self-definition produces an ambiguous parser (the defect Erratum 2968 fixes), while the corrected grammar produces an unambiguous one - choose deliberately, and assert the expected tree count in a test. + +## Safety + +The engines are bounded to stay usable on untrusted grammars and inputs: `WithMaxNodes` (transition graph), `WithMaxForestNodes` / `WithMaxSlots` (forest), `WithMaxRegexLen` (regex), and the generated parser's `MaxElements`. Numeric values are accepted up to 64 bits without panicking, and matching is Unicode-aware. + +## Troubleshooting + +### My ABNF grammar does not work + +**Q**: My ABNF grammar does not work. Do you have any idea why ? + +**A**: There could be many reasons to this. First make sure your grammar ends up by a newline (LF), and especially that the input content has a CR LF. As those appear the same, it is often a source of error. + +### Difference between pap and bap + +**Q**: Is there a difference between pap and [bap](https://github.com/ietf-tools/bap) ? + +**A**: Being written in Go makes `pap` more portable and easier to embed in workflows. Beyond that, the goals differ: `bap` aims to produce meaningful end-user errors, whereas `pap` is built to *challenge* it - and goes further by building transition graphs for fuzzing Go code and by supporting Unicode code points (useful for, e.g., [TOML, which is specified in ABNF](https://github.com/toml-lang/toml/blob/main/toml.abnf)). diff --git a/vendor/github.com/pandatix/go-abnf/SECURITY.md b/vendor/github.com/pandatix/go-abnf/SECURITY.md new file mode 100644 index 00000000000..cd92f4bcb0c --- /dev/null +++ b/vendor/github.com/pandatix/go-abnf/SECURITY.md @@ -0,0 +1,11 @@ +# Reporting Security Issues + +Please report any security issues you discovered in the API to lucastesson@protonmail.com. + +We will assess the risk, plus make a fix available before we create a GitHub issue. + +Thank you for your contribution. + +## Refering to this repository + +To refer to this repository using a CPE v2.3, please use `cpe:2.3:a:pandatix:go-abnf:*:*:*:*:*:*:*:*` with the `version` set to the tag you are using. diff --git a/vendor/github.com/pandatix/go-abnf/abnf.go b/vendor/github.com/pandatix/go-abnf/abnf.go new file mode 100644 index 00000000000..5173714486e --- /dev/null +++ b/vendor/github.com/pandatix/go-abnf/abnf.go @@ -0,0 +1,244 @@ +package goabnf + +import ( + "fmt" + "strconv" + "strings" +) + +// ElemItf defines the interface of all the element alternations: +// - ElemRulename +// - ElemGroup +// - ElemOption +// - ElemProseVal +// - ElemNumVal +// - ElemCharVal +// +// This is exposed for custom evaluation purposes, please don't use it else. +type ElemItf interface { + fmt.Stringer +} + +// Rule represents an ABNF rule, with its name and underlying alternation. +// +// This is exposed for custom evaluation purposes, please don't use it else. +type Rule struct { + // Name is the unique rule name. + // Notice it is case-insensitive according to RFC 5234 Section 2.1. + Name string + + Alternation Alternation +} + +func (rl Rule) String() string { + return fmt.Sprintf("%s = %s", rl.Name, rl.Alternation) +} + +// Alternation represents an ABNF alternation object. +// +// This is exposed for custom evaluation purposes, please don't use it else. +type Alternation struct { + // Concatenations contains the variants that validate the upper + // object (i.e. rule, group or option). + Concatenations []Concatenation +} + +func (alt Alternation) String() string { + str := "" + for _, concat := range alt.Concatenations { + str += fmt.Sprintf("%s / ", concat) + } + return strings.TrimSuffix(str, " / ") +} + +// Concatenation represents an ABNF concatenation object. +// +// This is exposed for custom evaluation purposes, please don't use it else. +type Concatenation struct { + // Repetitions contains the following repetitions, order matter. + Repetitions []Repetition +} + +func (cnt Concatenation) String() string { + str := "" + for _, rep := range cnt.Repetitions { + str += fmt.Sprintf("%s ", rep) + } + return strings.TrimSuffix(str, " ") +} + +// Repetition represents an ABNF repetition object. +// +// This is exposed for custom evaluation purposes, please don't use it else. +type Repetition struct { + Min, Max int + Element ElemItf +} + +func (rep Repetition) String() string { + if rep.Min == rep.Max { + if rep.Min == 1 { + return rep.Element.String() + } + return strconv.Itoa(rep.Min) + rep.Element.String() + } + str := "" + if rep.Min != 0 { + str += strconv.Itoa(rep.Min) + } + str += "*" + if rep.Max != inf { + str += strconv.Itoa(rep.Max) + } + return str + rep.Element.String() +} + +// ElemRulename represents an ABNF rulename object. +// +// This is exposed for custom evaluation purposes, please don't use it else. +type ElemRulename struct { + // Name is the unique rule name. + // Notice it is case-insensitive according to RFC 5234 Section 2.1. + Name string +} + +func (erln ElemRulename) String() string { + return erln.Name +} + +var _ ElemItf = (*ElemRulename)(nil) + +// ElemGroup represents an ABNF group object. +// It will be straightly passed through with its underlying alternation. +// +// This is exposed for custom evaluation purposes, please don't use it else. +type ElemGroup struct { + Alternation Alternation +} + +func (egrp ElemGroup) String() string { + return "(" + egrp.Alternation.String() + ")" +} + +var _ ElemItf = (*ElemGroup)(nil) + +// ElemOption represents an ABNF option object. +// It will be straightly converted to a 0*1 repetition. +// +// This is exposed for custom evaluation purposes, please don't use it else. +type ElemOption struct { + Alternation Alternation +} + +func (eopt ElemOption) String() string { + return "[" + eopt.Alternation.String() + "]" +} + +var _ ElemItf = (*ElemOption)(nil) + +// ElemCharVal represents an ABNF char-val object. +// +// This is exposed for custom evaluation purposes, please don't use it else. +type ElemCharVal struct { + // Sensitive is by default false, support clarrified by RFC 7405 hence + // only works on a-z and A-Z. + Sensitive bool + Values []rune +} + +func (ecvl ElemCharVal) String() string { + str := "" + for _, val := range ecvl.Values { + str += string(val) + } + return `"` + str + `"` +} + +var _ ElemItf = (*ElemCharVal)(nil) + +// ElemNumVal represents an ABNF num-val object. +// +// This is exposed for custom evaluation purposes, please don't use it else. +type ElemNumVal struct { + Base string + // Status could be: + // - `statSeries`: `elems` contains all the expected + // values in the order of the grammar defined them ; + // - `statRange`: `elems` contains the start and end + // bounds (so no more than two). + Status Status + Elems []string +} + +func (envl ElemNumVal) String() string { + str := "%" + envl.Base + spl := "." + if envl.Status == StatRange { + spl = "-" + } + for _, val := range envl.Elems { + str += val + spl + } + return strings.TrimSuffix(str, spl) +} + +var _ ElemItf = (*ElemNumVal)(nil) + +// ElemProseVal represents an ABNF prose-val object. +// +// This is exposed for custom evaluation purposes, please don't use it else. +type ElemProseVal struct { + values []string +} + +func (epvl ElemProseVal) String() string { + str := "" + for _, val := range epvl.values { + str += val + } + return "<" + str + ">" +} + +var _ ElemItf = (*ElemProseVal)(nil) + +// Status defines the type of a num-val, rather StatSeries or StatRange. +// +// This is exposed for custom evaluation purposes, please don't use it else. +type Status int + +const ( + // StatSeries represents a serie of unique byte value. + StatSeries Status = iota + // StatRange represents an interval of possible bytes. + StatRange +) + +// ABNF is the manually parsed+evaluated+validated ABNF grammar. +var ABNF = &Grammar{ + Rulemap: map[string]*Rule{ + abnfRulelist.Name: abnfRulelist, + abnfRule.Name: abnfRule, + abnfRulename.Name: abnfRulename, + abnfDefinedAs.Name: abnfDefinedAs, + abnfElements.Name: abnfElements, + abnfCWsp.Name: abnfCWsp, + abnfCNl.Name: abnfCNl, + abnfComment.Name: abnfComment, + abnfAlternation.Name: abnfAlternation, + abnfConcatenation.Name: abnfConcatenation, + abnfRepetition.Name: abnfRepetition, + abnfRepeat.Name: abnfRepeat, + abnfElement.Name: abnfElement, + abnfGroup.Name: abnfGroup, + abnfOption.Name: abnfOption, + abnfCharVal.Name: abnfCharVal, + abnfCaseInsensitiveString.Name: abnfCaseInsensitiveString, + abnfCaseSensitiveString.Name: abnfCaseSensitiveString, + abnfQuotedString.Name: abnfQuotedString, + abnfNumVal.Name: abnfNumVal, + abnfBinVal.Name: abnfBinVal, + abnfDecVal.Name: abnfDecVal, + abnfHexVal.Name: abnfHexVal, + abnfProseVal.Name: abnfProseVal, + }, +} diff --git a/vendor/github.com/pandatix/go-abnf/ast.go b/vendor/github.com/pandatix/go-abnf/ast.go new file mode 100644 index 00000000000..5b5adc6e23d --- /dev/null +++ b/vendor/github.com/pandatix/go-abnf/ast.go @@ -0,0 +1,394 @@ +package goabnf + +import ( + "fmt" + "math/rand" + "unicode/utf8" +) + +// Generates strings by recursive descent over the grammar AST, so unlike the* +// transition-graph Generator (a finite automaton, which can only express +// *regular* rules) it handles genuinely recursive rules -- nested objects, +// balanced delimiters, expression grammars -- where a rule is reached many times +// at many depths. +// +// The hard part of recursive generation is termination: a recursive rule can +// expand forever. This generator guarantees a finite, valid production by +// precomputing each rule's minimum expansion cost and, whenever a depth or +// length budget is exceeded, steering every choice toward the cheapest-to-finish +// expansion. It never truncates mid-derivation, so the output is always a +// complete word of the grammar. (It shares the tape/rand entropy `source` with +// the transition-graph generator, so it slots into testing.F the same way.) + +const astUnbounded = 1 << 30 // "infinite" min-length sentinel (non-productive) + +// ASTGenerator generates strings admitted by a (possibly recursive) grammar +// rule, directly from the AST. +type ASTGenerator struct { + g *Grammar + start *Rule + + maxDepth int // rule-expansion depth before steering to terminate + maxLen int // output bytes before steering to terminate + maxRepeat int // cap for unbounded ("*") repetitions + + minCost map[string]int // rule name -> minimum expansion cost (productivity) +} + +// ASTGenOption configures an ASTGenerator. +type ASTGenOption interface { + apply(*ASTGenerator) +} + +type astGenOptionFunc func(*ASTGenerator) + +func (f astGenOptionFunc) apply(g *ASTGenerator) { + f(g) +} + +// WithMaxDepth bounds rule-expansion (recursion) depth; past it the generator +// steers toward terminating expansions. Defaults to 32. +func WithMaxDepth(n int) ASTGenOption { + return astGenOptionFunc(func(g *ASTGenerator) { + g.maxDepth = n + }) +} + +// WithMaxLen bounds the output length; past it the generator steers toward +// terminating expansions. Defaults to 4096. (Mandatory repetitions are always +// emitted, so the bound is soft -- a single huge fixed repetition can exceed it.) +func WithMaxLen(n int) ASTGenOption { + return astGenOptionFunc(func(g *ASTGenerator) { + g.maxLen = n + }) +} + +// WithMaxRepeat caps how far an unbounded ("*" / "1*") repetition is unrolled. +// Defaults to 16. +func WithMaxRepeat(n int) ASTGenOption { + return astGenOptionFunc(func(g *ASTGenerator) { + g.maxRepeat = n + }) +} + +// NewASTGenerator prepares a generator for rule. It errors if the rule is +// undefined, or if any rule reachable from it is undefined or non-productive +// (cannot derive any finite string), since neither can be generated. Unlike the +// transition-graph generator, recursive rules are fully supported. +func NewASTGenerator(g *Grammar, rule string, opts ...ASTGenOption) (*ASTGenerator, error) { + start := GetRule(rule, g.Rulemap) + if start == nil { + return nil, &ErrRuleNotFound{Rulename: rule} + } + ag := &ASTGenerator{ + g: g, + start: start, + maxDepth: 32, + maxLen: 4096, + maxRepeat: 16, + } + for _, o := range opts { + o.apply(ag) + } + + // Least-fixpoint of minimum expansion cost over every rule reachable in + // principle -- user rules and the built-in core rules (ALPHA, DIGIT, ...), + // which GetRule resolves as a fallback but which are absent from Rulemap. + // Keyed by canonical rule name (GetRule is case-insensitive and honors user + // overrides of core rules). A rule stuck at the sentinel is non-productive. + effective := map[string]*Rule{} + for n := range g.Rulemap { + if r := GetRule(n, g.Rulemap); r != nil { + effective[r.Name] = r + } + } + for n := range coreRules { + if r := GetRule(n, g.Rulemap); r != nil { + effective[r.Name] = r + } + } + ag.minCost = make(map[string]int, len(effective)) + for name := range effective { + ag.minCost[name] = astUnbounded + } + for changed := true; changed; { + changed = false + for name, r := range effective { + if c := ag.costAlt(r.Alternation); c < ag.minCost[name] { + ag.minCost[name] = c + changed = true + } + } + } + + // Every rule reachable from start must be defined and productive. + seen := map[string]bool{start.Name: true} + var visitAlt func(Alternation) error + visitElem := func(e ElemItf) error { + switch x := e.(type) { + case ElemRulename: + r := GetRule(x.Name, g.Rulemap) + if r == nil { + return fmt.Errorf("rule %q (reachable from %q) is undefined and cannot be generated", x.Name, rule) + } + if seen[r.Name] { + return nil + } + seen[r.Name] = true + if ag.minCost[r.Name] >= astUnbounded { + return fmt.Errorf("rule %q (reachable from %q) is non-productive (cannot derive any finite string)", r.Name, rule) + } + return visitAlt(r.Alternation) + case ElemGroup: + return visitAlt(x.Alternation) + case ElemOption: + return visitAlt(x.Alternation) + } + return nil + } + visitAlt = func(a Alternation) error { + for _, c := range a.Concatenations { + for _, rep := range c.Repetitions { + if err := visitElem(rep.Element); err != nil { + return err + } + } + } + return nil + } + if ag.minCost[start.Name] >= astUnbounded { + return nil, fmt.Errorf("rule %q is non-productive (cannot derive any finite string)", rule) + } + if err := visitAlt(start.Alternation); err != nil { + return nil, err + } + return ag, nil +} + +// Generate maps the tape to a production the grammar admits. Total and +// deterministic: any tape (including nil) yields a valid production; an +// exhausted tape biases toward the shortest one. +func (ag *ASTGenerator) Generate(tape []byte) []byte { + return ag.walk(&tapeSource{tape: tape}) +} + +// GenerateRand draws a production using r as the entropy source. +func (ag *ASTGenerator) GenerateRand(r *rand.Rand) []byte { + return ag.walk(randSource{r: r}) +} + +func (ag *ASTGenerator) walk(src source) []byte { + out := []byte{} + ag.genAlt(src, &out, ag.start.Alternation, 0) + return out +} + +// over reports whether we should stop taking expansions for their own sake and +// steer toward the nearest terminal string. +func (ag *ASTGenerator) over(out *[]byte, depth int) bool { + return depth >= ag.maxDepth || len(*out) >= ag.maxLen +} + +func (ag *ASTGenerator) genAlt(src source, out *[]byte, alt Alternation, depth int) { + cs := alt.Concatenations + if len(cs) == 0 { + return + } + idx := 0 + if ag.over(out, depth) { + idx = ag.cheapestConcat(cs) // steer: pick the cheapest-to-finish branch + } else if len(cs) > 1 { + idx = src.intn(len(cs)) + } + ag.genConcat(src, out, cs[idx], depth) +} + +func (ag *ASTGenerator) genConcat(src source, out *[]byte, concat Concatenation, depth int) { + for _, rep := range concat.Repetitions { + ag.genRep(src, out, rep, depth) + } +} + +func (ag *ASTGenerator) genRep(src source, out *[]byte, rep Repetition, depth int) { + lo := rep.Min + hi := rep.Max + if hi == inf || hi < lo { // unbounded: cap the optional unrolling + hi = lo + ag.maxRepeat + } + n := lo + if !ag.over(out, depth) && hi > lo { + n = lo + src.intn(hi-lo+1) + } + for i := 0; i < n; i++ { + ag.genElem(src, out, rep.Element, depth) + // Stop optional extras once over length, but always emit the mandatory lo. + if i+1 >= lo && len(*out) >= ag.maxLen { + break + } + } +} + +func (ag *ASTGenerator) genElem(src source, out *[]byte, elem ElemItf, depth int) { + switch e := elem.(type) { + case ElemRulename: + // Guaranteed defined+productive by NewASTGenerator. + r := GetRule(e.Name, ag.g.Rulemap) + ag.genAlt(src, out, r.Alternation, depth+1) + + case ElemGroup: + ag.genAlt(src, out, e.Alternation, depth) + + case ElemOption: + if !ag.over(out, depth) && src.intn(2) == 1 { + ag.genAlt(src, out, e.Alternation, depth) + } + + case ElemCharVal: + for _, r := range e.Values { + if !e.Sensitive && isASCIILetter(r) && src.intn(2) == 1 { + r = flipASCIICase(r) + } + *out = append(*out, []byte(string(r))...) + } + + case ElemNumVal: + switch e.Status { + case StatSeries: + for _, v := range e.Elems { + r := numvalToRune(v, e.Base) + // Emit only real characters; an out-of-range value cannot appear + // in UTF-8 input, so the series would never match anyway. + if utf8.ValidRune(r) { + *out = append(*out, []byte(string(r))...) + } + } + case StatRange: + min, max := numvalToRune(e.Elems[0], e.Base), numvalToRune(e.Elems[1], e.Base) + // Emit within the representable (Unicode) subset of the range. + if max > utf8.MaxRune { + max = utf8.MaxRune + } + if min > utf8.MaxRune || min > max { + break // nothing representable to emit + } + span := int(max-min) + 1 + if span < 1 { + span = 1 + } + *out = append(*out, []byte(string(min+rune(src.intn(span))))...) + } + + case ElemProseVal: + // prose-val is informal text; nothing mechanical to emit. + } +} + +// cheapestConcat returns the index of the concatenation with the smallest +// minimum expansion cost -- the fastest way to finish the current alternation. +func (ag *ASTGenerator) cheapestConcat(cs []Concatenation) int { + best, bestCost := 0, ag.costConcat(cs[0]) + for i := 1; i < len(cs); i++ { + if c := ag.costConcat(cs[i]); c < bestCost { + best, bestCost = i, c + } + } + return best +} + +// ---- minimum-expansion-cost cost model (for productivity + steering) ---- + +func (ag *ASTGenerator) costAlt(alt Alternation) int { + if len(alt.Concatenations) == 0 { + return 0 + } + best := astUnbounded + for _, c := range alt.Concatenations { + if v := ag.costConcat(c); v < best { + best = v + } + } + return best +} + +func (ag *ASTGenerator) costConcat(concat Concatenation) int { + total := 0 + for _, rep := range concat.Repetitions { + total = addClamp(total, mulClamp(rep.Min, ag.costElem(rep.Element))) + } + return total +} + +func (ag *ASTGenerator) costElem(elem ElemItf) int { + switch e := elem.(type) { + case ElemRulename: + r := GetRule(e.Name, ag.g.Rulemap) + if r == nil { + return astUnbounded // undefined rule + } + if c, ok := ag.minCost[r.Name]; ok { + // +1 expansion penalty: referencing a rule always costs strictly + // more than emitting a terminal, so a recursive branch can never + // tie with (and be chosen over) a terminating one -- even when a + // nullable prefix makes their emitted lengths equal. + return addClamp(1, c) + } + return astUnbounded + case ElemGroup: + return ag.costAlt(e.Alternation) + case ElemOption: + return 0 // can be skipped + case ElemCharVal: + return len(string(e.Values)) + case ElemNumVal: + switch e.Status { + case StatSeries: + n := 0 + for _, v := range e.Elems { + n += len(string(numvalToRune(v, e.Base))) + } + return n + default: // StatRange: one rune, take the lower bound's encoded length + return len(string(numvalToRune(e.Elems[0], e.Base))) + } + case ElemProseVal: + return 0 + } + return 0 +} + +func addClamp(a, b int) int { + if a >= astUnbounded || b >= astUnbounded { + return astUnbounded + } + if s := a + b; s < astUnbounded { + return s + } + return astUnbounded +} + +func mulClamp(n, c int) int { + if n == 0 { + return 0 + } + if c >= astUnbounded { + return astUnbounded + } + if p := n * c; p >= 0 && p < astUnbounded { + return p + } + return astUnbounded +} + +func isASCIILetter(r rune) bool { + return (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') +} + +func flipASCIICase(r rune) rune { + switch { + case r >= 'a' && r <= 'z': + return r - 'a' + 'A' + case r >= 'A' && r <= 'Z': + return r - 'A' + 'a' + } + return r +} diff --git a/vendor/github.com/pandatix/go-abnf/bsr.go b/vendor/github.com/pandatix/go-abnf/bsr.go new file mode 100644 index 00000000000..8be32abb987 --- /dev/null +++ b/vendor/github.com/pandatix/go-abnf/bsr.go @@ -0,0 +1,604 @@ +package goabnf + +import ( + "math/big" + "unicode/utf8" +) + +// Binary Subtree Representation (BSR). +// +// This is an alternative derivation representation to the SPPF in sppf.go, +// following Scott, Johnstone & van Binsbergen, "Derivation representation using +// binary subtree sets" (Science of Computer Programming 175, 2019). Instead of +// interning symbol/intermediate/packed nodes, the parser records a flat set of +// elements (slot, l, k, r): a grammar slot whose consumed prefix derives the +// input span [l,r], with the most recently consumed symbol spanning [k,r] and +// the earlier prefix spanning [l,k]. The same GLL control (descriptors, GSS, +// native counted repetition) drives it; only what gets recorded differs. +// +// The GSS node's pos field already holds a production's left extent (the call +// position), so a BSR element needs no extra threading: on a dot advance the +// element is (newSlot, l=frameLeftExtent, k=pivot, r=end). +// +// The BSR engine is validated to produce identical Valid/NumTrees/Ambiguous +// results to the SPPF engine across the differential corpus; it is the intended +// foundation for grammar-directed parser generation. + +// bsrElem is one binary subtree element. The slot's dot is "just past" the most +// recently consumed symbol; that symbol spans [k,r] and the earlier prefix spans +// [l,k]. A complete production (dot == len) records the nonterminal's extent. +type bsrElem struct { + slot slot + l, k, r int +} + +// bsrGNode is a GSS node for the BSR engine. Unlike the SPPF GSS, its edges are +// unlabelled (no SPPF node is carried): the BSR pivots are recovered from node +// positions instead. +type bsrGNode struct { + ret slot // return slot in the caller's production + pos int // call position == the caller production's left extent for callees + rc int // repetition count (native counted repetition; 0 otherwise) + edges []*bsrGNode + edgeSet map[*bsrGNode]bool +} + +type bsrDesc struct { + L slot + u *bsrGNode + i int + rc int +} + +type bsrParser struct { + sg *slotGrammar + input []byte + + set map[bsrElem]bool + gss map[gssKey]*bsrGNode + u0 *bsrGNode + seen map[bsrDesc]bool + work []bsrDesc + popped map[*bsrGNode]map[int]bool + + maxElems int + aborted bool +} + +// addAlt adds the descriptor starting alternate L.alt of L.nt at i when the +// lookahead test admits it. +func (p *bsrParser) addAlt(L slot, u *bsrGNode, i, rc int) { + if p.sg.la.alt(L.nt, L.alt).admits(p.input, i) { + p.add(L, u, i, rc) + } +} + +func (p *bsrParser) add(L slot, u *bsrGNode, i, rc int) { + d := bsrDesc{L, u, i, rc} + if p.seen[d] { + return + } + p.seen[d] = true + p.work = append(p.work, d) +} + +func (p *bsrParser) gssNodeFor(ret slot, pos, rc int) *bsrGNode { + k := gssKey{ret, pos, rc} + if v, ok := p.gss[k]; ok { + return v + } + v := &bsrGNode{ret: ret, pos: pos, rc: rc, edgeSet: map[*bsrGNode]bool{}} + p.gss[k] = v + return v +} + +// record adds the BSR element for advancing into slot L over [l,r] with the last +// symbol pivoting at k. Per the binary-subtree scheme an element is kept only +// when the consumed prefix has length >= 2 (an intermediate extent) or the +// production is complete (dot == len). A length-1 prefix is degenerate (l == k) +// and is reconstructed directly from the single symbol, so it is not stored. +func (p *bsrParser) record(L slot, l, k, r int) { + prod := p.sg.nts[L.nt].alts[L.alt] + if L.dot >= 2 || L.dot == len(prod) { + if p.maxElems > 0 && len(p.set) >= p.maxElems { + p.aborted = true + return + } + p.set[bsrElem{L, l, k, r}] = true + } +} + +func (p *bsrParser) create(ret slot, u *bsrGNode, i, rc int) *bsrGNode { + v := p.gssNodeFor(ret, i, rc) + if !v.edgeSet[u] { + v.edgeSet[u] = true + v.edges = append(v.edges, u) + for j := range p.popped[v] { + p.record(ret, u.pos, i, j) + p.add(ret, u, j, rc) + } + } + return v +} + +func (p *bsrParser) pop(u *bsrGNode, j int) { + if u == p.u0 { + return + } + if p.popped[u] == nil { + p.popped[u] = map[int]bool{} + } + p.popped[u][j] = true + for _, v := range u.edges { + p.record(u.ret, v.pos, u.pos, j) + p.add(u.ret, v, j, u.rc) + } +} + +func (p *bsrParser) addRepAlts(ntID int, v *bsrGNode, i, rc int) { + nt := p.sg.nts[ntID] + if rc >= nt.repMin { + p.addAlt(slot{ntID, 0, 0}, v, i, rc) + } + if nt.repMax == inf || rc < nt.repMax { + p.addAlt(slot{ntID, 1, 0}, v, i, rc) + } +} + +func (p *bsrParser) matchTerm(s ssym, i int) int { + if s.kind == symEps { + return i + } + switch v := s.term.(type) { + case ElemCharVal: + idx := i + for k := 0; k < len(v.Values); k++ { + if idx >= len(p.input) { + return -1 + } + r, size := utf8.DecodeRune(p.input[idx:]) + if r == utf8.RuneError && size == 1 { + return -1 + } + if !sensequal(v.Values[k], r, v.Sensitive) { + return -1 + } + idx += size + } + return idx + case ElemNumVal: + switch v.Status { + case StatRange: + if i >= len(p.input) { + return -1 + } + min := numvalToRune(v.Elems[0], v.Base) + max := numvalToRune(v.Elems[1], v.Base) + r, size := utf8.DecodeRune(p.input[i:]) + if r == utf8.RuneError && size == 1 { + return -1 + } + if min <= r && r <= max { + return i + size + } + return -1 + case StatSeries: + idx := i + for k := 0; k < len(v.Elems); k++ { + ru := numvalToRune(v.Elems[k], v.Base) + if !utf8.ValidRune(ru) { + return -1 + } + str := string(ru) + sz := len(str) + if idx+sz > len(p.input) || str != string(p.input[idx:idx+sz]) { + return -1 + } + idx += sz + } + return idx + } + } + return -1 +} + +func (p *bsrParser) parse() { + startNT := p.sg.start + p.u0 = &bsrGNode{ret: slot{-1, -1, -1}, pos: 0, edgeSet: map[*bsrGNode]bool{}} + for ai := range p.sg.nts[startNT].alts { + p.addAlt(slot{startNT, ai, 0}, p.u0, 0, 0) + } + for len(p.work) > 0 { + if p.aborted { + return + } + d := p.work[len(p.work)-1] + p.work = p.work[:len(p.work)-1] + p.process(d) + } +} + +func (p *bsrParser) process(d bsrDesc) { + L, u, i, rc := d.L, d.u, d.i, d.rc + for { + prod := p.sg.nts[L.nt].alts[L.alt] + if L.dot == len(prod) { + p.pop(u, i) + return + } + s := prod[L.dot] + if s.kind == symNonterm { + if !p.sg.la.callAt(L).admits(p.input, i) { + return + } + ret := slot{L.nt, L.alt, L.dot + 1} + v := p.create(ret, u, i, rc) + if p.sg.nts[s.nt].isRep { + childRC := 0 + if s.nt == L.nt { + childRC = capRC(rc, p.sg.nts[s.nt]) + } + p.addRepAlts(s.nt, v, i, childRC) + } else { + for ai := range p.sg.nts[s.nt].alts { + p.addAlt(slot{s.nt, ai, 0}, v, i, 0) + } + } + return + } + j := p.matchTerm(s, i) + if j < 0 { + return + } + next := slot{L.nt, L.alt, L.dot + 1} + p.record(next, u.pos, i, j) + i = j + L = next + } +} + +// --------------------------------------------------------------------------- +// Queries over the BSR set +// --------------------------------------------------------------------------- + +// BSRForest is the BSR analogue of Forest: a set of binary subtree elements plus +// an index for reconstruction. Valid/NumTrees/Ambiguous/Tree mirror Forest. +type BSRForest struct { + sg *slotGrammar + input []byte + rulename string + set map[bsrElem]bool + index map[bsrKey][]int // (slot,l,r) -> pivots k + start int + n int +} + +type bsrKey struct { + slot slot + l, r int +} + +func (f *BSRForest) buildIndex() { + f.index = make(map[bsrKey][]int, len(f.set)) + for e := range f.set { + k := bsrKey{e.slot, e.l, e.r} + f.index[k] = append(f.index[k], e.k) + } +} + +// startElems returns, for each production of the start rule, the pivots of the +// complete-production elements spanning the whole input. +func (f *BSRForest) startElems() (slot, []int, bool) { + for ai, prod := range f.sg.nts[f.start].alts { + s := slot{f.start, ai, len(prod)} + if ks := f.index[bsrKey{s, 0, f.n}]; len(ks) > 0 { + return s, ks, true + } + } + return slot{}, nil, false +} + +// Valid reports whether the whole input is derivable by the root rule. +func (f *BSRForest) Valid() bool { + for ai, prod := range f.sg.nts[f.start].alts { + s := slot{f.start, ai, len(prod)} + if len(f.index[bsrKey{s, 0, f.n}]) > 0 { + return true + } + } + return false +} + +// Nodes returns the number of BSR elements (the representation's size). +func (f *BSRForest) Nodes() int { return len(f.set) } + +// symNode/interNode identities for memoisation and cycle detection. +type bsrNodeID struct { + inter bool + slot slot // for intermediate nodes + nt int // for symbol nodes + l, r int +} + +// NumTrees returns the number of distinct parse trees represented, or -1 for +// infinitely many (a cycle, i.e. an infinitely-ambiguous grammar). It mirrors +// Forest.NumTrees but counts over BSR extents instead of SPPF nodes. +func (f *BSRForest) NumTrees() *big.Int { + if !f.Valid() { + return big.NewInt(0) + } + memo := map[bsrNodeID]*big.Int{} + onStack := map[bsrNodeID]bool{} + infinite := false + + var symWays func(s ssym, a, b int) *big.Int + var countSym func(nt, l, r int) *big.Int + var countInter func(sl slot, l, r int) *big.Int + var countElem func(sl slot, l, k, r int) *big.Int + + symWays = func(s ssym, a, b int) *big.Int { + if s.kind == symNonterm { + return countSym(s.nt, a, b) + } + return big.NewInt(1) // terminal / eps leaf + } + + countElem = func(sl slot, l, k, r int) *big.Int { + prod := f.sg.nts[sl.nt].alts[sl.alt] + last := symWays(prod[sl.dot-1], k, r) + var pre *big.Int + switch sl.dot { + case 1: // empty prefix (l == k) + pre = big.NewInt(1) + case 2: // single-symbol prefix + pre = symWays(prod[0], l, k) + default: // intermediate prefix + pre = countInter(slot{sl.nt, sl.alt, sl.dot - 1}, l, k) + } + return new(big.Int).Mul(pre, last) + } + + countInter = func(sl slot, l, r int) *big.Int { + id := bsrNodeID{inter: true, slot: sl, l: l, r: r} + if v, ok := memo[id]; ok { + return v + } + if onStack[id] { + infinite = true + return big.NewInt(1) + } + onStack[id] = true + total := big.NewInt(0) + for _, k := range f.index[bsrKey{sl, l, r}] { + total.Add(total, countElem(sl, l, k, r)) + } + onStack[id] = false + memo[id] = total + return total + } + + countSym = func(nt, l, r int) *big.Int { + id := bsrNodeID{nt: nt, l: l, r: r} + if v, ok := memo[id]; ok { + return v + } + if onStack[id] { + infinite = true + return big.NewInt(1) + } + onStack[id] = true + total := big.NewInt(0) + for ai, prod := range f.sg.nts[nt].alts { + sl := slot{nt, ai, len(prod)} + for _, k := range f.index[bsrKey{sl, l, r}] { + total.Add(total, countElem(sl, l, k, r)) + } + } + onStack[id] = false + memo[id] = total + return total + } + + res := countSym(f.start, 0, f.n) + if infinite { + return big.NewInt(-1) + } + return res +} + +// Ambiguous reports whether the input has more than one distinct parse tree. +func (f *BSRForest) Ambiguous() bool { + if !f.Valid() { + return false + } + visited := map[bsrNodeID]bool{} + + var symAmb func(nt, l, r int) bool + var interAmb func(sl slot, l, r int) bool + var elemAmb func(sl slot, l, k, r int) bool + + symOf := func(s ssym, a, b int) bool { + if s.kind == symNonterm { + return symAmb(s.nt, a, b) + } + return false + } + elemAmb = func(sl slot, l, k, r int) bool { + prod := f.sg.nts[sl.nt].alts[sl.alt] + if symOf(prod[sl.dot-1], k, r) { + return true + } + switch sl.dot { + case 1: + return false + case 2: + return symOf(prod[0], l, k) + default: + return interAmb(slot{sl.nt, sl.alt, sl.dot - 1}, l, k) + } + } + interAmb = func(sl slot, l, r int) bool { + id := bsrNodeID{inter: true, slot: sl, l: l, r: r} + if visited[id] { + return false + } + visited[id] = true + ks := f.index[bsrKey{sl, l, r}] + if len(ks) > 1 { + return true + } + for _, k := range ks { + if elemAmb(sl, l, k, r) { + return true + } + } + return false + } + symAmb = func(nt, l, r int) bool { + id := bsrNodeID{nt: nt, l: l, r: r} + if visited[id] { + return false + } + visited[id] = true + packs := 0 + for ai, prod := range f.sg.nts[nt].alts { + sl := slot{nt, ai, len(prod)} + ks := f.index[bsrKey{sl, l, r}] + packs += len(ks) + } + if packs > 1 { + return true + } + for ai, prod := range f.sg.nts[nt].alts { + sl := slot{nt, ai, len(prod)} + for _, k := range f.index[bsrKey{sl, l, r}] { + if elemAmb(sl, l, k, r) { + return true + } + } + } + return false + } + return symAmb(f.start, 0, f.n) +} + +// Tree extracts a single parse tree (first element at each extent), or nil if +// the input is invalid. Synthetic nonterminals (groups, options, repetitions) +// are collapsed so only original rule names appear, matching Forest.Tree. +func (f *BSRForest) Tree() *ParseTree { + if !f.Valid() { + return nil + } + sl, ks, ok := f.startElems() + if !ok { + return nil + } + visited := map[bsrNodeID]bool{} + return f.emitSym(f.start, sl, ks[0], 0, f.n, visited) +} + +// emitSym builds a ParseTree node for nonterminal nt over [l,r], chosen via the +// complete-production slot sl with pivot k. +func (f *BSRForest) emitSym(nt int, sl slot, k, l, r int, visited map[bsrNodeID]bool) *ParseTree { + var kids []*ParseTree + f.collectElem(sl, l, k, r, &kids, visited) + info := f.sg.nts[nt] + if !info.isRule { + // synthetic (group/option/rep): splice children into the parent + return &ParseTree{Start: l, End: r, Children: kids} // caller flattens + } + return &ParseTree{Rule: info.ruleName, Start: l, End: r, Children: kids} +} + +// collectElem appends the children of element (sl,l,k,r) into out, flattening +// intermediate extents and synthetic nonterminals. +func (f *BSRForest) collectElem(sl slot, l, k, r int, out *[]*ParseTree, visited map[bsrNodeID]bool) { + prod := f.sg.nts[sl.nt].alts[sl.alt] + // prefix x_1..x_{dot-1} over [l,k] + switch sl.dot { + case 1: + // empty + case 2: + f.collectChild(prod[0], l, k, out, visited) + default: + isl := slot{sl.nt, sl.alt, sl.dot - 1} + if ks := f.index[bsrKey{isl, l, k}]; len(ks) > 0 { + f.collectElem(isl, l, ks[0], k, out, visited) + } + } + // last symbol x_{dot} over [k,r] + f.collectChild(prod[sl.dot-1], k, r, out, visited) +} + +func (f *BSRForest) collectChild(s ssym, a, b int, out *[]*ParseTree, visited map[bsrNodeID]bool) { + if s.kind != symNonterm { + if s.kind == symEps { + return // epsilon contributes no leaf + } + *out = append(*out, &ParseTree{Start: a, End: b}) + return + } + id := bsrNodeID{nt: s.nt, l: a, r: b} + if visited[id] { + return + } + visited[id] = true + // pick the first complete-production element for this nonterminal extent + for ai, prod := range f.sg.nts[s.nt].alts { + csl := slot{s.nt, ai, len(prod)} + if ks := f.index[bsrKey{csl, a, b}]; len(ks) > 0 { + info := f.sg.nts[s.nt] + if info.isRule { + var kids []*ParseTree + f.collectElem(csl, a, ks[0], b, &kids, visited) + *out = append(*out, &ParseTree{Rule: info.ruleName, Start: a, End: b, Children: kids}) + } else { + // synthetic: splice its children directly into the parent + f.collectElem(csl, a, ks[0], b, out, visited) + } + visited[id] = false + return + } + } + visited[id] = false +} + +func newBSRParser(sg *slotGrammar, input []byte, maxElems int) *bsrParser { + return &bsrParser{ + sg: sg, + input: input, + set: map[bsrElem]bool{}, + gss: map[gssKey]*bsrGNode{}, + seen: map[bsrDesc]bool{}, + popped: map[*bsrGNode]map[int]bool{}, + maxElems: maxElems, + } +} + +// ParseBSR parses input under rootRulename using the BSR engine. It accepts the +// same options as ParseForest (WithMaxForestNodes bounds the BSR set size). +func ParseBSR(input []byte, grammar *Grammar, rootRulename string, opts ...ForestOption) (*BSRForest, error) { + cfg := forestConfig{maxSlots: defaultMaxSlots} + for _, o := range opts { + o(&cfg) + } + sg, err := compileSlots(grammar, rootRulename, cfg.maxSlots) + if err != nil { + return nil, err + } + p := newBSRParser(sg, input, cfg.maxNodes) + p.parse() + if p.aborted { + return nil, &ErrForestTooLarge{Max: cfg.maxNodes} + } + f := &BSRForest{ + sg: sg, + input: input, + rulename: rootRulename, + set: p.set, + start: sg.start, + n: len(input), + } + f.buildIndex() + return f, nil +} diff --git a/vendor/github.com/pandatix/go-abnf/codegen.go b/vendor/github.com/pandatix/go-abnf/codegen.go new file mode 100644 index 00000000000..7cb138f647b --- /dev/null +++ b/vendor/github.com/pandatix/go-abnf/codegen.go @@ -0,0 +1,903 @@ +package goabnf + +import ( + "fmt" + "go/format" + "strconv" + "strings" + "unicode/utf8" +) + +// GenerateGoParserFromABNF is a convenience wrapper that parses ABNF grammar +// source and generates a specialised parser for it in one step. root is the rule +// the generated parser starts from; pkg is the package name of the emitted file. +// It is the natural entry point for a code-generation command (e.g. driven by +// `go generate`). +func GenerateGoParserFromABNF(abnfSource []byte, root, pkg string, opts ...ABNFOption) ([]byte, error) { + g, err := ParseABNF(abnfSource, opts...) + if err != nil { + return nil, fmt.Errorf("parse ABNF source: %w", err) + } + return GenerateGoParser(g, root, pkg) +} + +// GenerateGoParser emits a standalone Go source file containing a parser +// SPECIALISED to grammar g rooted at root. Unlike a table-driven interpreter, +// the grammar is compiled into control flow: +// +// - the parse loop is a jump-table switch over slot IDs; each case is the +// straight-line code for that one slot (its symbol, its successor, its +// completion), so there is no per-step table lookup and no symbol-kind +// dispatch at runtime; +// - terminals are inlined as byte comparisons / rune decodes against baked +// literals (no ElemItf, no closure table); +// - nonterminal calls, completions, and the {min,max} repetition gating are +// baked as constants at each site; +// - only the genuinely grammar-independent machinery (GSS, descriptor +// worklist, BSR set) remains as fixed runtime, plus a compact metadata table +// used solely by the reconstruction queries (NumTrees/Ambiguous/Tree), which +// inherently need grammar structure. +// +// The output depends only on the standard library, is panic-free (input decoded +// via utf8, no manual indexing) and bounded (MaxElements aborts pathological +// inputs with ErrTooLarge). It is validated against ParseBSR via the differential +// harness. pkg is the package name for the emitted file. +func GenerateGoParser(g *Grammar, root, pkg string) ([]byte, error) { + sg, err := compileSlots(g, root, defaultMaxSlots) + if err != nil { + return nil, err + } + gen := &codegen{sg: sg, root: root, pkg: pkg} + gen.assignSlots() + src := gen.emit() + formatted, ferr := format.Source([]byte(src)) + if ferr != nil { + return []byte(src), fmt.Errorf("generated source failed to format: %w", ferr) + } + return formatted, nil +} + +type sInfo struct { + nt, alt, dot, plen int +} + +type codegen struct { + sg *slotGrammar + root string + pkg string + + slots []sInfo // slot id -> (nt,alt,dot,prodLen) + idOf [][]int // nt,alt -> base slot id (dot 0); dot d == base+d + series []string + seriesIdx map[string]int + usesUTF8 bool // whether any emitted matcher references unicode/utf8 +} + +func (c *codegen) startID(nt, alt int) int { return c.idOf[nt][alt] } +func (c *codegen) compID(nt, alt int) int { return c.idOf[nt][alt] + len(c.sg.nts[nt].alts[alt]) } + +// assignSlots numbers every slot (nt,alt,dot) with dots within a production +// consecutive, so the slot at dot d-1 is (id-1). +func (c *codegen) assignSlots() { + c.seriesIdx = map[string]int{} + c.idOf = make([][]int, len(c.sg.nts)) + for nt, info := range c.sg.nts { + c.idOf[nt] = make([]int, len(info.alts)) + for alt, prod := range info.alts { + c.idOf[nt][alt] = len(c.slots) + for dot := 0; dot <= len(prod); dot++ { + c.slots = append(c.slots, sInfo{nt, alt, dot, len(prod)}) + } + } + } +} + +func (c *codegen) emit() string { + c.scanUTF8() + var b strings.Builder + fmt.Fprintf(&b, "// Code generated by go-abnf for root %q. DO NOT EDIT.\n\n", c.root) + fmt.Fprintf(&b, "package %s\n\n", c.pkg) + c.emitImports(&b) + b.WriteString(headerBoilerplate) + c.emitProcess(&b) + c.emitSeed(&b) + c.emitMetadata(&b) + b.WriteString(queryBoilerplate) + c.emitSeriesVars(&b) + return b.String() +} + +// scanUTF8 sets usesUTF8 if any terminal compiles to code that decodes runes +// (a num-val range, or a char-val with a non-ASCII rune). +func (c *codegen) scanUTF8() { + for _, nt := range c.sg.nts { + for _, prod := range nt.alts { + for _, s := range prod { + if s.kind != symTerm { + continue + } + switch v := s.term.(type) { + case ElemNumVal: + if v.Status == StatRange { + c.usesUTF8 = true + } + case ElemCharVal: + for _, r := range v.Values { + if r > 0x7f { + c.usesUTF8 = true + } + } + } + } + } + } +} + +func (c *codegen) emitImports(b *strings.Builder) { + b.WriteString("import (\n\t\"errors\"\n\t\"math/big\"\n") + if c.usesUTF8 { + b.WriteString("\t\"unicode/utf8\"\n") + } + b.WriteString(")\n") +} + +// emitProcess generates the specialised parse loop: a jump-table switch over +// slot ids, one case per slot. +func (c *codegen) emitProcess(b *strings.Builder) { + b.WriteString("\nfunc (p *parser) process(d desc) {\n") + b.WriteString("\tL, u, i, rc := d.L, d.u, d.i, d.rc\n") + b.WriteString("\t_ = rc\n") + b.WriteString("\tfor {\n\t\tswitch L {\n") + for id, s := range c.slots { + info := c.sg.nts[s.nt] + fmt.Fprintf(b, "\t\tcase %d: // %s\n", id, c.slotComment(s)) + if s.dot == s.plen { + b.WriteString("\t\t\tp.pop(u, i)\n\t\t\treturn\n") + continue + } + sym := info.alts[s.alt][s.dot] + nextID := id + 1 + switch sym.kind { + case symNonterm: + c.emitCall(b, s, sym.nt, nextID) + default: + c.emitTerminal(b, sym, nextID) + } + } + b.WriteString("\t\tdefault:\n\t\t\treturn\n") + b.WriteString("\t\t}\n\t}\n}\n") +} + +func (c *codegen) emitCall(b *strings.Builder, s sInfo, callee, retID int) { + fmt.Fprintf(b, "\t\t\tv := p.create(%d, u, i, rc)\n", retID) + ci := c.sg.nts[callee] + if ci.isRep { + if callee == s.nt { + // self-recursion inside the rep's own loop: continue the chain. + if ci.repMax == inf { + fmt.Fprintf(b, "\t\t\tcrc := rc + 1\n\t\t\tif crc > %d {\n\t\t\t\tcrc = %d\n\t\t\t}\n", ci.repMin, ci.repMin) + } else { + b.WriteString("\t\t\tcrc := rc + 1\n") + } + fmt.Fprintf(b, "\t\t\tif crc >= %d {\n\t\t\t\tp.add(%d, v, i, crc)\n\t\t\t}\n", ci.repMin, c.startID(callee, 0)) + if ci.repMax == inf { + fmt.Fprintf(b, "\t\t\tp.add(%d, v, i, crc)\n", c.startID(callee, 1)) + } else { + fmt.Fprintf(b, "\t\t\tif crc < %d {\n\t\t\t\tp.add(%d, v, i, crc)\n\t\t\t}\n", ci.repMax, c.startID(callee, 1)) + } + } else { + // external call: fresh chain at count 0; gating decided at gen time. + if ci.repMin == 0 { + fmt.Fprintf(b, "\t\t\tp.add(%d, v, i, 0)\n", c.startID(callee, 0)) + } + if ci.repMax == inf || 0 < ci.repMax { + fmt.Fprintf(b, "\t\t\tp.add(%d, v, i, 0)\n", c.startID(callee, 1)) + } + } + } else { + for alt := range ci.alts { + fmt.Fprintf(b, "\t\t\tp.add(%d, v, i, 0)\n", c.startID(callee, alt)) + } + } + b.WriteString("\t\t\treturn\n") +} + +func (c *codegen) emitTerminal(b *strings.Builder, sym ssym, nextID int) { + if sym.kind == symEps { + fmt.Fprintf(b, "\t\t\tp.record(%d, u.pos, i, i)\n", nextID) + fmt.Fprintf(b, "\t\t\tL = %d\n\t\t\tcontinue\n", nextID) + return + } + if !c.emitMatch(b, sym) { + // The terminal can never match (a prose-val, or a series carrying a + // non-Unicode value): emitMatch has emitted `return` and defined no j, + // so the success tail below would be unreachable and reference an + // undefined j ; we should stop here. + return + } + fmt.Fprintf(b, "\t\t\tp.record(%d, u.pos, i, j)\n", nextID) + fmt.Fprintf(b, "\t\t\ti = j\n\t\t\tL = %d\n\t\t\tcontinue\n", nextID) +} + +// emitMatch emits inline code setting j to the end index on success or return on +// failure. It reports whether the terminal can match at all: a false result +// means it emitted only `return` and defined no j (prose-val, or a series with a +// non-Unicode value), so the caller must not emit the j-dependent success tail. +func (c *codegen) emitMatch(b *strings.Builder, s ssym) bool { + switch v := s.term.(type) { + case ElemCharVal: + c.emitCharVal(b, v) + return true + case ElemNumVal: + switch v.Status { + case StatRange: + lo := numvalToRune(v.Elems[0], v.Base) + hi := numvalToRune(v.Elems[1], v.Base) + b.WriteString("\t\t\tif i >= len(p.input) {\n\t\t\t\treturn\n\t\t\t}\n") + b.WriteString("\t\t\trv, rs := utf8.DecodeRune(p.input[i:])\n") + b.WriteString("\t\t\tif rv == utf8.RuneError && rs == 1 {\n\t\t\t\treturn\n\t\t\t}\n") + fmt.Fprintf(b, "\t\t\tif rv < %d || rv > %d {\n\t\t\t\treturn\n\t\t\t}\n", lo, hi) + b.WriteString("\t\t\tj := i + rs\n") + return true + case StatSeries: + return c.emitSeries(b, v) + default: + b.WriteString("\t\t\treturn\n") + return false + } + default: // prose-val: never matches + b.WriteString("\t\t\treturn\n") + return false + } +} + +func (c *codegen) emitCharVal(b *strings.Builder, v ElemCharVal) { + allASCII := true + for _, r := range v.Values { + if r > 0x7f { + allASCII = false + break + } + } + if allASCII { + fmt.Fprintf(b, "\t\t\tif i+%d > len(p.input) {\n\t\t\t\treturn\n\t\t\t}\n", len(v.Values)) + for off, r := range v.Values { + bb := byte(r) + if v.Sensitive { + fmt.Fprintf(b, "\t\t\tif p.input[i+%d] != %d {\n\t\t\t\treturn\n\t\t\t}\n", off, bb) + } else { + fmt.Fprintf(b, "\t\t\tif foldB(p.input[i+%d]) != %d {\n\t\t\t\treturn\n\t\t\t}\n", off, foldByte(bb)) + } + } + fmt.Fprintf(b, "\t\t\tj := i + %d\n", len(v.Values)) + return + } + // rare non-ASCII char-val: decode rune-by-rune. + b.WriteString("\t\t\tj := i\n") + for _, r := range v.Values { + b.WriteString("\t\t\t{\n") + b.WriteString("\t\t\t\tif j >= len(p.input) {\n\t\t\t\t\treturn\n\t\t\t\t}\n") + b.WriteString("\t\t\t\trv, rs := utf8.DecodeRune(p.input[j:])\n") + b.WriteString("\t\t\t\tif rv == utf8.RuneError && rs == 1 {\n\t\t\t\t\treturn\n\t\t\t\t}\n") + if v.Sensitive { + fmt.Fprintf(b, "\t\t\t\tif rv != %d {\n\t\t\t\t\treturn\n\t\t\t\t}\n", r) + } else { + fmt.Fprintf(b, "\t\t\t\tif foldR(rv) != %d {\n\t\t\t\t\treturn\n\t\t\t\t}\n", foldRune(r)) + } + b.WriteString("\t\t\t\tj += rs\n\t\t\t}\n") + } +} + +func (c *codegen) emitSeries(b *strings.Builder, v ElemNumVal) bool { + var sb strings.Builder + for _, e := range v.Elems { + r := numvalToRune(e, v.Base) + if !utf8.ValidRune(r) { + b.WriteString("\t\t\treturn // series contains a non-Unicode value\n") + return false + } + sb.WriteRune(r) + } + idx := c.seriesVar([]byte(sb.String())) + fmt.Fprintf(b, "\t\t\tif i+len(series%d) > len(p.input) || !bytesEqual(p.input[i:i+len(series%d)], series%d) {\n\t\t\t\treturn\n\t\t\t}\n", idx, idx, idx) + fmt.Fprintf(b, "\t\t\tj := i + len(series%d)\n", idx) + return true +} + +func (c *codegen) seriesVar(bs []byte) int { + key := string(bs) + if i, ok := c.seriesIdx[key]; ok { + return i + } + i := len(c.series) + c.seriesIdx[key] = i + parts := make([]string, len(bs)) + for k, x := range bs { + parts[k] = strconv.Itoa(int(x)) + } + c.series = append(c.series, "[]byte{"+strings.Join(parts, ", ")+"}") + return i +} + +func (c *codegen) emitSeed(b *strings.Builder) { + b.WriteString("\nfunc (p *parser) seed() {\n") + for alt := range c.sg.nts[c.sg.start].alts { + fmt.Fprintf(b, "\tp.add(%d, p.u0, 0, 0)\n", c.startID(c.sg.start, alt)) + } + b.WriteString("}\n") +} + +func (c *codegen) emitMetadata(b *strings.Builder) { + b.WriteString("\n// ---- baked metadata (reconstruction queries only) ----\n\n") + fmt.Fprintf(b, "const gRootName = %q\n", c.root) + fmt.Fprintf(b, "const gStart = %d\n\n", c.sg.start) + + b.WriteString("var slotInfo = []sInfo{\n") + for _, s := range c.slots { + fmt.Fprintf(b, "\t{%d, %d, %d, %d},\n", s.nt, s.alt, s.dot, s.plen) + } + b.WriteString("}\n\n") + + b.WriteString("var ntComplete = [][]int{\n") + for nt := range c.sg.nts { + b.WriteString("\t{") + for alt := range c.sg.nts[nt].alts { + if alt > 0 { + b.WriteString(", ") + } + fmt.Fprintf(b, "%d", c.compID(nt, alt)) + } + b.WriteString("},\n") + } + b.WriteString("}\n\n") + + b.WriteString("var qNTs = []qNT{\n") + for _, nt := range c.sg.nts { + fmt.Fprintf(b, "\t{isRule: %v, ruleName: %q, alts: ", nt.isRule, nt.ruleName) + if len(nt.alts) == 0 { + b.WriteString("nil},\n") + continue + } + b.WriteString("[][]qSym{") + for ai, prod := range nt.alts { + if ai > 0 { + b.WriteString(", ") + } + b.WriteString("{") + for si, sym := range prod { + if si > 0 { + b.WriteString(", ") + } + switch sym.kind { + case symNonterm: + fmt.Fprintf(b, "{kind: kNT, nt: %d}", sym.nt) + case symEps: + b.WriteString("{kind: kEps}") + default: + b.WriteString("{kind: kTerm}") + } + } + b.WriteString("}") + } + b.WriteString("}},\n") + } + b.WriteString("}\n") +} + +func (c *codegen) emitSeriesVars(b *strings.Builder) { + if len(c.series) == 0 { + return + } + b.WriteString("\n// ---- baked series literals ----\n") + for i, s := range c.series { + fmt.Fprintf(b, "var series%d = %s\n", i, s) + } +} + +func (c *codegen) slotComment(s sInfo) string { + nt := c.sg.nts[s.nt] + name := nt.ruleName + if name == "" { + name = nt.label + } + prod := nt.alts[s.alt] + parts := make([]string, 0, len(prod)+1) + for d, sym := range prod { + if d == s.dot { + parts = append(parts, ".") + } + parts = append(parts, symStr(c.sg, sym)) + } + if s.dot == len(prod) { + parts = append(parts, ".") + } + return strings.TrimSpace(name+" ::= "+strings.Join(parts, " ")) + + fmt.Sprintf(" (%d,%d,%d)", s.nt, s.alt, s.dot) +} + +func symStr(sg *slotGrammar, s ssym) string { + switch s.kind { + case symNonterm: + n := sg.nts[s.nt] + if n.ruleName != "" { + return n.ruleName + } + if n.label != "" { + return n.label + } + return "nt" + strconv.Itoa(s.nt) + case symEps: + return "eps" + default: + if s.term != nil { + return sanitizeComment(s.term.String()) + } + return "?" + } +} + +func sanitizeComment(s string) string { + s = strings.ReplaceAll(s, "*/", "* /") + s = strings.ReplaceAll(s, "\r", " ") + s = strings.ReplaceAll(s, "\n", " ") + return s +} + +func foldByte(b byte) byte { + if b >= 'A' && b <= 'Z' { + return b - 'A' + 'a' + } + return b +} + +func foldRune(r rune) rune { + if r >= 'A' && r <= 'Z' { + return r - 'A' + 'a' + } + return r +} + +// headerBoilerplate: runtime types, GSS/BSR engine state. Grammar-independent. +// (Imports are emitted separately so unicode/utf8 is only pulled in when a +// terminal actually decodes runes.) The generated process()/seed() and metadata +// complete the file. +const headerBoilerplate = ` +// MaxElements bounds the BSR set; parsing aborts with ErrTooLarge beyond it. +// Set to 0 to disable the bound. +var MaxElements = 1 << 22 + +// ErrTooLarge is returned when the BSR set exceeds MaxElements. +var ErrTooLarge = errors.New("parse: BSR set exceeded MaxElements") + +const inf = -1 + +const ( + kNT = 0 + kTerm = 1 + kEps = 2 +) + +// sInfo decodes a slot id into (nonterminal, alternate, dot, production length). +type sInfo struct{ nt, alt, dot, plen int } + +// qSym is a query-only symbol descriptor (kind, and nt for nonterminals). +type qSym struct{ kind, nt int } + +// qNT is query-only nonterminal metadata. +type qNT struct { + isRule bool + ruleName string + alts [][]qSym +} + +type bsrElem struct { + sl int + l, k, r int +} + +type gssKey struct { + ret int + pos int + rc int +} + +type gnode struct { + ret int + pos int + rc int + edges []*gnode + edgeSet map[*gnode]bool +} + +type desc struct { + L, i, rc int + u *gnode +} + +type parser struct { + input []byte + set map[bsrElem]bool + gss map[gssKey]*gnode + u0 *gnode + seen map[desc]bool + work []desc + popped map[*gnode]map[int]bool + aborted bool +} + +func foldB(b byte) byte { + if b >= 'A' && b <= 'Z' { + return b - 'A' + 'a' + } + return b +} + +func foldR(r rune) rune { + if r >= 'A' && r <= 'Z' { + return r - 'A' + 'a' + } + return r +} + +func bytesEqual(a, b []byte) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} + +func (p *parser) add(L int, u *gnode, i, rc int) { + d := desc{L: L, i: i, rc: rc, u: u} + if p.seen[d] { + return + } + p.seen[d] = true + p.work = append(p.work, d) +} + +func (p *parser) gssNodeFor(ret, pos, rc int) *gnode { + k := gssKey{ret, pos, rc} + if v, ok := p.gss[k]; ok { + return v + } + v := &gnode{ret: ret, pos: pos, rc: rc, edgeSet: map[*gnode]bool{}} + p.gss[k] = v + return v +} + +// record inserts a BSR element, keeping only intermediate (prefix length >= 2) +// or complete (dot == len) extents. +func (p *parser) record(L, l, k, r int) { + si := slotInfo[L] + if si.dot >= 2 || si.dot == si.plen { + if MaxElements > 0 && len(p.set) >= MaxElements { + p.aborted = true + return + } + p.set[bsrElem{L, l, k, r}] = true + } +} + +func (p *parser) create(ret int, u *gnode, i, rc int) *gnode { + v := p.gssNodeFor(ret, i, rc) + if !v.edgeSet[u] { + v.edgeSet[u] = true + v.edges = append(v.edges, u) + for j := range p.popped[v] { + p.record(ret, u.pos, i, j) + p.add(ret, u, j, rc) + } + } + return v +} + +func (p *parser) pop(u *gnode, j int) { + if u == p.u0 { + return + } + if p.popped[u] == nil { + p.popped[u] = map[int]bool{} + } + p.popped[u][j] = true + for _, v := range u.edges { + p.record(u.ret, v.pos, u.pos, j) + p.add(u.ret, v, j, u.rc) + } +} +` + +// queryBoilerplate: Result, reconstruction queries (over slotInfo/ntComplete/ +// qNTs), and the public Parse. Grammar-independent. +const queryBoilerplate = ` +// ---- result + reconstruction queries ---- + +type ParseTree struct { + Rule string + Start, End int + Children []*ParseTree +} + +type bsrKey struct { + sl int + l, r int +} + +type nodeID struct { + sl int // slot id for intermediate nodes; -1 for symbol nodes + nt int // nonterminal id for symbol nodes + l, r int +} + +// Result holds the BSR set for a parse and answers queries over it. +type Result struct { + input []byte + set map[bsrElem]bool + index map[bsrKey][]int + n int +} + +func (r *Result) buildIndex() { + if r.index != nil { + return + } + r.index = make(map[bsrKey][]int, len(r.set)) + for e := range r.set { + bk := bsrKey{e.sl, e.l, e.r} + r.index[bk] = append(r.index[bk], e.k) + } +} + +// Valid reports whether the whole input is derivable by the root rule. +func (r *Result) Valid() bool { + r.buildIndex() + for _, sl := range ntComplete[gStart] { + if len(r.index[bsrKey{sl, 0, r.n}]) > 0 { + return true + } + } + return false +} + +// Elements returns the number of BSR elements (representation size). +func (r *Result) Elements() int { return len(r.set) } + +// NumTrees returns the number of distinct parse trees, or -1 for infinitely many. +func (r *Result) NumTrees() *big.Int { + if !r.Valid() { + return big.NewInt(0) + } + memo := map[nodeID]*big.Int{} + onStack := map[nodeID]bool{} + infinite := false + + var symWays func(s qSym, a, b int) *big.Int + var countSym func(nt, l, rr int) *big.Int + var countInter func(sl, l, rr int) *big.Int + var countElem func(sl, l, k, rr int) *big.Int + + symWays = func(s qSym, a, b int) *big.Int { + if s.kind == kNT { + return countSym(s.nt, a, b) + } + return big.NewInt(1) + } + countElem = func(sl, l, k, rr int) *big.Int { + si := slotInfo[sl] + prod := qNTs[si.nt].alts[si.alt] + last := symWays(prod[si.dot-1], k, rr) + var pre *big.Int + switch { + case si.dot == 1: + pre = big.NewInt(1) + case si.dot == 2: + pre = symWays(prod[0], l, k) + default: + pre = countInter(sl-1, l, k) + } + return new(big.Int).Mul(pre, last) + } + countInter = func(sl, l, rr int) *big.Int { + id := nodeID{sl: sl, nt: -1, l: l, r: rr} + if v, ok := memo[id]; ok { + return v + } + if onStack[id] { + infinite = true + return big.NewInt(1) + } + onStack[id] = true + total := big.NewInt(0) + for _, k := range r.index[bsrKey{sl, l, rr}] { + total.Add(total, countElem(sl, l, k, rr)) + } + onStack[id] = false + memo[id] = total + return total + } + countSym = func(nt, l, rr int) *big.Int { + id := nodeID{sl: -1, nt: nt, l: l, r: rr} + if v, ok := memo[id]; ok { + return v + } + if onStack[id] { + infinite = true + return big.NewInt(1) + } + onStack[id] = true + total := big.NewInt(0) + for _, sl := range ntComplete[nt] { + for _, k := range r.index[bsrKey{sl, l, rr}] { + total.Add(total, countElem(sl, l, k, rr)) + } + } + onStack[id] = false + memo[id] = total + return total + } + res := countSym(gStart, 0, r.n) + if infinite { + return big.NewInt(-1) + } + return res +} + +// Ambiguous reports whether the input has more than one distinct parse tree. +func (r *Result) Ambiguous() bool { + if !r.Valid() { + return false + } + visited := map[nodeID]bool{} + var symAmb func(nt, l, rr int) bool + var interAmb func(sl, l, rr int) bool + var elemAmb func(sl, l, k, rr int) bool + var symOf func(s qSym, a, b int) bool + symOf = func(s qSym, a, b int) bool { + if s.kind == kNT { + return symAmb(s.nt, a, b) + } + return false + } + elemAmb = func(sl, l, k, rr int) bool { + si := slotInfo[sl] + prod := qNTs[si.nt].alts[si.alt] + if symOf(prod[si.dot-1], k, rr) { + return true + } + switch { + case si.dot == 1: + return false + case si.dot == 2: + return symOf(prod[0], l, k) + default: + return interAmb(sl-1, l, k) + } + } + interAmb = func(sl, l, rr int) bool { + id := nodeID{sl: sl, nt: -1, l: l, r: rr} + if visited[id] { + return false + } + visited[id] = true + ks := r.index[bsrKey{sl, l, rr}] + if len(ks) > 1 { + return true + } + for _, k := range ks { + if elemAmb(sl, l, k, rr) { + return true + } + } + return false + } + symAmb = func(nt, l, rr int) bool { + id := nodeID{sl: -1, nt: nt, l: l, r: rr} + if visited[id] { + return false + } + visited[id] = true + packs := 0 + for _, sl := range ntComplete[nt] { + packs += len(r.index[bsrKey{sl, l, rr}]) + } + if packs > 1 { + return true + } + for _, sl := range ntComplete[nt] { + for _, k := range r.index[bsrKey{sl, l, rr}] { + if elemAmb(sl, l, k, rr) { + return true + } + } + } + return false + } + return symAmb(gStart, 0, r.n) +} + +// Tree extracts a single parse tree (first element at each extent), or nil if +// the input is invalid. Synthetic nonterminals are collapsed to rule names. +func (r *Result) Tree() *ParseTree { + if !r.Valid() { + return nil + } + for _, sl := range ntComplete[gStart] { + if ks := r.index[bsrKey{sl, 0, r.n}]; len(ks) > 0 { + var kids []*ParseTree + r.collectElem(sl, 0, ks[0], r.n, &kids, map[nodeID]bool{}) + return &ParseTree{Rule: qNTs[gStart].ruleName, Start: 0, End: r.n, Children: kids} + } + } + return nil +} + +func (r *Result) collectElem(sl, l, k, rr int, out *[]*ParseTree, visited map[nodeID]bool) { + si := slotInfo[sl] + prod := qNTs[si.nt].alts[si.alt] + switch { + case si.dot == 1: + case si.dot == 2: + r.collectChild(prod[0], l, k, out, visited) + default: + if ks := r.index[bsrKey{sl - 1, l, k}]; len(ks) > 0 { + r.collectElem(sl-1, l, ks[0], k, out, visited) + } + } + r.collectChild(prod[si.dot-1], k, rr, out, visited) +} + +func (r *Result) collectChild(s qSym, a, b int, out *[]*ParseTree, visited map[nodeID]bool) { + if s.kind != kNT { + if s.kind == kEps { + return + } + *out = append(*out, &ParseTree{Start: a, End: b}) + return + } + id := nodeID{sl: -1, nt: s.nt, l: a, r: b} + if visited[id] { + return + } + visited[id] = true + for _, sl := range ntComplete[s.nt] { + if ks := r.index[bsrKey{sl, a, b}]; len(ks) > 0 { + if qNTs[s.nt].isRule { + var kids []*ParseTree + r.collectElem(sl, a, ks[0], b, &kids, visited) + *out = append(*out, &ParseTree{Rule: qNTs[s.nt].ruleName, Start: a, End: b, Children: kids}) + } else { + r.collectElem(sl, a, ks[0], b, out, visited) + } + visited[id] = false + return + } + } + visited[id] = false +} + +// Parse parses input under the baked root rule using the specialised engine. +func Parse(input []byte) (*Result, error) { + p := &parser{ + input: input, + set: map[bsrElem]bool{}, + gss: map[gssKey]*gnode{}, + seen: map[desc]bool{}, + popped: map[*gnode]map[int]bool{}, + } + p.u0 = &gnode{ret: -1, edgeSet: map[*gnode]bool{}} + p.seed() + for len(p.work) > 0 { + if p.aborted { + return nil, ErrTooLarge + } + d := p.work[len(p.work)-1] + p.work = p.work[:len(p.work)-1] + p.process(d) + } + return &Result{input: input, set: p.set, n: len(input)}, nil +} +` diff --git a/vendor/github.com/pandatix/go-abnf/dag.go b/vendor/github.com/pandatix/go-abnf/dag.go new file mode 100644 index 00000000000..4d004e41379 --- /dev/null +++ b/vendor/github.com/pandatix/go-abnf/dag.go @@ -0,0 +1,237 @@ +package goabnf + +import ( + "fmt" + "slices" + "strings" +) + +type node struct { + Rulename string + index, lowlink int + onStack bool + Dependencies []string +} + +// Depgraph contains for each rule the rules it depends on. +// Notice it does not differentiate between a mandatory dependency +// or an avoidable one. +type Depgraph map[string]*node + +// DependencyGraph creates a dependency graph for a whole grammar. +// It includes core rules only if necessary. +func (g *Grammar) DependencyGraph() Depgraph { + graph := Depgraph{} + for _, corerule := range coreRules { + graph[strings.ToLower(corerule.Name)] = &node{ + Rulename: corerule.Name, + Dependencies: getDependencies(corerule.Alternation), + } + } + for _, rule := range g.Rulemap { + graph[strings.ToLower(rule.Name)] = &node{ + Rulename: rule.Name, + Dependencies: getDependencies(rule.Alternation), + } + } + return graph +} + +func getDependencies(alt Alternation) []string { + deps := []string{} + for _, conc := range alt.Concatenations { + for _, rep := range conc.Repetitions { + switch v := rep.Element.(type) { + case ElemGroup: + deps = appendDeps(deps, getDependencies(v.Alternation)...) + case ElemOption: + deps = appendDeps(deps, getDependencies(v.Alternation)...) + case ElemRulename: + deps = appendDeps(deps, strings.ToLower(v.Name)) + } + } + } + return deps +} + +func appendDeps(deps []string, ndeps ...string) []string { + for _, ndep := range ndeps { + if !slices.Contains(deps, ndep) { + deps = append(deps, ndep) + } + } + return deps +} + +// Mermaid returns a flowchart of the dependency graph. +func (dg Depgraph) Mermaid() string { + var out strings.Builder + out.WriteString("flowchart TD\n") + for _, node := range dg { + for _, dep := range node.Dependencies { + fmt.Fprintf(&out, "\t%s --> %s\n", node.Rulename, dep) + } + out.WriteString("\n") + } + return out.String() +} + +// IsDag find Strongly Connected Components using Tarjan's algorithm +// and returns whether it contains cycle or not. +// Could be improved by Nuutila's or Pearce's algorithms, or replaced +// by Kosaraju's algorithm. +func (g *Grammar) IsDAG() bool { + scc := &cycle{ + index: 0, + stack: []*node{}, + dg: g.DependencyGraph(), + } + scc.find() + + for _, sccs := range scc.sccs { + if len(sccs) > 1 { + return false + } + } + return true +} + +// RuleContainsCycle returns whether the rule contains a cycle or not. +// It travels through the whole rule dependency graph, such that +// it checks if the rule is cyclic AND if one of its dependency is too. +func (g *Grammar) RuleContainsCycle(rulename string) (bool, error) { + // Check the rule exists + rule := GetRule(rulename, g.Rulemap) + if rule == nil { + return false, &ErrRuleNotFound{ + Rulename: rulename, + } + } + + // Get all SCCs + scc := &cycle{ + index: 0, + stack: []*node{}, + dg: g.DependencyGraph(), + } + scc.find() + + return ruleContainsCycle(scc.sccs, rulename), nil +} + +func ruleContainsCycle(sccs [][]*node, rulename string) bool { + // Index nodes by lowercase name and flag those sitting in a non-trivial SCC + // (size > 1 == a cycle). Precomputing this removes the O(V) SCC scan that + // the previous implementation did on every recursive step. + nodeOf := make(map[string]*node, len(sccs)) + inCycle := make(map[string]bool, len(sccs)) + + for _, s := range sccs { + nontrivial := len(s) > 1 + for _, nd := range s { + key := strings.ToLower(nd.Rulename) + nodeOf[key] = nd + inCycle[key] = nontrivial + } + } + return ruleCycleVisit(strings.ToLower(rulename), nodeOf, inCycle, map[string]bool{}) +} + +// ruleCycleVisit reports whether the rule (or any transitive dependency) is +// cyclic. The visited set keeps each rule explored at most once: without it a +// diamond-shaped dependency DAG is walked along every root-to-leaf path, which +// is exponential. Marking on entry is safe because a cyclic rule returns true +// here before it can ever be marked-and-skipped, so a memoized rule is always +// genuinely acyclic. +func ruleCycleVisit(key string, nodeOf map[string]*node, inCycle, visited map[string]bool) bool { + if visited[key] { + return false + } + visited[key] = true + nd := nodeOf[key] + if nd == nil { + // Unknown rule: treat as acyclic (matches the previous behaviour). + return false + } + + // Self-loop (a size-1 SCC with a self-edge is not flagged by inCycle). + for _, dep := range nd.Dependencies { + if strings.ToLower(dep) == key { + return true + } + } + if inCycle[key] { + // If it depends on itself or is part of an SCC, then is cylic + return true + } + + for _, dep := range nd.Dependencies { + dk := strings.ToLower(dep) + if dk == key { + continue + } + if ruleCycleVisit(dk, nodeOf, inCycle, visited) { + return true + } + } + return false +} + +type cycle struct { + index int + stack []*node + sccs [][]*node + + dg Depgraph +} + +func (c *cycle) find() { + for _, v := range c.dg { + if v.index == 0 { + c.strongconnect(v) + } + } +} + +func (c *cycle) strongconnect(v *node) { + // Set the depth index for v to the smallest unused index + v.index = c.index + v.lowlink = c.index + c.index++ + c.stack = append(c.stack, v) + v.onStack = true + + // Consider successors of v + for _, dep := range v.Dependencies { + w, ok := c.dg[dep] + if !ok { + // core rules, as we know they won't have a cycle thus + // no SCC, we don't need to recurse. + continue + } + if w.index == 0 { + // Successor w has not yet been visited; recurse on it + c.strongconnect(w) + v.lowlink = min(v.lowlink, w.lowlink) + } else { + if w.onStack { + // Successor w is in stack S and hence in the current SCC + // If w is not on stack, then (v, w) is an edge pointing + // to an SCC already found and must be ignored. + v.lowlink = min(v.lowlink, w.index) + } + } + } + // If v is a root node, pop the stack and generate an SCC + if v.lowlink == v.index { + scc := []*node{} + w := (*node)(nil) + for w == nil || !strings.EqualFold(v.Rulename, w.Rulename) { + w = c.stack[len(c.stack)-1] + c.stack = c.stack[:len(c.stack)-1] + w.onStack = false + scc = append(scc, w) + } + c.sccs = append(c.sccs, scc) + } +} diff --git a/vendor/github.com/pandatix/go-abnf/errors.go b/vendor/github.com/pandatix/go-abnf/errors.go new file mode 100644 index 00000000000..ea39058a1e1 --- /dev/null +++ b/vendor/github.com/pandatix/go-abnf/errors.go @@ -0,0 +1,245 @@ +package goabnf + +import ( + "errors" + "fmt" + "sort" + "strings" +) + +var ( + // ErrNoSolutionFound is an error returned when parsing an ABNF + // grammar and no solution has been found. + // + // On the ABNF parsing path it is wrapped by a [*ParseError] carrying the + // position at which parsing got stuck; errors.Is(err, ErrNoSolutionFound) + // keeps matching. + ErrNoSolutionFound = errors.New("no solution found, input ABNF grammar may be invalid") + + // ErrHandlingProseVal is an error returned when an operation tried + // to produce something on a prose-val, but it can't be handled properly. + ErrHandlingProseVal = errors.New("can't handle prose-val descriptions") + + // ErrProseValInTransitionGraph is returned when building a transition graph + // from a grammar that contains a prose-val, which has no defined expansion. + ErrProseValInTransitionGraph = errors.New("prose value is not supported in transition graphs") + + // ErrRepetitionThreshold is returned when expanding a repetition whose bound + // exceeds the threshold set with WithRepetitionThreshold. + ErrRepetitionThreshold = errors.New("repetition threshold reached") + + // ErrRepetitionMinNotOne is returned when a mandatory (max == 1) repetition + // has a minimum other than 1. + ErrRepetitionMinNotOne = errors.New("repetition minimum must be 1 when maximum is 1") + + // ErrRepetitionMinGtMax is returned when a repetition has a minimum greater + // than its maximum. + ErrRepetitionMinGtMax = errors.New("repetition minimum is greater than maximum") +) + +// ParseError reports where parsing input against a grammar got stuck. It is +// returned (wrapped around [ErrNoSolutionFound]) when an input does not parse, +// to help locate the offending construct -- in particular when writing or +// debugging an ABNF grammar. +type ParseError struct { + // Offset is the byte offset into the input of the furthest position the + // parser was able to reach before failing. + Offset int + // Line and Col are the 1-based line and column of Offset. + Line, Col int + // Expected lists the terminals the parser could have consumed at Offset, if + // that information was available. It may be empty. + Expected []string + // Found describes the input byte at Offset (e.g. `%x0A (LF)`), or "EOF" when + // Offset is at the end of input. It may be empty. + Found string + // Hint, when set, gives a plain-language diagnosis of a common mistake (e.g. + // bare LF line endings where ABNF requires CR LF). + Hint string +} + +var _ error = (*ParseError)(nil) + +func (e *ParseError) Error() string { + var b strings.Builder + fmt.Fprintf(&b, "parse error at line %d, column %d (offset %d)", e.Line, e.Col, e.Offset) + switch { + case len(e.Expected) > 0: + fmt.Fprintf(&b, ": expected %s", strings.Join(e.Expected, ", ")) + if e.Found != "" { + fmt.Fprintf(&b, ", found %s", e.Found) + } + case e.Found != "": + fmt.Fprintf(&b, ": unexpected %s", e.Found) + default: + fmt.Fprintf(&b, ": %s", ErrNoSolutionFound) + } + if e.Hint != "" { + fmt.Fprintf(&b, " (hint: %s)", e.Hint) + } + return b.String() +} + +// Unwrap reports ErrNoSolutionFound so that callers matching the historical +// sentinel with errors.Is keep working. +func (e *ParseError) Unwrap() error { return ErrNoSolutionFound } + +// describeByte renders an input byte the way ABNF terminals are written, naming +// the control characters that matter most when debugging a grammar. +func describeByte(b byte) string { + switch b { + case '\r': + return "%x0D (CR)" + case '\n': + return "%x0A (LF)" + case '\t': + return "%x09 (HTAB)" + case ' ': + return "%x20 (SP)" + } + if b >= 0x20 && b < 0x7f { + return fmt.Sprintf("%q", string(b)) + } + return fmt.Sprintf("%%x%02X", b) +} + +// newParseError builds a ParseError from a byte offset into input, computing the +// 1-based line and column, the byte found there, and a hint for the bare-LF +// line-ending mistake (the most common ABNF authoring error). +func newParseError(input []byte, offset int, expected []string) *ParseError { + if offset < 0 { + offset = 0 + } + if offset > len(input) { + offset = len(input) + } + line, col := 1, 1 + for i := 0; i < offset; i++ { + if input[i] == '\n' { + line++ + col = 1 + } else { + col++ + } + } + exp := append([]string(nil), expected...) + sort.Strings(exp) + pe := &ParseError{Offset: offset, Line: line, Col: col, Expected: exp} + if offset < len(input) { + pe.Found = describeByte(input[offset]) + // Bare LF where the previous byte is not CR: ABNF lines end with CR LF, + // so a lone LF is almost always the cause. + if input[offset] == '\n' && (offset == 0 || input[offset-1] != '\r') { + pe.Hint = `input uses bare LF line endings, but ABNF requires CR LF ("\r\n")` + } + } else { + pe.Found = "EOF" + } + return pe +} + +// ErrMultipleSolutionsFound is an error returned when a parser found +// multiple solutions when none or one were expected. +type ErrMultipleSolutionsFound struct{} + +var _ error = (*ErrMultipleSolutionsFound)(nil) + +func (err ErrMultipleSolutionsFound) Error() string { + return "multiple solutions found, this should not happen. Please open an issue. This could eventually need an Erratum from IETF tracking" +} + +// ErrRuleNotFound is an error returned when the rule was not found +// as part of the grammar. +type ErrRuleNotFound struct { + Rulename string +} + +var _ error = (*ErrRuleNotFound)(nil) + +func (err ErrRuleNotFound) Error() string { + return fmt.Sprintf("rule %s was not found in grammar", err.Rulename) +} + +// ErrCoreRuleModify is an error returned when an incremental alternative +// for a core rule. +type ErrCoreRuleModify struct { + CoreRulename string +} + +var _ error = (*ErrCoreRuleModify)(nil) + +func (err ErrCoreRuleModify) Error() string { + return fmt.Sprintf("core rule %s can't be modified", err.CoreRulename) +} + +// ErrDependencyNotFound is an error returned during ABNF grammar +// semantic vaildation, if a rule depends on an unexisting rule. +type ErrDependencyNotFound struct { + Rulename string +} + +var _ error = (*ErrDependencyNotFound)(nil) + +func (err ErrDependencyNotFound) Error() string { + return fmt.Sprintf("unsatisfied dependency (rule) %s", err.Rulename) +} + +// ErrSemanticRepetition is an error returned during ABNF grammar +// semantic validation, if a repetition has min < max. +type ErrSemanticRepetition struct { + Repetition Repetition +} + +var _ error = (*ErrSemanticRepetition)(nil) + +func (err ErrSemanticRepetition) Error() string { + return fmt.Sprintf("invalid semantic of input ABNF grammar for repetition %s", err.Repetition) +} + +// ErrTooLargeNumeral is an error returned when the numeral value +// provided to parse cannot be handled as a 7-bit US-ASCII valid value. +type ErrTooLargeNumeral struct { + Base, Value string +} + +var _ error = (*ErrTooLargeNumeral)(nil) + +func (err ErrTooLargeNumeral) Error() string { + return fmt.Sprintf("too large numeral value %s for base %s", err.Value, err.Base) +} + +// ErrDuplicatedRule is an error returned when the rule already +// exist as part of the grammar. +type ErrDuplicatedRule struct { + Rulename string +} + +var _ error = (*ErrDuplicatedRule)(nil) + +func (err ErrDuplicatedRule) Error() string { + return fmt.Sprintf("rule %s was already defined in grammar", err.Rulename) +} + +// ErrCyclicRule is an error returned when can't work due to an +// unavoidable cyclic rule. +type ErrCyclicRule struct { + Rulename string +} + +var _ error = (*ErrCyclicRule)(nil) + +func (err ErrCyclicRule) Error() string { + return fmt.Sprintf("can't generate a content as the rule %s involves an unavoidable cycle", err.Rulename) +} + +// ErrMaxNodesExceeded is returned by TransitionGraph when construction would +// allocate more nodes than the budget set with WithMaxNodes. +type ErrMaxNodesExceeded struct { + Max int +} + +var _ error = (*ErrMaxNodesExceeded)(nil) + +func (err ErrMaxNodesExceeded) Error() string { + return fmt.Sprintf("transition graph node budget of %d exceeded", err.Max) +} diff --git a/vendor/github.com/pandatix/go-abnf/fuzz.go b/vendor/github.com/pandatix/go-abnf/fuzz.go new file mode 100644 index 00000000000..e8d79ff31aa --- /dev/null +++ b/vendor/github.com/pandatix/go-abnf/fuzz.go @@ -0,0 +1,775 @@ +package goabnf + +import ( + "fmt" + "math/rand" + "unicode/utf8" +) + +// fuzz.go turns a (fully expanded) transition graph into a total +// function from arbitrary bytes to a string the grammar admits. It is the core +// primitive for structure-aware fuzzing with testing.F: because every input +// maps to a valid production, the fuzzing engine's mutations explore the +// grammar's structure instead of bouncing off the target's first syntax check. +// +// The walk is deliberately different from a TransitionGraphReader: the reader +// enumerates entrypoint->endpoint trails (each directed edge at most once) for +// finite coverage, whereas the generator takes a free walk that MAY repeat +// edges -- so it can unroll a "*" repetition into "a", "aa", "aaa", ... -- and +// is kept finite by explicit budgets (max length, per-edge repetitions, total +// steps) rather than by the no-repeat rule. When a budget is hit the walk does +// not stop dead (that could leave it on a non-accepting node); it steers along +// shortest paths to the nearest endpoint, so the output is always a complete, +// valid production. + +// source supplies the decisions that drive a walk. Two implementations exist: a +// byte tape (for testing.F, where mutations of the tape are the fuzzing signal) +// and a *rand.Rand (for property tests and corpus generation). intn(n) returns +// a value in [0, n); when a tape is exhausted it returns 0, which makes +// generation total and biases an empty/short tape toward the shortest +// production. +type source interface { + intn(n int) int +} + +type tapeSource struct { + tape []byte + pos int +} + +func (s *tapeSource) intn(n int) int { + if n <= 1 { + return 0 + } + // Read just enough bytes to address n, big-endian. One byte per decision + // when n <= 256 keeps locality high: a single-byte mutation flips at most + // one decision (until the number of decisions changes). + width := 1 + for max := n; max > 256; max = (max + 255) / 256 { + width++ + } + v := 0 + for i := 0; i < width; i++ { + b := 0 + if s.pos < len(s.tape) { + b = int(s.tape[s.pos]) + s.pos++ + } + v = v<<8 | b + if v < 0 { // overflow guard on absurd widths + v &= 0x7fffffff + } + } + return v % n +} + +type randSource struct{ r *rand.Rand } + +func (s randSource) intn(n int) int { + if n <= 1 { + return 0 + } + return s.r.Intn(n) +} + +// indexedSource addresses the tape by decision number rather than a moving +// cursor: the i-th decision always reads the fixed-width slot [i*stride, ...], +// regardless of how many bytes earlier decisions "needed". This keeps a tape +// edit local -- it can only perturb the one decision whose slot it lands in -- +// so an upstream structural change no longer reframes the bytes every later +// decision reads (the residual non-locality is purely semantic: a forked walk +// reaches different nodes, where the same slot byte means a different choice). +// stride 4 addresses any choice count up to 2^32, well above the widest +// (full-Unicode) num-val range. +type indexedSource struct { + tape []byte + i int +} + +const indexedStride = 4 + +func (s *indexedSource) intn(n int) int { + if n <= 1 { + return 0 + } + off := s.i * indexedStride + s.i++ + v := 0 + for k := range indexedStride { + b := 0 + if off+k < len(s.tape) { + b = int(s.tape[off+k]) + } + v = v<<8 | b + if v < 0 { + v &= 0x7fffffff + } + } + return v % n +} + +// Generator produces strings admitted by a transition graph. +type Generator struct { + tg *TransitionGraph + + maxLength int + maxReps int + maxSteps int + + stableAddr bool + + endSet map[string]bool // node ID -> is an endpoint ("" == emptyNode) + canReach map[string]bool // node ID -> can still reach some endpoint + distToEnd map[string]int // node ID -> fewest steps to an endpoint + entries []*Node // entrypoints that can reach an endpoint + + emitCache map[string][]emission // node ID -> octet emissions (lazy, for ExpectedNext) + emitOK map[string]bool // node ID -> emissions analyzable +} + +// GenOption configures a Generator. +type GenOption interface { + apply(*Generator) +} + +type genOptionFunc func(*Generator) + +func (f genOptionFunc) apply(g *Generator) { + f(g) +} + +// WithMaxLength bounds the number of bytes a generated production may contain. +// Once reached the walk steers to the nearest endpoint. Defaults to 4096. +func WithMaxLength(n int) GenOption { + return genOptionFunc(func(g *Generator) { + g.maxLength = n + }) +} + +// WithMaxReps bounds how many times a single edge may be traversed, which caps +// how far repetitions ("*" / "1*" / "n*m") are unrolled. Defaults to 32. +func WithMaxReps(n int) GenOption { + return genOptionFunc(func(g *Generator) { + g.maxReps = n + }) +} + +// WithMaxSteps is a hard backstop on the total number of walk steps, defending +// against pathological graphs. Defaults to 100000. +func WithMaxSteps(n int) GenOption { + return genOptionFunc(func(g *Generator) { + g.maxSteps = n + }) +} + +// WithStableAddressing makes Generate address the tape by decision number +// (fixed-width slots) instead of a moving cursor. This improves locality for a +// coverage-guided engine -- a one-byte tape edit perturbs a single decision +// rather than reframing every decision downstream of it -- at the cost of +// consuming the tape less densely. Recommended for use under testing.F. +func WithStableAddressing() GenOption { + return genOptionFunc(func(g *Generator) { + g.stableAddr = true + }) +} + +// NewGenerator prepares a Generator from a transition graph. The graph must be +// fully expanded -- build it with WithDeflateRules(true) when the target rule +// references other rules -- because an unexpanded rule node carries no bytes and +// could not be turned into a valid production. It returns an error naming the +// first such node otherwise. (Recursive rules cannot be expanded at all; in that +// case TransitionGraph itself returns ErrCyclicRule before reaching here.) +func NewGenerator(tg *TransitionGraph, opts ...GenOption) (*Generator, error) { + g := &Generator{ + tg: tg, + maxLength: 4096, + maxReps: 32, + maxSteps: 100_000, + } + for _, o := range opts { + o.apply(g) + } + + // Collect every reachable node and the reverse adjacency, and verify the + // graph is fully expanded (only producing leaves and the empty node). + revAdj := map[string][]*Node{} + nodes := map[string]*Node{} + var stack []*Node + for _, e := range tg.Entrypoints { + if e != emptyNode { + stack = append(stack, e) + } + } + for len(stack) > 0 { + n := stack[len(stack)-1] + stack = stack[:len(stack)-1] + id := tgNodeID(n) + if _, ok := nodes[id]; ok { + continue + } + nodes[id] = n + if !producing(n) { + return nil, fmt.Errorf("transition graph contains an unexpanded rule node %q; build it with WithDeflateRules(true)", n.Elem.String()) + } + for _, sc := range n.Nexts { + if sc == emptyNode { + continue + } + revAdj[tgNodeID(sc)] = append(revAdj[tgNodeID(sc)], n) + stack = append(stack, sc) + } + } + + // endSet, then backward BFS from endpoints for canReach + distToEnd. + g.endSet = map[string]bool{} + g.canReach = map[string]bool{} + g.distToEnd = map[string]int{} + var q []string + for _, e := range tg.Endpoints { + id := tgNodeID(e) + g.endSet[id] = true + if !g.canReach[id] { + g.canReach[id] = true + g.distToEnd[id] = 0 + q = append(q, id) + } + } + for len(q) > 0 { + id := q[0] + q = q[1:] + for _, p := range revAdj[id] { + pid := tgNodeID(p) + if g.canReach[pid] { + continue + } + g.canReach[pid] = true + g.distToEnd[pid] = g.distToEnd[id] + 1 + q = append(q, pid) + } + } + + for _, e := range tg.Entrypoints { + if g.canReach[tgNodeID(e)] { + g.entries = append(g.entries, e) + } + } + return g, nil +} + +// Generate maps the tape to a production the grammar admits. It is total: any +// tape (including nil) yields a valid production; an exhausted tape biases +// toward the shortest completion. +func (g *Generator) Generate(tape []byte) []byte { + if g.stableAddr { + return g.walk(&indexedSource{tape: tape}, nil) + } + return g.walk(&tapeSource{tape: tape}, nil) +} + +// GenerateRand draws a production using r as the entropy source. +func (g *Generator) GenerateRand(r *rand.Rand) []byte { + return g.walk(randSource{r: r}, nil) +} + +// traceSeg records that out[off:off+length] was emitted by a node carrying elem. +type traceSeg struct { + off, length int + elem ElemItf +} + +func (g *Generator) walk(src source, trace *[]traceSeg) []byte { + out := []byte{} + if len(g.entries) == 0 { + return out + } + node := g.entries[src.intn(len(g.entries))] + + visits := map[string]int{} + steps := 0 + for { + if node != emptyNode && producing(node) { + _, vt := nodeEmit(node, 0) + if vt < 1 { + vt = 1 + } + var idx int32 + if vt > 1 { + idx = int32(src.intn(int(vt))) + } + b, _ := nodeEmit(node, idx) + if trace != nil && len(b) > 0 { + *trace = append(*trace, traceSeg{off: len(out), length: len(b), elem: node.Elem}) + } + out = append(out, b...) + } + + // The empty node is a terminal entry/endpoint sentinel with no + // successors; reaching it as an entrypoint means the whole production is + // nullable, so stopping here yields the (valid) empty production. + if node == emptyNode { + break + } + + id := tgNodeID(node) + atEnd := g.endSet[id] + + // liveAll: successors that can still reach an endpoint (used for + // termination steering). liveCapped: those not yet at the per-edge cap + // (used for ordinary random choice). + var liveAll, liveCapped []*Node + for _, sc := range node.Nexts { + if sc == emptyNode || !g.canReach[tgNodeID(sc)] { + continue + } + liveAll = append(liveAll, sc) + if visits[id+">"+tgNodeID(sc)] < g.maxReps { + liveCapped = append(liveCapped, sc) + } + } + if len(liveAll) == 0 { + break // terminal; canReach guarantees this node is an endpoint + } + + over := len(out) >= g.maxLength || steps >= g.maxSteps + terminating := over || len(liveCapped) == 0 + + var next *Node + if atEnd { + if terminating { + break + } + // One decision: 0 == stop here, otherwise continue to a successor. + c := src.intn(len(liveCapped) + 1) + if c == 0 { + break + } + next = liveCapped[c-1] + } else if terminating { + next = g.nearestToEnd(liveAll) + } else { + next = liveCapped[src.intn(len(liveCapped))] + } + + visits[id+">"+tgNodeID(next)]++ + steps++ + node = next + } + return out +} + +// nearestToEnd returns the candidate with the fewest steps to an endpoint. +// Following it repeatedly walks a shortest path to an endpoint (distToEnd +// strictly decreases), so the termination phase never repeats an edge. +func (g *Generator) nearestToEnd(cands []*Node) *Node { + best := cands[0] + bestD := g.distToEnd[tgNodeID(best)] + for _, c := range cands[1:] { + if d := g.distToEnd[tgNodeID(c)]; d < bestD { + best, bestD = c, d + } + } + return best +} + +// From here, generate *negative* test inputs: strings that lie just outside the +// grammar, each paired with a label describing the violation. They are the +// complement of Generate's valid productions -- a correct target MUST reject them +// -- and they reach the error-handling paths (truncation, boundary overruns, +// trailing input) where many parser bugs live and which a valid-only generator +// can never exercise. +// +// A perturbation is NOT assumed invalid: grammars can be ambiguous or redundant +// (flipping "a" to "b" in `"a" / "b"` is still valid), so every candidate is +// checked against the grammar and only genuinely-rejected ones are returned. +// This makes the "target must reject" oracle sound. + +// NearMiss returns a string just outside the grammar together with a label +// describing how it violates the grammar, or ok=false if no minimal +// perturbation within the attempt budget produced a string the grammar rejects. +// It is deterministic in tape and total. +func (g *Generator) NearMiss(tape []byte) (production []byte, label string, ok bool) { + src := &tapeSource{tape: tape} + var trace []traceSeg + base := g.walk(src, &trace) + + for _, c := range g.nearMissCandidates(src, base, trace) { + if c.bytes == nil || string(c.bytes) == string(base) { + continue + } + valid, err := g.tg.grammar.IsValid(g.tg.rulename, c.bytes) + if err == nil && !valid { + return c.bytes, c.label, true + } + } + return nil, "", false +} + +// NearMissRand is the *rand.Rand-driven variant of NearMiss. +func (g *Generator) NearMissRand(r *rand.Rand) (production []byte, label string, ok bool) { + // Draw a tape and reuse the deterministic path so behaviour is identical. + tape := make([]byte, 64) + r.Read(tape) + return g.NearMiss(tape) +} + +type candidate struct { + bytes []byte + label string +} + +func cloneBytes(b []byte) []byte { + c := make([]byte, len(b)) + copy(c, b) + return c +} + +// nearMissCandidates proposes minimal perturbations of base, ordered with a +// little entropy from src so different tapes explore different operators first. +// Correctness does not depend on the order: NearMiss verifies each against the +// grammar. +func (g *Generator) nearMissCandidates(src source, base []byte, trace []traceSeg) []candidate { + var cs []candidate + + // (1) Boundary violations: take a num-val range segment one step past its + // edge. These are the high-value off-by-one cases. + for _, seg := range trace { + nv, isRange := numvalRange(seg.elem) + if !isRange { + continue + } + for _, b := range []struct { + r rune + what string + }{ + {rune(nv.min - 1), "below"}, + {rune(nv.max + 1), "above"}, + } { + if b.r < 0 || b.r > 0x10FFFF { + continue + } + cand := cloneBytes(base) + repl := []byte(string(b.r)) + out := append(append(cloneBytes(cand[:seg.off]), repl...), cand[seg.off+seg.length:]...) + cs = append(cs, candidate{out, fmt.Sprintf("byte at offset %d is one %s num-val range %s", seg.off, b.what, nv.label)}) + } + } + + // (2) Off-alphabet substitution at a chosen position: replace one byte with + // a "wild" value. Verified, so even an unlucky pick is safe. + if len(base) > 0 { + pos := src.intn(len(base)) + for _, nb := range []byte{0x00, 0xFF, base[pos] ^ 0x20, base[pos] + 1, '!'} { + if nb == base[pos] { + continue + } + cand := cloneBytes(base) + cand[pos] = nb + cs = append(cs, candidate{cand, fmt.Sprintf("byte at offset %d substituted with 0x%02x", pos, nb)}) + } + } + + // (3) Truncation: drop a suffix (incomplete input -- exercises overread / + // "premature end" handling). + for _, n := range []int{1, 2, len(base) / 2, len(base) - 1} { + if n <= 0 || n > len(base) { + continue + } + cs = append(cs, candidate{cloneBytes(base[:len(base)-n]), fmt.Sprintf("truncated by %d byte(s) (len %d)", n, len(base)-n)}) + } + + // (4) Trailing input: append a byte (exercises "ignores trailing garbage"). + for _, nb := range []byte{0x00, '!', 0xFF, 'A'} { + out := append(cloneBytes(base), nb) + cs = append(cs, candidate{out, fmt.Sprintf("trailing byte 0x%02x appended", nb)}) + } + + // (5) Deletion of a single interior byte. + if len(base) >= 2 { + pos := src.intn(len(base)) + out := append(cloneBytes(base[:pos]), base[pos+1:]...) + cs = append(cs, candidate{out, fmt.Sprintf("byte at offset %d deleted", pos)}) + } + + // Light entropy-driven rotation so the first verified hit varies by tape. + if len(cs) > 1 { + k := src.intn(len(cs)) + cs = append(cs[k:], cs[:k]...) + } + return cs +} + +type numvalInfo struct { + min, max int32 + label string +} + +// numvalRange reports whether elem is a num-val range and, if so, its bounds. +func numvalRange(elem ElemItf) (numvalInfo, bool) { + nv, ok := elem.(ElemNumVal) + if !ok || nv.Status != StatRange { + return numvalInfo{}, false + } + min, max := numvalToInt32(nv.Elems[0], nv.Base), numvalToInt32(nv.Elems[1], nv.Base) + // Clamp the upper bound to the Unicode range so the "just above" boundary + // probe (max+1) stays a meaningful rune rather than overflowing. + if max > utf8.MaxRune { + max = utf8.MaxRune + } + return numvalInfo{min: min, max: max, label: elem.String()}, true +} + +// From here answers for a given prefix "which octets may legally come next, and +// is the prefix itself a complete word?" by simulating the transition-graph +// automaton over the prefix and reading the frontier. The graph is not a plain +// byte DFA -- each node emits a multi-octet string with per-position choices +// (letter casings, num-val ranges) -- so this is an NFA-style frontier simulation, +// not a single-state walk. +// +// Its headline use is sharpening NearMiss: appending an octet that is NOT in the +// expected-next set yields a string that is provably not a prefix of any word in +// the language, i.e. guaranteed invalid, with a precise label. It also enables +// error-position testing ("the target must fail at byte N"). + +// octetSet is a 256-bit set of octets. +type octetSet [4]uint64 + +func (s *octetSet) add(b byte) { + s[b>>6] |= 1 << (b & 63) +} + +func (s octetSet) has(b byte) bool { + return s[b>>6]&(1<<(b&63)) != 0 +} + +func (s octetSet) union(o octetSet) octetSet { + return octetSet{s[0] | o[0], s[1] | o[1], s[2] | o[2], s[3] | o[3]} +} + +func (s octetSet) bytes() []byte { + var out []byte + for b := range 256 { + if s.has(byte(b)) { + out = append(out, byte(b)) + } + } + return out +} + +// emission is one fixed-length alternative of a node's output: pos[j] is the set +// of octets the node may emit at byte offset j. A node usually has exactly one +// emission; a num-val range that spans several UTF-8 lengths has one per length. +type emission struct { + pos []octetSet +} + +// expectedNextRangeCap bounds how many runes a multi-byte num-val range is +// expanded into for analysis; beyond it ExpectedNext reports ok=false rather +// than doing unbounded work. ASCII ranges are always handled (<=128 octets). +const expectedNextRangeCap = 1 << 16 + +func (g *Generator) emissions(n *Node) ([]emission, bool) { + id := tgNodeID(n) + if g.emitCache == nil { + g.emitCache = map[string][]emission{} + g.emitOK = map[string]bool{} + } + if e, seen := g.emitCache[id]; seen { + return e, g.emitOK[id] + } + e, ok := nodeEmissions(n) + g.emitCache[id] = e + g.emitOK[id] = ok + return e, ok +} + +func nodeEmissions(n *Node) ([]emission, bool) { + switch v := n.Elem.(type) { + case ElemCharVal: + e := emission{} + for _, r := range v.Values { + if !v.Sensitive && ((r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z')) { + lo, up := r, r + if lo >= 'A' && lo <= 'Z' { + lo = lo - 'A' + 'a' + } + if up >= 'a' && up <= 'z' { + up = up - 'a' + 'A' + } + var s octetSet + s.add(byte(lo)) + s.add(byte(up)) + e.pos = append(e.pos, s) + } else { + for _, b := range []byte(string(r)) { + var s octetSet + s.add(b) + e.pos = append(e.pos, s) + } + } + } + return []emission{e}, true + + case ElemNumVal: + switch v.Status { + case StatSeries: + e := emission{} + for _, el := range v.Elems { + r := numvalToRune(el, v.Base) + if !utf8.ValidRune(r) { + // An out-of-range element can never appear in UTF-8 input. + return nil, false + } + for _, b := range []byte(string(r)) { + var s octetSet + s.add(b) + e.pos = append(e.pos, s) + } + } + return []emission{e}, true + case StatRange: + min, max := numvalToInt32(v.Elems[0], v.Base), numvalToInt32(v.Elems[1], v.Base) + // Clamp to the Unicode range first: otherwise an out-of-range bound + // near MaxInt32 overflows the int32 count below, bypasses the cap + // guard, and turns the enumeration loop into a DoS. A range wholly + // above U+10FFFF emits nothing. + if max > utf8.MaxRune { + max = utf8.MaxRune + } + if min > utf8.MaxRune || min > max { + return nil, false + } + if max <= 0x7f { // ASCII: a single one-octet emission + var s octetSet + for b := min; b <= max && b <= 0x7f; b++ { // explicit ASCII bound for the conversion + s.add(byte(b)) + } + return []emission{{pos: []octetSet{s}}}, true + } + if max-min+1 > expectedNextRangeCap { + return nil, false + } + byLen := map[int]*emission{} + for r := min; r <= max; r++ { + bs := []byte(string(rune(r))) + e := byLen[len(bs)] + if e == nil { + e = &emission{pos: make([]octetSet, len(bs))} + byLen[len(bs)] = e + } + for j, b := range bs { + e.pos[j].add(b) + } + } + out := make([]emission, 0, len(byLen)) + for _, e := range byLen { + out = append(out, *e) + } + return out, true + } + } + return nil, false +} + +// ExpectedNext simulates the automaton over prefix and reports the octets that +// may legally follow it, whether prefix is itself a complete word, and whether +// the analysis succeeded (ok is false only when the graph uses a feature this +// analyzer declines to expand, e.g. an enormous multi-byte num-val range). When +// prefix is not a viable prefix of any word, next is empty and accepts is false. +func (g *Generator) ExpectedNext(prefix []byte) (next []byte, accepts bool, ok bool) { + type state struct { + n *Node + e, p int + } + ready := []state{} + seen := map[state]bool{} + push := func(s state) { + if !seen[s] { + seen[s] = true + ready = append(ready, s) + } + } + start := func(n *Node) bool { + ems, o := g.emissions(n) + if !o { + return false + } + for ei, em := range ems { + if len(em.pos) > 0 { + push(state{n, ei, 0}) + } + } + return true + } + for _, e := range g.tg.Entrypoints { + if e == emptyNode { + continue + } + if !start(e) { + return nil, false, false + } + } + + lastCompletedEndpoint := false + consumedAll := true + for idx, b := range prefix { + next2 := []state{} + seen2 := map[state]bool{} + push2 := func(s state) { + if !seen2[s] { + seen2[s] = true + next2 = append(next2, s) + } + } + lastCompletedEndpoint = false + for _, s := range ready { + ems, _ := g.emissions(s.n) + em := ems[s.e] + if !em.pos[s.p].has(b) { + continue + } + if s.p+1 == len(em.pos) { // node s.n completes on this octet + if g.endSet[tgNodeID(s.n)] { + lastCompletedEndpoint = true + } + for _, m := range s.n.Nexts { + if m == emptyNode { + continue + } + ems2, o := g.emissions(m) + if !o { + return nil, false, false + } + for ei, em2 := range ems2 { + if len(em2.pos) > 0 { + push2(state{m, ei, 0}) + } + } + } + } else { + push2(state{s.n, s.e, s.p + 1}) + } + } + ready, seen = next2, seen2 + if len(ready) == 0 { + if idx != len(prefix)-1 { + consumedAll = false // dead before the end: prefix not viable + } + break + } + } + + if !consumedAll { + return nil, false, true + } + if len(prefix) == 0 { + accepts = g.endSet[""] // empty word accepted iff emptyNode is an endpoint + } else { + accepts = lastCompletedEndpoint + } + var acc octetSet + for _, s := range ready { + ems, _ := g.emissions(s.n) + acc = acc.union(ems[s.e].pos[s.p]) + } + return acc.bytes(), accepts, true +} diff --git a/vendor/github.com/pandatix/go-abnf/generate.go b/vendor/github.com/pandatix/go-abnf/generate.go new file mode 100644 index 00000000000..179db934935 --- /dev/null +++ b/vendor/github.com/pandatix/go-abnf/generate.go @@ -0,0 +1,253 @@ +package goabnf + +import ( + "errors" + "maps" + "math/rand" + "strings" + "unicode/utf8" +) + +// Generate is an experimental feature that consumes a seed for +// a pseudo-random number generator, used to randomly travel through +// the grammar given a rulename to work on. +// With this, it provides reproducibility, usefull for fuzz crashers. +// +// You can leverage its capacities (maximum repetitions, length +// threshold, etc.) with the functional options. +// +// It is a good capability for testing and fuzzing parsers during +// testing, compliance, fuzzing or optimization. +func (g *Grammar) Generate(seed int64, rulename string, opts ...GenerateOption) ([]byte, error) { + if err := checkCanGenerateSafely(g, rulename); err != nil { + return nil, err + } + + // Use a pseudo-random number generator + rand := rand.NewSource(seed) + + // Define conditions from options + options := &genOpts{ + repMax: 16, + threshold: 256, + } + for _, opt := range opts { + opt.apply(options) + } + + // Generate actual content (rule exists, checked first) + rule := GetRule(rulename, g.Rulemap) + out := []byte{} + generateAlt(rand, g, &out, rule.Alternation, options) + return out, nil +} + +func generateAlt(rand rand.Source, g *Grammar, out *[]byte, alt Alternation, options *genOpts) { + // Select any possible path in the alternation + cnt := alt.Concatenations[int(rand.Int63())%len(alt.Concatenations)] + + // Travel through all the repetitions + for _, rep := range cnt.Repetitions { + repmax := rep.Max + if repmax == inf { + repmax = options.repMax + } + torep := rep.Min + int(rand.Int63())%(repmax-rep.Min+1) + for i := 0; generateKeepGoing(i, torep, rep.Min, len(*out), options.threshold); i++ { + switch elem := rep.Element.(type) { + case ElemRulename: + rule := GetRule(elem.Name, g.Rulemap) + generateAlt(rand, g, out, rule.Alternation, options) + + case ElemOption: + if (rand.Int63() % 2) == 0 { + generateAlt(rand, g, out, elem.Alternation, options) + } + + case ElemGroup: + generateAlt(rand, g, out, elem.Alternation, options) + + case ElemNumVal: + switch elem.Status { + case StatRange: + min, max := numvalToRune(elem.Elems[0], elem.Base), numvalToRune(elem.Elems[1], elem.Base) + // Generation emits UTF-8 text, so clamp to the representable + // (Unicode) subset; a range wholly above U+10FFFF (or with + // max < min) yields no character. + if max > utf8.MaxRune { + max = utf8.MaxRune + } + if min <= utf8.MaxRune && min <= max { + r := min + (rune(rand.Int63()&0x0F) % (max - min + 1)) // mask 0x0F to keep the 32-bit part (rune size) + appendPtr(out, r) + } + + case StatSeries: + for _, v := range elem.Elems { + r := numvalToRune(v, elem.Base) + // Skip values that are not real characters; they cannot be + // emitted as UTF-8 text. + if utf8.ValidRune(r) { + appendPtr(out, r) + } + } + } + + case ElemCharVal: + for _, val := range elem.Values { + if !elem.Sensitive && (int(rand.Int63())%2) == 0 { + val = runeMax(val) + } + appendPtr(out, val) + } + } + + // Prose-val is not covered, checked before to avoid generation + } + } +} + +func appendPtr(slc *[]byte, v ...rune) { + b := *slc + b = append(b, []byte(string(v))...) + *slc = b +} + +func generateKeepGoing(i, torep, minrep, lenout, threshold int) bool { + // If can do no run and reached threshold, skip + if i == 0 && minrep == 0 && lenout >= threshold { + return false + } + return i < torep +} + +// GenerateOption is an option for the *Grammar.Generate method. +type GenerateOption interface { + apply(*genOpts) +} + +type genOpts struct { + repMax int + threshold int +} + +type repMaxOption int + +// WithRepMax defines the maximum repetition to stop generating at. +func WithRepMax(repMax int) repMaxOption { + return repMaxOption(repMax) +} + +func (opt repMaxOption) apply(opts *genOpts) { + opts.repMax = int(opt) +} + +type thresholdOption int + +// WithThreshold defines the length threshold to stop generating at. +func WithThreshold(threshold int) thresholdOption { + return thresholdOption(threshold) +} + +func (opt thresholdOption) apply(opts *genOpts) { + opts.threshold = int(opt) +} + +// checkCanGenerateSafely returns no error if the rule can be generated +// safely i.e. if the rule can exist without infinite recursion, AND if it +// does not contain a prose-val as it could not generate from it. +// Factually, it checks if all involved rules have no path v such that it +// produces a cycle (v:rule-*->rule) AND that this path is mandatory +// (no option, no repetition with a minimum of zero). +func checkCanGenerateSafely(g *Grammar, rulename string) error { + rule := GetRule(rulename, g.Rulemap) + if rule == nil { + return &ErrRuleNotFound{ + Rulename: rulename, + } + } + knownRules := map[string]struct{}{ + rulename: {}, + } + if err := checkCanGenerateSafelyAlt(g, knownRules, rule.Alternation); err != nil { + if err, ok := err.(*ErrCyclicRule); ok { + return err + } + return &ErrCyclicRule{ + Rulename: rulename, + } + } + return nil +} + +func checkCanGenerateSafelyAlt(g *Grammar, knownRules map[string]struct{}, alt Alternation) error { + errs := make([]error, len(alt.Concatenations)) + for alti, concat := range alt.Concatenations { + errs[alti] = checkCanGenerateSafelyConcat(g, knownRules, concat) + } + allErrors := true + for i := 0; i < len(errs) && allErrors; i++ { + if errs[i] == nil { + allErrors = false + } + } + if allErrors { + return errors.Join(errs...) + } + return nil +} + +func checkCanGenerateSafelyConcat(g *Grammar, knownRules map[string]struct{}, concat Concatenation) error { + for _, rep := range concat.Repetitions { + // If the repetition is not mandatory, we can escape so can + // generate safely. + if rep.Min == 0 { + continue + } + + // Deal with the repetition itself then. + switch elem := rep.Element.(type) { + case ElemRulename: + // Copy rules to only focus on rules that made use come here. + // If shared with others, the dependency graph can lead to the same rule + // from another path without it being a cycle, thus must be handled. + scopeRules := duplicateMap(knownRules) + for known := range scopeRules { + if strings.EqualFold(elem.Name, known) { + return &ErrCyclicRule{ + Rulename: elem.Name, + } + } + } + rule := GetRule(elem.Name, g.Rulemap) + scopeRules[elem.Name] = struct{}{} + if err := checkCanGenerateSafelyAlt(g, scopeRules, rule.Alternation); err != nil { + if err, ok := err.(*ErrCyclicRule); ok { + return err + } + return &ErrCyclicRule{ + Rulename: elem.Name, + } + } + + case ElemGroup: + if err := checkCanGenerateSafelyAlt(g, knownRules, elem.Alternation); err != nil { + return err + } + + case ElemProseVal: + return ErrHandlingProseVal + + // Other types are not considered for the following reasons: + // - option: equivalent to rep.min==0, escapable path even if could be cyclic + // - num-val, char-val: termination paths, can't be cyclic + } + } + return nil +} + +func duplicateMap[T comparable, V any](m map[T]V) map[T]V { + n := make(map[T]V, len(m)) + maps.Copy(n, m) + return n +} diff --git a/vendor/github.com/pandatix/go-abnf/go.work b/vendor/github.com/pandatix/go-abnf/go.work new file mode 100644 index 00000000000..bf64daee7e9 --- /dev/null +++ b/vendor/github.com/pandatix/go-abnf/go.work @@ -0,0 +1,6 @@ +go 1.26.0 + +use ( + . + cmd/pap +) diff --git a/vendor/github.com/pandatix/go-abnf/go.work.sum b/vendor/github.com/pandatix/go-abnf/go.work.sum new file mode 100644 index 00000000000..4214fafd665 --- /dev/null +++ b/vendor/github.com/pandatix/go-abnf/go.work.sum @@ -0,0 +1,11 @@ +github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= +golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/vendor/github.com/pandatix/go-abnf/grammar.go b/vendor/github.com/pandatix/go-abnf/grammar.go new file mode 100644 index 00000000000..8b3932af4e4 --- /dev/null +++ b/vendor/github.com/pandatix/go-abnf/grammar.go @@ -0,0 +1,443 @@ +package goabnf + +import ( + "fmt" + "slices" + "strconv" + "strings" +) + +// Grammar represents an ABNF grammar as defined by RFC 5234. +// It is constituted of a set of rules with a unique name. +type Grammar struct { + Rulemap map[string]*Rule +} + +// IsValid checks there exist at least a path that completly consumes +// input, hence is valid given this grammar and especially one of its +// rule. +func (g *Grammar) IsValid(rulename string, input []byte) (bool, error) { + rule := GetRule(rulename, g.Rulemap) + if rule == nil { + return false, &ErrRuleNotFound{Rulename: rulename} + } + // Validity only needs to know whether some derivation consumes the whole + // input, never the (possibly exponentially many) derivations themselves. + // We therefore propagate the set of reachable end-positions per + // (element, index) instead of enumerating paths, which keeps this + // polynomial. Left-recursive rules are resolved by seed-growing rather than + // refused; see recognize.go and leftrec.go. + r := &recognizer{ + g: g, + input: input, + memo: map[string]map[int]bool{}, + inProgress: map[string]bool{}, + leftRec: g.leftRecursiveSCCs(), + growing: map[string]map[int]bool{}, + } + ends := r.reachElem(ElemRulename{Name: rulename}, 0) + return ends[len(input)], nil +} + +// String returns the representation of the grammar that is valid +// according to the ABNF specifications/RFCs. +// This notably imply the use of CRLF instead of LF, and does not +// preserve the initial order nor pretty print it. +func (g *Grammar) String() string { + str := "" + for _, rule := range g.Rulemap { + str += rule.String() + "\r\n" + } + return str +} + +// PrettyPrint returns a prettified string that represents the grammar. +func (g *Grammar) PrettyPrint() string { + // Determine maximum rulename length + rulenameLength := 0 + for rulename := range g.Rulemap { + if len(rulename) > rulenameLength { + rulenameLength = len(rulename) + } + } + + // Construct output + out := "" + for rulename, rl := range g.Rulemap { + spaces := "" + for i := 0; i < rulenameLength-len(rulename); i++ { + spaces += " " + } + + out += fmt.Sprintf("%s%s = %s\r\n", rulename, spaces, rl.Alternation) + } + return out +} + +// ParseABNF is a helper facilitating the call to Parse using the +// pre-computed ABNF grammar and evaluated the resulting to produce +// a ready-to-use grammar. +func ParseABNF(input []byte, opts ...ABNFOption) (*Grammar, error) { + o := process(opts...) + + // Parse the ABNF source using the ABNF meta-grammar with the GLL engine. + f, err := Parse(input, ABNF, "rulelist") + if err != nil { + return nil, err + } + if !f.Valid() { + return nil, f.ParseError() + } + if f.Ambiguous() { + return nil, &ErrMultipleSolutionsFound{} + } + + g, err := evalForest(input, f, o) + if err != nil { + return nil, err + } + + if o.validate { + if err := SemvalABNF(g); err != nil { + return nil, err + } + } + return g, nil +} + +// Parse parses input against grammar starting at rootRulename using the GLL +// engine, returning the shared packed parse forest. Unlike the previous +// backtracking parser it accepts ANY grammar -- left, right and mutual +// recursion and ambiguity included -- in worst-case cubic time, so it never +// stack-overflows on left recursion nor blows up exponentially. Inspect the +// result with the Forest methods (Valid, Ambiguous, NumTrees, Tree). +func Parse(input []byte, grammar *Grammar, rootRulename string) (*Forest, error) { + return ParseForest(input, grammar, rootRulename) +} + +// EvaluateABNF evaluates a parse forest -- produced by parsing an ABNF source +// against the ABNF meta-grammar -- into a ready-to-use *Grammar. +func EvaluateABNF(input []byte, f *Forest, opts ...ABNFOption) (*Grammar, error) { + return evalForest(input, f, process(opts...)) +} + +func evalForest(input []byte, f *Forest, o *abnfOptions) (*Grammar, error) { + tree := f.Tree() + if tree == nil { + return nil, ErrNoSolutionFound + } + ev := &feval{input: input, o: o} + return ev.rulelist(tree) +} + +// feval evaluates the rule-keyed ParseTree of an ABNF source into a *Grammar. +// It dispatches on rule names and reads byte spans, so it is decoupled from the +// exact (scaffolding-collapsed) shape of the tree. +type feval struct { + input []byte + o *abnfOptions +} + +func ptChildren(t *ParseTree, name string) []*ParseTree { + var out []*ParseTree + for _, c := range t.Children { + if c.Rule == name { + out = append(out, c) + } + } + return out +} + +func ptFirst(t *ParseTree, names ...string) *ParseTree { + for _, c := range t.Children { + if slices.Contains(names, c.Rule) { + return c + } + } + return nil +} + +func (e *feval) span(t *ParseTree) string { return string(e.input[t.Start:t.End]) } + +func (e *feval) rulelist(t *ParseTree) (*Grammar, error) { + mp := map[string]*Rule{} + for _, rc := range ptChildren(t, "rule") { + rl, definedAs, err := e.rule(rc) + if err != nil { + return nil, err + } + switch definedAs { + case "=": + isCoreRule := GetRule(rl.Name, nil) != nil + if isCoreRule { + if !e.o.redefineCore { + return nil, &ErrCoreRuleModify{CoreRulename: rl.Name} + } + } else if GetRule(rl.Name, mp) != nil { + return nil, &ErrDuplicatedRule{Rulename: rl.Name} + } + mp[rl.Name] = rl + case "=/": + isCoreRule := GetRule(rl.Name, nil) != nil + if !e.o.redefineCore && isCoreRule { + return nil, &ErrCoreRuleModify{CoreRulename: rl.Name} + } + rule := GetRule(rl.Name, mp) + if rule == nil { + return nil, &ErrRuleNotFound{Rulename: rl.Name} + } + rule.Alternation.Concatenations = append(rule.Alternation.Concatenations, rl.Alternation.Concatenations...) + mp[rule.Name] = rule + } + } + return &Grammar{Rulemap: mp}, nil +} + +func (e *feval) rule(t *ParseTree) (*Rule, string, error) { + nameNode := ptFirst(t, "rulename") + defNode := ptFirst(t, "defined-as") + elemsNode := ptFirst(t, "elements") + if nameNode == nil || defNode == nil || elemsNode == nil { + return nil, "", ErrNoSolutionFound + } + definedAs := "=" + if strings.Contains(e.span(defNode), "=/") { + definedAs = "=/" + } + altNode := ptFirst(elemsNode, "alternation") + if altNode == nil { + return nil, "", ErrNoSolutionFound + } + alt, err := e.alternation(altNode) + if err != nil { + return nil, "", err + } + return &Rule{Name: e.span(nameNode), Alternation: alt}, definedAs, nil +} + +func (e *feval) alternation(t *ParseTree) (Alternation, error) { + var cs []Concatenation + for _, cc := range ptChildren(t, "concatenation") { + c, err := e.concatenation(cc) + if err != nil { + return Alternation{}, err + } + cs = append(cs, c) + } + return Alternation{Concatenations: cs}, nil +} + +func (e *feval) concatenation(t *ParseTree) (Concatenation, error) { + var rs []Repetition + for _, rc := range ptChildren(t, "repetition") { + r, err := e.repetition(rc) + if err != nil { + return Concatenation{}, err + } + rs = append(rs, r) + } + return Concatenation{Repetitions: rs}, nil +} + +func (e *feval) repetition(t *ParseTree) (Repetition, error) { + min, max := 1, 1 + if rep := ptFirst(t, "repeat"); rep != nil { + min, max = e.parseRepeat(rep) + } + elNode := ptFirst(t, "element") + if elNode == nil { + return Repetition{}, ErrNoSolutionFound + } + elem, err := e.element(elNode) + if err != nil { + return Repetition{}, err + } + return Repetition{Min: min, Max: max, Element: elem}, nil +} + +func (e *feval) parseRepeat(t *ParseTree) (int, int) { + s := e.span(t) + before, after, ok := strings.Cut(s, "*") + if !ok { + d, _ := strconv.Atoi(s) + return d, d + } + min, max := 0, inf + if pre := before; pre != "" { + min, _ = strconv.Atoi(pre) + } + if post := after; post != "" { + max, _ = strconv.Atoi(post) + } + return min, max +} + +func (e *feval) element(t *ParseTree) (ElemItf, error) { + c := ptFirst(t, "rulename", "group", "option", "char-val", "num-val", "prose-val") + if c == nil { + return nil, ErrNoSolutionFound + } + switch c.Rule { + case "rulename": + return ElemRulename{Name: e.span(c)}, nil + case "group": + alt, err := e.alternation(ptFirst(c, "alternation")) + if err != nil { + return nil, err + } + return ElemGroup{Alternation: alt}, nil + case "option": + alt, err := e.alternation(ptFirst(c, "alternation")) + if err != nil { + return nil, err + } + return ElemOption{Alternation: alt}, nil + case "char-val": + return e.charVal(c), nil + case "num-val": + return e.numVal(c), nil + case "prose-val": + return e.proseVal(c), nil + } + return nil, ErrNoSolutionFound +} + +func (e *feval) charVal(t *ParseTree) ElemCharVal { + s := e.span(t) + sensitive := false + if len(s) >= 2 && s[0] == '%' { + if s[1] == 's' || s[1] == 'S' { + sensitive = true + } + s = s[2:] + } + first := strings.IndexByte(s, '"') + last := strings.LastIndexByte(s, '"') + value := []rune{} + if first >= 0 && last > first { + value = []rune(s[first+1 : last]) + } + return ElemCharVal{Sensitive: sensitive, Values: value} +} + +func (e *feval) numVal(t *ParseTree) ElemNumVal { + s := e.span(t) + base := "d" + rest := "" + if len(s) >= 2 && s[0] == '%' { + switch s[1] { + case 'b', 'B': + base = "b" + case 'd', 'D': + base = "d" + case 'x', 'X': + base = "x" + } + rest = s[2:] + } + status := StatSeries + var elems []string + if strings.ContainsRune(rest, '-') { + status = StatRange + elems = strings.Split(rest, "-") + } else { + elems = strings.Split(rest, ".") + } + // A num-val outside the Unicode range is still a well-formed numeric + // encoding (legitimate for non-textual / on-wire grammars), so it is kept + // as-is here. Strict semantic validation (SemvalABNF) rejects it for textual + // use; consumers that match against decoded runes treat an unrepresentable + // value as non-matching rather than panicking. + return ElemNumVal{Base: base, Status: status, Elems: elems} +} + +func (e *feval) proseVal(t *ParseTree) ElemProseVal { + values := []string{} + for i := t.Start + 1; i < t.End-1; i++ { + values = append(values, string(e.input[i])) + } + return ElemProseVal{values: values} +} + +func sensequal(target, actual rune, sensitive bool) bool { + if !sensitive { + target, actual = runeMin(target), runeMin(actual) + } + return target == actual +} + +func runeMin(r rune) rune { + if r >= 'A' && r <= 'Z' { + return r - 'A' + 'a' + } + return r +} + +func runeMax(r rune) rune { + if r >= 'a' && r <= 'z' { + return r - 'a' + 'A' + } + return r +} + +// SemvalABNF proceed to semantic validations of an ABNF grammar. +// It currently support the following checks: +// - for all rules, its dependencies (rules) exist +// - for repetition, min <= max +// - for num-val, that the value fits in 7-bits (US-ASCII encoded) +// To update this list, please open an issue. +func SemvalABNF(g *Grammar) error { + // Check all dependencies exist + for _, rule := range g.Rulemap { + deps := getDependencies(rule.Alternation) + for _, dep := range deps { + r := GetRule(dep, g.Rulemap) + if r == nil { + return &ErrDependencyNotFound{ + Rulename: dep, + } + } + } + } + + for _, rule := range g.Rulemap { + if err := semvalAlternation(rule.Alternation); err != nil { + return err + } + } + return nil +} + +func semvalAlternation(alt Alternation) error { + for _, concat := range alt.Concatenations { + for _, rep := range concat.Repetitions { + // min <= max + if rep.Max != inf && rep.Min > rep.Max { + return &ErrSemanticRepetition{ + Repetition: rep, + } + } + switch elem := rep.Element.(type) { + // num-val base + case ElemNumVal: + for _, val := range elem.Elems { + if err := checkBounds(val, elem.Base); err != nil { + return err + } + } + + // propagate recursion + case ElemGroup: + if err := semvalAlternation(elem.Alternation); err != nil { + return err + } + + case ElemOption: + if err := semvalAlternation(elem.Alternation); err != nil { + return err + } + } + } + } + return nil +} diff --git a/vendor/github.com/pandatix/go-abnf/leftrec.go b/vendor/github.com/pandatix/go-abnf/leftrec.go new file mode 100644 index 00000000000..f86806d2a64 --- /dev/null +++ b/vendor/github.com/pandatix/go-abnf/leftrec.go @@ -0,0 +1,159 @@ +package goabnf + +import ( + "slices" + "strings" +) + +// Identifies left-recursive rules so the recognizer can handle them by seed-growing +// instead of refusing them. A rule is left-recursive when it can re-derive itself +// without consuming input -- i.e. it appears on a cycle of the "left-corner" +// relation: A left-corners B if B can begin a derivation of A at the same input +// position. Crucially this skips over prefixes that can vanish (zero-min repetitions, +// options, and nullable elements), so it also catches *hidden* left recursion such as +// `a = b a / "x"` where `b` is nullable -- which a purely positional check misses. + +// nullableRules computes, by least fixpoint over user and core rules, which rules can +// derive the empty string. +func (g *Grammar) nullableRules() map[string]bool { + rules := map[string]*Rule{} + for n := range g.Rulemap { + if r := GetRule(n, g.Rulemap); r != nil { + rules[strings.ToLower(r.Name)] = r + } + } + for n := range coreRules { + if r := GetRule(n, g.Rulemap); r != nil { + rules[strings.ToLower(r.Name)] = r + } + } + null := map[string]bool{} + for changed := true; changed; { + changed = false + for name, r := range rules { + if !null[name] && altNullable(g, null, r.Alternation) { + null[name] = true + changed = true + } + } + } + return null +} + +func altNullable(g *Grammar, null map[string]bool, alt Alternation) bool { + for _, c := range alt.Concatenations { + nullable := true + for _, rep := range c.Repetitions { + if !repNullable(g, null, rep) { + nullable = false + break + } + } + if nullable { + return true + } + } + return false +} + +func repNullable(g *Grammar, null map[string]bool, rep Repetition) bool { + if rep.Min == 0 { + return true + } + return elemNullable(g, null, rep.Element) +} + +func elemNullable(g *Grammar, null map[string]bool, elem ElemItf) bool { + switch v := elem.(type) { + case ElemOption: + return true + case ElemGroup: + return altNullable(g, null, v.Alternation) + case ElemRulename: + return null[strings.ToLower(v.Name)] + case ElemCharVal: + return len(v.Values) == 0 + } + // num-val always consumes; prose-val is treated as unmatchable. + return false +} + +// leftRecursiveSCCs returns, for every left-recursive rule (keyed by lowercase +// name), the set of rule names in its left-corner strongly-connected component +// (mutually left-recursive rules grown together). Non-left-recursive rules are +// absent from the map. +func (g *Grammar) leftRecursiveSCCs() map[string][]string { + null := g.nullableRules() + + // Build the left-corner dependency graph and reuse Tarjan's SCC finder. + lc := Depgraph{} + add := func(r *Rule) { + name := strings.ToLower(r.Name) + if _, ok := lc[name]; ok { + return + } + lc[name] = &node{Rulename: r.Name, Dependencies: leftCornerDeps(g, null, r.Alternation)} + } + for n := range g.Rulemap { + if r := GetRule(n, g.Rulemap); r != nil { + add(r) + } + } + for n := range coreRules { + if r := GetRule(n, g.Rulemap); r != nil { + add(r) + } + } + + finder := &cycle{index: 0, stack: []*node{}, dg: lc} + finder.find() + + out := map[string][]string{} + for _, scc := range finder.sccs { + members := make([]string, len(scc)) + for i, nd := range scc { + members[i] = strings.ToLower(nd.Rulename) + } + leftRecursive := len(scc) > 1 + if !leftRecursive { // size 1: left-recursive only with a self left-corner + leftRecursive = slices.Contains(scc[0].Dependencies, members[0]) + } + if leftRecursive { + for _, m := range members { + out[m] = members + } + } + } + return out +} + +// leftCornerDeps collects the rules reachable as a left corner of alt: walk each +// concatenation left to right, collecting rulename references, and stop at the +// first repetition that cannot vanish. +func leftCornerDeps(g *Grammar, null map[string]bool, alt Alternation) []string { + deps := []string{} + var addAlt func(Alternation) + addElem := func(e ElemItf) { + switch v := e.(type) { + case ElemRulename: + deps = appendDeps(deps, strings.ToLower(v.Name)) + case ElemGroup: + addAlt(v.Alternation) + case ElemOption: + addAlt(v.Alternation) + } + } + addAlt = func(a Alternation) { + for _, c := range a.Concatenations { + for _, rep := range c.Repetitions { + addElem(rep.Element) + if rep.Min == 0 || elemNullable(g, null, rep.Element) { + continue // this position can be empty; the next is also a left corner + } + break + } + } + } + addAlt(alt) + return deps +} diff --git a/vendor/github.com/pandatix/go-abnf/lookahead.go b/vendor/github.com/pandatix/go-abnf/lookahead.go new file mode 100644 index 00000000000..a0229a6d480 --- /dev/null +++ b/vendor/github.com/pandatix/go-abnf/lookahead.go @@ -0,0 +1,298 @@ +package goabnf + +import ( + "fmt" + "sort" + "unicode/utf8" +) + +// One-symbol lookahead for the GLL engines. +// +// GLL as published guards parsing steps with a selection test test(x, A, γ): +// a sequence γ in a production of A is only explored at input position i when +// the next input symbol x is in FIRST(γ), or γ is nullable and x is in +// FOLLOW(A) (Scott & Johnstone, "GLL parse-tree generation", Science of +// Computer Programming 78(10), 2013, §3.3). It guards the descriptor started +// for each alternate α of A (§4.3), and the call of each nonterminal instance +// A ::= α·Xβ with γ = Xβ (§4.1), so no GSS node is created for a call that +// cannot consume the next symbol. +// +// The guard prunes sub-parses whose continuation cannot consume the next input +// symbol, such as a repetition 1*ALPHA stopping inside a run of letters. This +// keeps the forest proportional to the derivations that can still complete, +// e.g. linear in the run length for 1*ALPHA followed by a delimiter. +// +// A sequence rejected by the guard would have failed on a terminal at the same +// position, so its FIRST (and, when nullable, FOLLOW) terminals feed the +// furthest-failure diagnostics in its place. +// +// Symbols are runes (ABNF terminals are matched on decoded UTF-8), so sets are +// rune intervals plus an end-of-input marker. FIRST/FOLLOW are computed on the +// context-free reading of the slot grammar, ignoring repetition bounds: the +// bounded language is a subset of it, so the sets over-approximate and the test +// never rejects a viable sequence. + +type runeRange struct{ lo, hi rune } + +// laSet is a lookahead set: rune intervals, the end-of-input marker, and the +// terminals contributing to it (for furthest-failure diagnostics). +type laSet struct { + ranges []runeRange // sorted, non-overlapping, non-adjacent + ascii [2]uint64 // bitmap of ranges below utf8.RuneSelf + eof bool + terms []ssym + descs map[string]bool +} + +func (s *laSet) addRange(lo, hi rune) bool { + if lo > hi { + return false + } + if s.coversRange(lo, hi) { + return false + } + s.ranges = append(s.ranges, runeRange{lo, hi}) + sort.Slice(s.ranges, func(i, j int) bool { return s.ranges[i].lo < s.ranges[j].lo }) + merged := s.ranges[:1] + for _, r := range s.ranges[1:] { + last := &merged[len(merged)-1] + if r.lo <= last.hi+1 { + if r.hi > last.hi { + last.hi = r.hi + } + continue + } + merged = append(merged, r) + } + s.ranges = merged + return true +} + +func (s *laSet) coversRange(lo, hi rune) bool { + for _, r := range s.ranges { + if r.lo <= lo && hi <= r.hi { + return true + } + } + return false +} + +func (s *laSet) addTerm(t ssym) bool { + desc := "?" + if str, ok := t.term.(fmt.Stringer); ok { + desc = str.String() + } + if s.descs == nil { + s.descs = map[string]bool{} + } + if s.descs[desc] { + return false + } + s.descs[desc] = true + s.terms = append(s.terms, t) + return true +} + +// union adds o to s (optionally including o's end-of-input marker) and +// reports whether s changed. +func (s *laSet) union(o *laSet, withEOF bool) bool { + changed := false + for _, r := range o.ranges { + if s.addRange(r.lo, r.hi) { + changed = true + } + } + if withEOF && o.eof && !s.eof { + s.eof = true + changed = true + } + for _, t := range o.terms { + if s.addTerm(t) { + changed = true + } + } + return changed +} + +func (s *laSet) seal() { + for _, r := range s.ranges { + for c := r.lo; c <= r.hi && c < utf8.RuneSelf; c++ { + if c >= 0 { + s.ascii[c/64] |= 1 << (c % 64) + } + } + } +} + +func (s *laSet) contains(c rune) bool { + if c >= 0 && c < utf8.RuneSelf { + return s.ascii[c/64]&(1<<(c%64)) != 0 + } + i := sort.Search(len(s.ranges), func(i int) bool { return s.ranges[i].hi >= c }) + return i < len(s.ranges) && s.ranges[i].lo <= c +} + +// termFirst returns the FIRST set of a terminal and whether it matches the +// empty string, mirroring matchTerm. +func termFirst(t ssym) (laSet, bool) { + var s laSet + if t.kind == symEps { + return s, true + } + switch v := t.term.(type) { + case ElemCharVal: + if len(v.Values) == 0 { + return s, true + } + r := v.Values[0] + if v.Sensitive { + s.addRange(r, r) + } else { + lo := runeMin(r) + s.addRange(lo, lo) + up := runeMax(lo) + s.addRange(up, up) + } + + case ElemNumVal: + switch v.Status { + case StatRange: + s.addRange(numvalToRune(v.Elems[0], v.Base), numvalToRune(v.Elems[1], v.Base)) + + case StatSeries: + if len(v.Elems) == 0 { + return s, true + } + if r := numvalToRune(v.Elems[0], v.Base); utf8.ValidRune(r) { + s.addRange(r, r) + } + } + } + // A terminal that never matches (prose-val, out-of-range series) has an + // empty rune set but is still an attempted terminal for diagnostics. + s.addTerm(t) + return s, false +} + +// lookahead holds the selection sets FIRST(γ) ∪ (FOLLOW(A) if γ is nullable) +// of the slot grammar: sel for every alternate γ = α of A, call for every +// nonterminal instance A ::= α·Xβ with γ = Xβ. +type lookahead struct { + sel [][]laSet // [nt][alt] + call [][][]laSet // [nt][alt][dot], set only where the symbol at dot is a nonterminal +} + +func computeLookahead(sg *slotGrammar) *lookahead { + n := len(sg.nts) + nullable := make([]bool, n) + first := make([]laSet, n) + follow := make([]laSet, n) + + symNullable := func(s ssym) bool { + switch s.kind { + case symNonterm: + return nullable[s.nt] + + default: + _, null := termFirst(s) + return null + } + } + // firstSeq returns FIRST(seq) and whether seq is nullable. + firstSeq := func(seq []ssym) (laSet, bool) { + var out laSet + for _, s := range seq { + if s.kind == symNonterm { + out.union(&first[s.nt], false) + } else { + f, _ := termFirst(s) + out.union(&f, false) + } + if !symNullable(s) { + return out, false + } + } + return out, true + } + + for changed := true; changed; { + changed = false + for a, nt := range sg.nts { + for _, alt := range nt.alts { + f, null := firstSeq(alt) + if first[a].union(&f, false) { + changed = true + } + if null && !nullable[a] { + nullable[a] = true + changed = true + } + } + } + } + + follow[sg.start].eof = true + for changed := true; changed; { + changed = false + for a, nt := range sg.nts { + for _, alt := range nt.alts { + for k, s := range alt { + if s.kind != symNonterm { + continue + } + f, null := firstSeq(alt[k+1:]) + if follow[s.nt].union(&f, false) { + changed = true + } + if null && follow[s.nt].union(&follow[a], true) { + changed = true + } + } + } + } + } + + selection := func(a int, seq []ssym) laSet { + f, null := firstSeq(seq) + if null { + f.union(&follow[a], true) + } + f.seal() + return f + } + la := &lookahead{sel: make([][]laSet, n), call: make([][][]laSet, n)} + for a, nt := range sg.nts { + la.sel[a] = make([]laSet, len(nt.alts)) + la.call[a] = make([][]laSet, len(nt.alts)) + for ai, alt := range nt.alts { + la.sel[a][ai] = selection(a, alt) + la.call[a][ai] = make([]laSet, len(alt)) + for k, sym := range alt { + if sym.kind == symNonterm { + la.call[a][ai][k] = selection(a, alt[k:]) + } + } + } + } + return la +} + +// alt returns the selection set guarding alternate alt of nonterminal nt. +func (la *lookahead) alt(nt, alt int) *laSet { return &la.sel[nt][alt] } + +// callAt returns the selection set guarding the nonterminal call at slot L. +func (la *lookahead) callAt(L slot) *laSet { return &la.call[L.nt][L.alt][L.dot] } + +// admits reports whether the next input symbol at i is in s, i.e. whether the +// guarded sequence may derive a prefix of input[i:] in some context. +func (s *laSet) admits(input []byte, i int) bool { + if i >= len(input) { + return s.eof + } + r, size := utf8.DecodeRune(input[i:]) + if r == utf8.RuneError && size == 1 { + // No terminal matches an invalid UTF-8 sequence (see matchTerm). + return false + } + return s.contains(r) +} diff --git a/vendor/github.com/pandatix/go-abnf/option.go b/vendor/github.com/pandatix/go-abnf/option.go new file mode 100644 index 00000000000..95664c2c2a6 --- /dev/null +++ b/vendor/github.com/pandatix/go-abnf/option.go @@ -0,0 +1,64 @@ +package goabnf + +const ( + defaultValidate = true + defaultRedefine = false +) + +func process(opts ...ABNFOption) *abnfOptions { + o := &abnfOptions{ + validate: defaultValidate, + redefineCore: defaultRedefine, + } + for _, opt := range opts { + opt.apply(o) + } + return o +} + +// ABNFOption defines the interface for parsing ABNF functional options. +type ABNFOption interface { + apply(*abnfOptions) +} + +type abnfOptions struct { + validate bool + redefineCore bool +} + +// Defines if proceed to semantic validation. +type validateOption bool + +var _ ABNFOption = (*validateOption)(nil) + +func (o validateOption) apply(opts *abnfOptions) { + opts.validate = bool(o) +} + +// WithValidation returns a functional option to proceed to +// validation or not. +// Default is true. +func WithValidation(validate bool) ABNFOption { + return validateOption(validate) +} + +// Defines if an ABNF grammar could redefine a core rule. +type redefineCoreOption bool + +var _ ABNFOption = (*redefineCoreOption)(nil) + +func (o redefineCoreOption) apply(opts *abnfOptions) { + opts.redefineCore = bool(o) +} + +// WithRedefineCoreRules returns a functional option to +// enable redefining a core rule. +// Default is false. +// +// WARNING: use with caution, as we left to the user the responsibility +// to ensure the redefinition keeps the ABNF grammar coherent (i.e. +// there is an isomorphism between the core rule and the redefinition, +// not especially the same textual representation). +func WithRedefineCoreRules(redefine bool) ABNFOption { + return redefineCoreOption(redefine) +} diff --git a/vendor/github.com/pandatix/go-abnf/recognize.go b/vendor/github.com/pandatix/go-abnf/recognize.go new file mode 100644 index 00000000000..df443cf3f30 --- /dev/null +++ b/vendor/github.com/pandatix/go-abnf/recognize.go @@ -0,0 +1,320 @@ +package goabnf + +import ( + "strconv" + "strings" + "unicode/utf8" +) + +// recognize.go holds the engine behind (*Grammar).IsValid. +// +// Validity only needs the SET of input positions each element can reach, never +// the (possibly exponential) set of distinct parse trees that reach them. We +// compute, for each (element, start-index), the set of reachable end-indices, +// deduplicating positions at every step and memoizing per (element, index). +// Positions live in [0, len(input)], so the work is polynomial rather than the +// exponential path enumeration that Parse performs. +// +// IsValid enforces left-termination before calling in, so the in-progress +// guard below is defense-in-depth: it can never fire for a left-terminating +// grammar (a same-index re-entry implies the rule is not left terminating). + +type recognizer struct { + g *Grammar + input []byte + memo map[string]map[int]bool // element.String()+"@"+index -> reachable end set + inProgress map[string]bool + + // Left-recursion support. leftRec maps each left-recursive rule (lowercase + // name) to its left-corner SCC, grown jointly. growing holds the live seed + // of each rule currently being grown (key "rule@index"); a re-entrant call + // returns the seed instead of recursing. + leftRec map[string][]string + growing map[string]map[int]bool +} + +func cloneSet(s map[int]bool) map[int]bool { + out := make(map[int]bool, len(s)) + for k := range s { + out[k] = true + } + return out +} + +func (r *recognizer) reachAlt(alt Alternation, index int) map[int]bool { + out := map[int]bool{} + for _, c := range alt.Concatenations { + for e := range r.reachConcat(c, index) { + out[e] = true + } + } + return out +} + +func (r *recognizer) reachConcat(c Concatenation, index int) map[int]bool { + cur := map[int]bool{index: true} + for _, rep := range c.Repetitions { + next := map[int]bool{} + for p := range cur { + for e := range r.reachRep(rep, p) { + next[e] = true + } + } + cur = next + if len(cur) == 0 { + break + } + } + return cur +} + +func (r *recognizer) reachRep(rep Repetition, index int) map[int]bool { + ends := map[int]bool{} + if rep.Min == 0 { + ends[index] = true + } + + // level = positions reachable after exactly c occurrences. + level := map[int]bool{index: true} + // seen = every position reached at any count so far. reachElem only moves + // forward (an end position is >= its start), so once a step introduces no + // position outside seen, the reachable set has closed and no later + // repetition can add a new end position. Breaking there keeps an unbounded + // repetition over an element with large reach-sets (e.g. *(1*ALPHA)) at + // O(n^2); the previous len(next)==len(level) test never tripped because + // level shrank by one each step ({1..n}, {2..n}, ...), giving O(n^3). + seen := map[int]bool{index: true} + + // Levels are stationary after at most len(input)+1 counts: reachElem only + // moves forward, so whether position x is in a level depends on positions + // below x plus x reaching itself, and each position settles one count after + // the ones below it. The stationary check below therefore fires within cap. + cap := len(r.input) + 2 + for c := 1; c <= cap; c++ { + if rep.Max != inf && c > rep.Max { + break + } + next := map[int]bool{} + for p := range level { + for e := range r.reachElem(rep.Element, p) { + next[e] = true + } + } + if len(next) == 0 { + break + } + if c >= rep.Min { + for e := range next { + ends[e] = true + } + } + grew := false + for e := range next { + if !seen[e] { + seen[e] = true + grew = true + } + } + // Stationary level: every further count reaches the same positions, so + // a minimum not yet met is met by that same set. This happens with a + // nullable element, e.g. 3"" on an empty input, where the count must + // go past the input length to reach the minimum. + if c < rep.Min && sameSet(next, level) { + if rep.Max == inf || rep.Min <= rep.Max { + for e := range next { + ends[e] = true + } + } + break + } + level = next + // Reachable set closed and the minimum-count requirement met. + if !grew && c >= rep.Min { + break + } + } + return ends +} + +func (r *recognizer) reachElem(elem ElemItf, index int) map[int]bool { + // Left-recursive rulenames are resolved by seed-growing rather than the + // flat memoized recursion below (which would cut the recursion to empty and + // under-accept). + if rn, ok := elem.(ElemRulename); ok { + if scc, isLR := r.leftRec[strings.ToLower(rn.Name)]; isLR { + return r.reachLeftRec(strings.ToLower(rn.Name), scc, index) + } + } + + key := elem.String() + "@" + strconv.Itoa(index) + if cached, ok := r.memo[key]; ok { + return cached + } + if r.inProgress[key] { + // Re-entry at the same position == non-progressing recursion in a rule + // not flagged left-recursive. Return empty (defense-in-depth) instead of + // looping forever. + return map[int]bool{} + } + r.inProgress[key] = true + out := r.computeElem(elem, index) + delete(r.inProgress, key) + // Safe to cache: a rule reached here either is independent of any actively + // growing seed, or is itself a growing SCC member (handled by reachLeftRec + // via r.growing, not this path). A lower left-corner SCC cannot reference + // the SCC growing above it -- they would be one SCC -- so its result is + // stable. Caching nested SCCs is what keeps left recursion polynomial. + r.memo[key] = cloneSet(out) + return out +} + +// reachLeftRec computes the reachable end-set of a left-recursive rule at index +// by growing the whole left-corner SCC from the empty seed to its least +// fixpoint. Each growth step adds end-positions (bounded by len(input)), so it +// terminates; a re-entrant call to any SCC member at the same index returns the +// current seed. Pure (base-less) left recursion correctly settles at the empty +// set, i.e. "no match". +func (r *recognizer) reachLeftRec(name string, scc []string, index int) map[int]bool { + sidx := "@" + strconv.Itoa(index) + key := name + sidx + if cached, ok := r.memo[key]; ok { + return cached + } + if seed, ok := r.growing[key]; ok { + return seed // re-entrant: return the current seed + } + + keys := make([]string, len(scc)) + for i, x := range scc { + keys[i] = x + sidx + r.growing[keys[i]] = map[int]bool{} + } + for changed := true; changed; { + changed = false + for i, x := range scc { + rule := GetRule(x, r.g.Rulemap) + if rule == nil { + continue + } + ns := r.reachAlt(rule.Alternation, index) + cur := r.growing[keys[i]] + for e := range ns { + if !cur[e] { + cur[e] = true + changed = true + } + } + } + } + + // The fixpoint converged. These end-sets are final and independent of any + // outer growing seed (the SCC only re-references its own members, held in + // r.growing, and strictly lower SCCs, which cannot reference it back), so + // they can be memoized unconditionally. + var result map[int]bool + for i, x := range scc { + v := r.growing[keys[i]] + delete(r.growing, keys[i]) + r.memo[keys[i]] = cloneSet(v) + if x == name { + result = cloneSet(v) + } + } + return result +} + +func (r *recognizer) computeElem(elem ElemItf, index int) map[int]bool { + out := map[int]bool{} + switch v := elem.(type) { + case ElemRulename: + rule := GetRule(v.Name, r.g.Rulemap) + if rule == nil { + return out + } + return r.reachAlt(rule.Alternation, index) + + case ElemGroup: + return r.reachAlt(v.Alternation, index) + + case ElemOption: + out[index] = true // 0 occurrences + for e := range r.reachAlt(v.Alternation, index) { + out[e] = true + } + return out + + case ElemProseVal: + // prose-val can't be matched (same as the original parser) + return out + + case ElemNumVal: + switch v.Status { + case StatRange: + if index >= len(r.input) { + return out + } + // Bounds are numeric: a range straddling the Unicode ceiling still + // matches its representable (rune) subset, one wholly above it matches + // nothing. numvalToRune never panics (saturates out-of-int32 bounds). + min := numvalToRune(v.Elems[0], v.Base) + max := numvalToRune(v.Elems[1], v.Base) + ru, size := utf8.DecodeRune(r.input[index:]) + if ru == utf8.RuneError && size == 1 { + return out + } + if min <= ru && ru <= max { + out[index+size] = true + } + case StatSeries: + idx := index + for i := 0; i < len(v.Elems); i++ { + ru := numvalToRune(v.Elems[i], v.Base) + // A series element must denote a real character; an out-of-range + // value can never appear in UTF-8 input, so the series matches + // nothing (and we avoid a spurious U+FFFD coincidence). + if !utf8.ValidRune(ru) { + return out + } + s := string(ru) + sz := len([]byte(s)) + if idx+sz > len(r.input) || s != string(r.input[idx:idx+sz]) { + return out + } + idx += sz + } + out[idx] = true + } + return out + + case ElemCharVal: + idx := index + for i := 0; i < len(v.Values); i++ { + if idx >= len(r.input) { + return out + } + ru, size := utf8.DecodeRune(r.input[idx:]) + if ru == utf8.RuneError && size == 1 { + return out + } + if !sensequal(v.Values[i], ru, v.Sensitive) { + return out + } + idx += size + } + out[idx] = true + return out + } + return out +} + +func sameSet(a, b map[int]bool) bool { + if len(a) != len(b) { + return false + } + for k := range a { + if !b[k] { + return false + } + } + return true +} diff --git a/vendor/github.com/pandatix/go-abnf/regex.go b/vendor/github.com/pandatix/go-abnf/regex.go new file mode 100644 index 00000000000..b3d49b29761 --- /dev/null +++ b/vendor/github.com/pandatix/go-abnf/regex.go @@ -0,0 +1,468 @@ +package goabnf + +import ( + "fmt" + "regexp" + "sort" + "strings" + "unicode/utf8" +) + +// Regex builds a regular expression that recognises the given rulename. +// +// The rule must not contain a cycle in its dependency graph: a regular +// expression (RE2, no subroutines) cannot express recursion. +// +// Because RE2 has no subroutine/backreference, a rule referenced N times is +// inlined N times, so the output can grow exponentially with reference-nesting +// depth for diamond-shaped grammars. Pass WithMaxRegexLen to fail fast with +// *ErrRegexTooLarge instead of exhausting memory. The output is simplified +// (character classes merged, redundant groups and {1} quantifiers dropped) but +// is not guaranteed minimal. +func (g *Grammar) Regex(rulename string, opts ...RegexOption) (string, error) { + o := ®exOptions{} + for _, op := range opts { + op.applyRegex(o) + } + + isCyclic, err := g.RuleContainsCycle(rulename) + if err != nil { + return "", err + } + if isCyclic { + return "", &ErrCyclicRule{Rulename: rulename} + } + rule := GetRule(rulename, g.Rulemap) + if rule == nil { + return "", &ErrRuleNotFound{Rulename: rulename} + } + + b := &reBuilder{g: g, memo: map[string]reNode{}} + node := b.alt(rule.Alternation) + if b.err != nil { + return "", b.err + } + node = simplify(node, map[reNode]reNode{}) + + r := &reRenderer{max: o.maxLen} + var sb strings.Builder + r.render(&sb, node, precAlt) + if r.err != nil { + return "", r.err + } + return sb.String(), nil +} + +// ---- options & error ---- + +type regexOptions struct{ maxLen int } + +// RegexOption configures Regex. +type RegexOption interface{ applyRegex(*regexOptions) } + +type regexOptionFunc func(*regexOptions) + +func (f regexOptionFunc) applyRegex(o *regexOptions) { f(o) } + +// WithMaxRegexLen bounds the produced regex length in bytes. When the budget is +// exceeded, Regex returns *ErrRegexTooLarge. Zero (the default) is unbounded. +func WithMaxRegexLen(n int) RegexOption { + return regexOptionFunc(func(o *regexOptions) { o.maxLen = n }) +} + +// ErrRegexTooLarge is returned by Regex when the output would exceed the budget +// set with WithMaxRegexLen. +type ErrRegexTooLarge struct{ Max int } + +var _ error = (*ErrRegexTooLarge)(nil) + +func (e *ErrRegexTooLarge) Error() string { + return fmt.Sprintf("regex exceeds the configured %d-byte budget", e.Max) +} + +// ---- intermediate representation ---- + +const ( + precAlt = 1 // alternation binds loosest + precConcat = 2 + precRepeat = 3 + precAtom = 4 // a class or single char binds tightest +) + +type reNode interface{ prec() int } + +type reRange struct{ lo, hi rune } + +type reClass struct{ ranges []reRange } // a single [c,c] renders as a bare char +type reConcat struct{ parts []reNode } +type reAlt struct{ opts []reNode } +type reRepeat struct { + sub reNode + min, max int +} +type reEmpty struct{} + +// reNever matches nothing. It represents a num-val a RE2/Unicode pattern cannot +// express (a value entirely above U+10FFFF), keeping Regex consistent with the +// recognizer, which also matches nothing in that case. +type reNever struct{} + +func (*reClass) prec() int { return precAtom } +func (*reConcat) prec() int { return precConcat } +func (*reAlt) prec() int { return precAlt } +func (*reRepeat) prec() int { return precRepeat } +func (*reEmpty) prec() int { return precAtom } +func (*reNever) prec() int { return precAtom } + +var reEmptyV reNode = &reEmpty{} +var reNeverV reNode = &reNever{} + +func isNever(n reNode) bool { _, ok := n.(*reNever); return ok } + +// ---- build (memoised per rule => linear over the grammar DAG) ---- + +type reBuilder struct { + g *Grammar + memo map[string]reNode + err error +} + +func (b *reBuilder) rule(name string) reNode { + key := strings.ToLower(name) + if n, ok := b.memo[key]; ok { + return n + } + r := GetRule(name, b.g.Rulemap) + if r == nil { + return reEmptyV + } + n := b.alt(r.Alternation) + b.memo[key] = n + return n +} + +func (b *reBuilder) alt(a Alternation) reNode { + opts := make([]reNode, 0, len(a.Concatenations)) + for _, c := range a.Concatenations { + opts = append(opts, b.concat(c)) + } + return &reAlt{opts: opts} +} + +func (b *reBuilder) concat(c Concatenation) reNode { + parts := make([]reNode, 0, len(c.Repetitions)) + for _, rep := range c.Repetitions { + parts = append(parts, &reRepeat{sub: b.elem(rep.Element), min: rep.Min, max: rep.Max}) + } + return &reConcat{parts: parts} +} + +func (b *reBuilder) elem(e ElemItf) reNode { + switch v := e.(type) { + case ElemRulename: + return b.rule(v.Name) + case ElemGroup: + return b.alt(v.Alternation) + case ElemOption: + return &reRepeat{sub: b.alt(v.Alternation), min: 0, max: 1} + case ElemNumVal: + switch v.Status { + case StatRange: + lo := numvalToRune(v.Elems[0], v.Base) + hi := min( + // RE2 patterns are Unicode (max U+10FFFF). A range straddling that + // ceiling is clamped to its representable subset; one wholly above it + // matches nothing -- mirroring the recognizer. + numvalToRune(v.Elems[1], v.Base), utf8.MaxRune) + if lo > utf8.MaxRune || lo > hi { + return reNeverV + } + return &reClass{ranges: []reRange{{lo, hi}}} + default: // StatSeries is a CONCATENATION of values (a fixed sequence). + parts := make([]reNode, 0, len(v.Elems)) + for _, s := range v.Elems { + c := numvalToRune(s, v.Base) + // A series element must be a real character; if it is not, the + // whole sequence can never match any (Unicode) input. + if !utf8.ValidRune(c) { + return reNeverV + } + parts = append(parts, &reClass{ranges: []reRange{{c, c}}}) + } + return &reConcat{parts: parts} + } + case ElemCharVal: + // A char-val is case-insensitive unless Sensitive (RFC 7405); the + // recognizer folds ASCII A-Z only (see runeMin/sensequal), so we match + // that exactly here. + parts := make([]reNode, 0, len(v.Values)) + for _, c := range v.Values { + parts = append(parts, charClass(c, v.Sensitive)) + } + return &reConcat{parts: parts} + case ElemProseVal: + if b.err == nil { + b.err = ErrHandlingProseVal + } + return reEmptyV + } + return reEmptyV +} + +func charClass(c rune, sensitive bool) reNode { + if !sensitive { + switch { + case c >= 'A' && c <= 'Z': + lo := c - 'A' + 'a' + return &reClass{ranges: []reRange{{c, c}, {lo, lo}}} + case c >= 'a' && c <= 'z': + up := c - 'a' + 'A' + return &reClass{ranges: []reRange{{up, up}, {c, c}}} + } + } + return &reClass{ranges: []reRange{{c, c}}} +} + +// ---- simplification (pointer-memoised => linear over the DAG) ---- + +func simplify(n reNode, memo map[reNode]reNode) reNode { + if n == nil { + return reEmptyV + } + if s, ok := memo[n]; ok { + return s + } + var res reNode + switch v := n.(type) { + case *reClass: + res = &reClass{ranges: normalizeRanges(v.ranges)} + case *reEmpty: + res = reEmptyV + case *reNever: + res = reNeverV + case *reConcat: + parts := make([]reNode, 0, len(v.parts)) + never := false + for _, p := range v.parts { + sp := simplify(p, memo) + switch x := sp.(type) { + case *reEmpty: // drop: empty in a concatenation contributes nothing + case *reNever: // one un-matchable part makes the whole sequence un-matchable + never = true + case *reConcat: + parts = append(parts, x.parts...) // flatten + default: + parts = append(parts, sp) + } + } + switch { + case never: + res = reNeverV + case len(parts) == 0: + res = reEmptyV + case len(parts) == 1: + res = parts[0] + default: + res = &reConcat{parts: parts} + } + case *reAlt: + opts := make([]reNode, 0, len(v.opts)) + for _, o := range v.opts { + so := simplify(o, memo) + if isNever(so) { // a never-matching branch is dead; drop it + continue + } + if x, ok := so.(*reAlt); ok { + opts = append(opts, x.opts...) // flatten + } else { + opts = append(opts, so) + } + } + // Fold every class option into a single merged class: turns an + // alternation of single characters / ranges into one [...]. + var ranges []reRange + classes := 0 + others := make([]reNode, 0, len(opts)) + for _, o := range opts { + if c, ok := o.(*reClass); ok { + ranges = append(ranges, c.ranges...) + classes++ + } else { + others = append(others, o) + } + } + merged := make([]reNode, 0, len(others)+1) + if classes > 0 { + merged = append(merged, &reClass{ranges: normalizeRanges(ranges)}) + } + merged = append(merged, others...) + switch len(merged) { + case 0: + // Every branch was un-matchable: the alternation matches nothing. + res = reNeverV + case 1: + res = merged[0] + default: + res = &reAlt{opts: merged} + } + case *reRepeat: + sub := simplify(v.sub, memo) + switch { + case isNever(sub): + // Zero occurrences are allowed iff min == 0 (then it matches empty); + // otherwise the repetition is itself un-matchable. + if v.min == 0 { + res = reEmptyV + } else { + res = reNeverV + } + case isEmpty(sub): + res = reEmptyV + case v.min == 1 && v.max == 1: + res = sub + default: + res = &reRepeat{sub: sub, min: v.min, max: v.max} + } + default: + res = n + } + memo[n] = res + return res +} + +func isEmpty(n reNode) bool { _, ok := n.(*reEmpty); return ok } + +// normalizeRanges sorts and coalesces overlapping/adjacent ranges. +func normalizeRanges(rs []reRange) []reRange { + if len(rs) <= 1 { + return rs + } + cp := append([]reRange(nil), rs...) + sort.Slice(cp, func(i, j int) bool { + return cp[i].lo < cp[j].lo || (cp[i].lo == cp[j].lo && cp[i].hi < cp[j].hi) + }) + out := []reRange{cp[0]} + for _, r := range cp[1:] { + last := &out[len(out)-1] + if r.lo <= last.hi+1 { // overlapping or adjacent + if r.hi > last.hi { + last.hi = r.hi + } + } else { + out = append(out, r) + } + } + return out +} + +// ---- render (minimal grouping, length-budgeted) ---- + +type reRenderer struct { + max int + n int + err error +} + +func (rr *reRenderer) write(sb *strings.Builder, s string) { + if rr.err != nil { + return + } + rr.n += len(s) + if rr.max > 0 && rr.n > rr.max { + rr.err = &ErrRegexTooLarge{Max: rr.max} + return + } + sb.WriteString(s) +} + +// render emits node; it self-groups with (?:...) when its precedence is looser +// than the surrounding context requires. +func (rr *reRenderer) render(sb *strings.Builder, n reNode, ctx int) { + if rr.err != nil { + return + } + grp := n.prec() < ctx + if grp { + rr.write(sb, "(?:") + } + switch v := n.(type) { + case *reEmpty: + // (?: ) already opened if grouped; otherwise emit an explicit empty. + if !grp { + rr.write(sb, "(?:)") + } + case *reNever: + // Negated full-Unicode class: matches no character, hence nothing. + rr.write(sb, "[^\\x00-\\x{10ffff}]") + case *reClass: + rr.write(sb, renderClass(v)) + case *reConcat: + for _, p := range v.parts { + rr.render(sb, p, precConcat) + } + case *reAlt: + for i, o := range v.opts { + if i > 0 { + rr.write(sb, "|") + } + rr.render(sb, o, precAlt) + } + case *reRepeat: + rr.render(sb, v.sub, precAtom) // operand of a quantifier must be an atom + rr.write(sb, quantifier(v.min, v.max)) + } + if grp { + rr.write(sb, ")") + } +} + +func renderClass(c *reClass) string { + if len(c.ranges) == 1 && c.ranges[0].lo == c.ranges[0].hi { + return regescape(c.ranges[0].lo) // bare single character + } + var b strings.Builder + b.WriteByte('[') + for _, r := range c.ranges { + if r.lo == r.hi { + b.WriteString(classEscape(r.lo)) + } else { + b.WriteString(classEscape(r.lo)) + b.WriteByte('-') + b.WriteString(classEscape(r.hi)) + } + } + b.WriteByte(']') + return b.String() +} + +func quantifier(min, max int) string { + switch { + case min == 0 && max == 1: + return "?" + case min == 0 && max == inf: + return "*" + case min == 1 && max == inf: + return "+" + case max == inf: + return fmt.Sprintf("{%d,}", min) + case min == max: + return fmt.Sprintf("{%d}", min) + case min == 0: + return fmt.Sprintf("{0,%d}", max) + default: + return fmt.Sprintf("{%d,%d}", min, max) + } +} + +// regescape quotes a single rune for use outside a character class. +func regescape(r rune) string { return regexp.QuoteMeta(string(r)) } + +// classEscape renders a rune as a character-class member. Alphanumerics are +// never class metacharacters; everything else is emitted as \x{...} to avoid +// the class-specific escaping pitfalls of ], ^, - and \. +func classEscape(r rune) string { + if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') { + return string(r) + } + return fmt.Sprintf("\\x{%x}", r) +} diff --git a/vendor/github.com/pandatix/go-abnf/rules.go b/vendor/github.com/pandatix/go-abnf/rules.go new file mode 100644 index 00000000000..a2c3987af6f --- /dev/null +++ b/vendor/github.com/pandatix/go-abnf/rules.go @@ -0,0 +1,1867 @@ +package goabnf + +var ( + inf int = -1 + + // coreRules are the core rules specified in RFC 5234 Section 8.1 + coreRules = map[string]*Rule{ + alpha.Name: alpha, + bit.Name: bit, + char.Name: char, + cr.Name: cr, + crlf.Name: crlf, + ctl.Name: ctl, + digit.Name: digit, + dquote.Name: dquote, + hexdig.Name: hexdig, + htab.Name: htab, + lf.Name: lf, + lwsp.Name: lwsp, + octet.Name: octet, + sp.Name: sp, + vchar.Name: vchar, + wsp.Name: wsp, + } + + // ALPHA = %x41-5A / %x61-7A + alpha = &Rule{ + Name: "ALPHA", + Alternation: Alternation{ + Concatenations: []Concatenation{ + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemNumVal{ + Base: "x", + Status: StatRange, + Elems: []string{"41", "5A"}, + }, + }, + }, + }, { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemNumVal{ + Base: "x", + Status: StatRange, + Elems: []string{"61", "7A"}, + }, + }, + }, + }, + }, + }, + } + + // BIT = "0" / "1" + bit = &Rule{ + Name: "BIT", + Alternation: Alternation{ + Concatenations: []Concatenation{ + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemCharVal{ + Sensitive: false, + Values: []rune{'0'}, + }, + }, + }, + }, { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemCharVal{ + Sensitive: false, + Values: []rune{'1'}, + }, + }, + }, + }, + }, + }, + } + + // CHAR = %x01-7F + char = &Rule{ + Name: "CHAR", + Alternation: Alternation{ + Concatenations: []Concatenation{ + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemNumVal{ + Base: "x", + Status: StatRange, + Elems: []string{"01", "7F"}, + }, + }, + }, + }, + }, + }, + } + + // CR = %x0D + cr = &Rule{ + Name: "CR", + Alternation: Alternation{ + Concatenations: []Concatenation{ + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemNumVal{ + Base: "x", + Status: StatSeries, + Elems: []string{"0D"}, + }, + }, + }, + }, + }, + }, + } + + // CRLF = CR LF + crlf = &Rule{ + Name: "CRLF", + Alternation: Alternation{ + Concatenations: []Concatenation{ + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemRulename{ + Name: "CR", + }, + }, { + Min: 1, + Max: 1, + Element: ElemRulename{ + Name: "LF", + }, + }, + }, + }, + }, + }, + } + + // CTL = %x00-1F / %x7F + ctl = &Rule{ + Name: "CTL", + Alternation: Alternation{ + Concatenations: []Concatenation{ + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemNumVal{ + Base: "x", + Status: StatRange, + Elems: []string{"00", "1F"}, + }, + }, + }, + }, { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemNumVal{ + Base: "x", + Status: StatSeries, + Elems: []string{"7F"}, + }, + }, + }, + }, + }, + }, + } + + // DIGIT = %x30-39 + digit = &Rule{ + Name: "DIGIT", + Alternation: Alternation{ + Concatenations: []Concatenation{ + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemNumVal{ + Base: "x", + Status: StatRange, + Elems: []string{"30", "39"}, + }, + }, + }, + }, + }, + }, + } + + // DQUOTE = %x22 + dquote = &Rule{ + Name: "DQUOTE", + Alternation: Alternation{ + Concatenations: []Concatenation{ + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemNumVal{ + Base: "x", + Status: StatSeries, + Elems: []string{"22"}, + }, + }, + }, + }, + }, + }, + } + + // HEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F" + hexdig = &Rule{ + Name: "HEXDIG", + Alternation: Alternation{ + Concatenations: []Concatenation{ + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemRulename{ + Name: "DIGIT", + }, + }, + }, + }, { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemCharVal{ + Sensitive: false, + Values: []rune{'A'}, + }, + }, + }, + }, { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemCharVal{ + Sensitive: false, + Values: []rune{'B'}, + }, + }, + }, + }, { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemCharVal{ + Sensitive: false, + Values: []rune{'C'}, + }, + }, + }, + }, { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemCharVal{ + Sensitive: false, + Values: []rune{'D'}, + }, + }, + }, + }, { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemCharVal{ + Sensitive: false, + Values: []rune{'E'}, + }, + }, + }, + }, { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemCharVal{ + Sensitive: false, + Values: []rune{'F'}, + }, + }, + }, + }, + }, + }, + } + + // HTAB = %x09 + htab = &Rule{ + Name: "HTAB", + Alternation: Alternation{ + Concatenations: []Concatenation{ + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemNumVal{ + Base: "x", + Status: StatSeries, + Elems: []string{"09"}, + }, + }, + }, + }, + }, + }, + } + + // LF = %x0A + lf = &Rule{ + Name: "LF", + Alternation: Alternation{ + Concatenations: []Concatenation{ + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemNumVal{ + Base: "x", + Status: StatSeries, + Elems: []string{"0A"}, + }, + }, + }, + }, + }, + }, + } + + // LWSP = *(WSP / CRLF WSP) + lwsp = &Rule{ + Name: "LWSP", + Alternation: Alternation{ + Concatenations: []Concatenation{ + { + Repetitions: []Repetition{ + { + Min: 0, + Max: inf, + Element: ElemGroup{ + Alternation: Alternation{ + Concatenations: []Concatenation{ + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemRulename{ + Name: "WSP", + }, + }, + }, + }, { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemRulename{ + Name: "CRLF", + }, + }, { + Min: 1, + Max: 1, + Element: ElemRulename{ + Name: "WSP", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + // OCTET = %x00-FF + octet = &Rule{ + Name: "OCTET", + Alternation: Alternation{ + Concatenations: []Concatenation{ + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemNumVal{ + Base: "x", + Status: StatRange, + Elems: []string{"00", "FF"}, + }, + }, + }, + }, + }, + }, + } + + // SP = %x20 + sp = &Rule{ + Name: "SP", + Alternation: Alternation{ + Concatenations: []Concatenation{ + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemNumVal{ + Base: "x", + Status: StatSeries, + Elems: []string{"20"}, + }, + }, + }, + }, + }, + }, + } + + // VCHAR = %x21-7E + vchar = &Rule{ + Name: "VCHAR", + Alternation: Alternation{ + Concatenations: []Concatenation{ + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemNumVal{ + Base: "x", + Status: StatRange, + Elems: []string{"21", "7E"}, + }, + }, + }, + }, + }, + }, + } + + // WSP = SP / HTAB + wsp = &Rule{ + Name: "WSP", + Alternation: Alternation{ + Concatenations: []Concatenation{ + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemRulename{ + Name: "SP", + }, + }, + }, + }, { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemRulename{ + Name: "HTAB", + }, + }, + }, + }, + }, + }, + } +) + +var ( + abnfRulelist = &Rule{ + Name: "rulelist", + Alternation: Alternation{ + Concatenations: []Concatenation{ + { + Repetitions: []Repetition{ + { + Min: 1, + Max: inf, + Element: ElemGroup{ + Alternation: Alternation{ + Concatenations: []Concatenation{ + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemRulename{ + Name: "rule", + }, + }, + }, + }, { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemGroup{ + Alternation: Alternation{ + Concatenations: []Concatenation{ + { + Repetitions: []Repetition{ + { + Min: 0, + Max: inf, + Element: ElemRulename{ + Name: "WSP", // Fixed according to Errata 3076 + }, + }, { + Min: 1, + Max: 1, + Element: ElemRulename{ + Name: "c-nl", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + abnfRule = &Rule{ + Name: "rule", + Alternation: Alternation{ + Concatenations: []Concatenation{ + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemRulename{ + Name: "rulename", + }, + }, { + Min: 1, + Max: 1, + Element: ElemRulename{ + Name: "defined-as", + }, + }, { + Min: 1, + Max: 1, + Element: ElemRulename{ + Name: "elements", + }, + }, { + Min: 1, + Max: 1, + Element: ElemRulename{ + Name: "c-nl", + }, + }, + }, + }, + }, + }, + } + + abnfRulename = &Rule{ + Name: "rulename", + Alternation: Alternation{ + Concatenations: []Concatenation{ + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemRulename{ + Name: "ALPHA", + }, + }, { + Min: 0, + Max: inf, + Element: ElemGroup{ + Alternation: Alternation{ + Concatenations: []Concatenation{ + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemRulename{ + Name: "ALPHA", + }, + }, + }, + }, { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemRulename{ + Name: "DIGIT", + }, + }, + }, + }, { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemCharVal{ + Sensitive: false, + Values: []rune{'-'}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + abnfDefinedAs = &Rule{ + Name: "defined-as", + Alternation: Alternation{ + Concatenations: []Concatenation{ + { + Repetitions: []Repetition{ + { + Min: 0, + Max: inf, + Element: ElemRulename{ + Name: "c-wsp", + }, + }, { + Min: 1, + Max: 1, + Element: ElemGroup{ + Alternation: Alternation{ + Concatenations: []Concatenation{ + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemCharVal{ + Sensitive: false, + Values: []rune{'='}, + }, + }, + }, + }, { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemCharVal{ + Sensitive: false, + Values: []rune{'=', '/'}, + }, + }, + }, + }, + }, + }, + }, + }, { + Min: 0, + Max: inf, + Element: ElemRulename{ + Name: "c-wsp", + }, + }, + }, + }, + }, + }, + } + + abnfElements = &Rule{ + Name: "elements", + Alternation: Alternation{ + Concatenations: []Concatenation{ + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemRulename{ + Name: "alternation", + }, + }, { + Min: 0, + Max: inf, + Element: ElemRulename{ + Name: "WSP", // Fixed according to Errata 2968 + }, + }, + }, + }, + }, + }, + } + + abnfCWsp = &Rule{ + Name: "c-wsp", + Alternation: Alternation{ + Concatenations: []Concatenation{ + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemRulename{ + Name: "WSP", + }, + }, + }, + }, { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemGroup{ + Alternation: Alternation{ + Concatenations: []Concatenation{ + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemRulename{ + Name: "c-nl", + }, + }, { + Min: 1, + Max: 1, + Element: ElemRulename{ + Name: "WSP", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + abnfCNl = &Rule{ + Name: "c-nl", + Alternation: Alternation{ + Concatenations: []Concatenation{ + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemRulename{ + Name: "comment", + }, + }, + }, + }, { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemRulename{ + Name: "CRLF", + }, + }, + }, + }, + }, + }, + } + + abnfComment = &Rule{ + Name: "comment", + Alternation: Alternation{ + Concatenations: []Concatenation{ + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemCharVal{ + Sensitive: false, + Values: []rune{';'}, + }, + }, + { + Min: 0, + Max: inf, + Element: ElemGroup{ + Alternation: Alternation{ + Concatenations: []Concatenation{ + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemRulename{ + Name: "WSP", + }, + }, + }, + }, + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemRulename{ + Name: "VCHAR", + }, + }, + }, + }, + }, + }, + }, + }, + { + Min: 1, + Max: 1, + Element: ElemRulename{ + Name: "CRLF", + }, + }, + }, + }, + }, + }, + } + + abnfAlternation = &Rule{ + Name: "alternation", + Alternation: Alternation{ + Concatenations: []Concatenation{ + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemRulename{ + Name: "concatenation", + }, + }, + { + Min: 0, + Max: inf, + Element: ElemGroup{ + Alternation: Alternation{ + Concatenations: []Concatenation{ + { + Repetitions: []Repetition{ + { + Min: 0, + Max: inf, + Element: ElemRulename{ + Name: "c-wsp", + }, + }, + { + Min: 1, + Max: 1, + Element: ElemCharVal{ + Sensitive: false, + Values: []rune{'/'}, + }, + }, + { + Min: 0, + Max: inf, + Element: ElemRulename{ + Name: "c-wsp", + }, + }, + { + Min: 1, + Max: 1, + Element: ElemRulename{ + Name: "concatenation", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + abnfConcatenation = &Rule{ + Name: "concatenation", + Alternation: Alternation{ + Concatenations: []Concatenation{ + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemRulename{ + Name: "repetition", + }, + }, + { + Min: 0, + Max: inf, + Element: ElemGroup{ + Alternation: Alternation{ + Concatenations: []Concatenation{ + { + Repetitions: []Repetition{ + { + Min: 1, + Max: inf, + Element: ElemRulename{ + Name: "c-wsp", + }, + }, + { + Min: 1, + Max: 1, + Element: ElemRulename{ + Name: "repetition", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + abnfRepetition = &Rule{ + Name: "repetition", + Alternation: Alternation{ + Concatenations: []Concatenation{ + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemOption{ + Alternation: Alternation{ + Concatenations: []Concatenation{ + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemRulename{ + Name: "repeat", + }, + }, + }, + }, + }, + }, + }, + }, + { + Min: 1, + Max: 1, + Element: ElemRulename{ + Name: "element", + }, + }, + }, + }, + }, + }, + } + + abnfRepeat = &Rule{ + Name: "repeat", + Alternation: Alternation{ + Concatenations: []Concatenation{ + { + Repetitions: []Repetition{ + { + Min: 1, + Max: inf, + Element: ElemRulename{ + Name: "DIGIT", + }, + }, + }, + }, + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemGroup{ + Alternation: Alternation{ + Concatenations: []Concatenation{ + { + Repetitions: []Repetition{ + { + Min: 0, + Max: inf, + Element: ElemRulename{ + Name: "DIGIT", + }, + }, + { + Min: 1, + Max: 1, + Element: ElemCharVal{ + Sensitive: false, + Values: []rune{'*'}, + }, + }, + { + Min: 0, + Max: inf, + Element: ElemRulename{ + Name: "DIGIT", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + abnfElement = &Rule{ + Name: "element", + Alternation: Alternation{ + Concatenations: []Concatenation{ + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemRulename{ + Name: "rulename", + }, + }, + }, + }, + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemRulename{ + Name: "group", + }, + }, + }, + }, + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemRulename{ + Name: "option", + }, + }, + }, + }, + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemRulename{ + Name: "char-val", + }, + }, + }, + }, + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemRulename{ + Name: "num-val", + }, + }, + }, + }, + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemRulename{ + Name: "prose-val", + }, + }, + }, + }, + }, + }, + } + + abnfGroup = &Rule{ + Name: "group", + Alternation: Alternation{ + Concatenations: []Concatenation{ + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemCharVal{ + Sensitive: false, + Values: []rune{'('}, + }, + }, + { + Min: 0, + Max: inf, + Element: ElemRulename{ + Name: "c-wsp", + }, + }, + { + Min: 1, + Max: 1, + Element: ElemRulename{ + Name: "alternation", + }, + }, + { + Min: 0, + Max: inf, + Element: ElemRulename{ + Name: "c-wsp", + }, + }, + { + Min: 1, + Max: 1, + Element: ElemCharVal{ + Sensitive: false, + Values: []rune{')'}, + }, + }, + }, + }, + }, + }, + } + + abnfOption = &Rule{ + Name: "option", + Alternation: Alternation{ + Concatenations: []Concatenation{ + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemCharVal{ + Sensitive: false, + Values: []rune{'['}, + }, + }, + { + Min: 0, + Max: inf, + Element: ElemRulename{ + Name: "c-wsp", + }, + }, + { + Min: 1, + Max: 1, + Element: ElemRulename{ + Name: "alternation", + }, + }, + { + Min: 0, + Max: inf, + Element: ElemRulename{ + Name: "c-wsp", + }, + }, + { + Min: 1, + Max: 1, + Element: ElemCharVal{ + Sensitive: false, + Values: []rune{']'}, + }, + }, + }, + }, + }, + }, + } + + // Written as overrided by RFC 7405 + abnfCharVal = &Rule{ + Name: "char-val", + Alternation: Alternation{ + Concatenations: []Concatenation{ + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemRulename{ + Name: "case-insensitive-string", + }, + }, + }, + }, { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemRulename{ + Name: "case-sensitive-string", + }, + }, + }, + }, + }, + }, + } + + abnfCaseInsensitiveString = &Rule{ + Name: "case-insensitive-string", + Alternation: Alternation{ + Concatenations: []Concatenation{ + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemOption{ + Alternation: Alternation{ + Concatenations: []Concatenation{ + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemCharVal{ + Sensitive: false, + Values: []rune{'%', 'i'}, + }, + }, + }, + }, + }, + }, + }, + }, { + Min: 1, + Max: 1, + Element: ElemRulename{ + Name: "quoted-string", + }, + }, + }, + }, + }, + }, + } + + abnfCaseSensitiveString = &Rule{ + Name: "case-sensitive-string", + Alternation: Alternation{ + Concatenations: []Concatenation{ + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemCharVal{ + Sensitive: false, + Values: []rune{'%', 's'}, + }, + }, { + Min: 1, + Max: 1, + Element: ElemRulename{ + Name: "quoted-string", + }, + }, + }, + }, + }, + }, + } + + abnfQuotedString = &Rule{ + Name: "quoted-string", + Alternation: Alternation{ + Concatenations: []Concatenation{ + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemRulename{ + Name: "DQUOTE", + }, + }, { + Min: 0, + Max: inf, + Element: ElemGroup{ + Alternation: Alternation{ + Concatenations: []Concatenation{ + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemNumVal{ + Base: "x", + Status: StatRange, + Elems: []string{"20", "21"}, + }, + }, + }, + }, { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemNumVal{ + Base: "x", + Status: StatRange, + Elems: []string{"23", "7E"}, + }, + }, + }, + }, + }, + }, + }, + }, { + Min: 1, + Max: 1, + Element: ElemRulename{ + Name: "DQUOTE", + }, + }, + }, + }, + }, + }, + } + + abnfNumVal = &Rule{ + Name: "num-val", + Alternation: Alternation{ + Concatenations: []Concatenation{ + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemCharVal{ + Sensitive: false, + Values: []rune{'%'}, + }, + }, + { + Min: 1, + Max: 1, + Element: ElemGroup{ + Alternation: Alternation{ + Concatenations: []Concatenation{ + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemRulename{ + Name: "bin-val", + }, + }, + }, + }, + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemRulename{ + Name: "dec-val", + }, + }, + }, + }, + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemRulename{ + Name: "hex-val", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + abnfBinVal = &Rule{ + Name: "bin-val", + Alternation: Alternation{ + Concatenations: []Concatenation{ + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemCharVal{ + Sensitive: false, + Values: []rune{'b'}, + }, + }, + { + Min: 1, + Max: inf, + Element: ElemRulename{ + Name: "BIT", + }, + }, + { + Min: 1, + Max: 1, + Element: ElemOption{ + Alternation: Alternation{ + Concatenations: []Concatenation{ + { + Repetitions: []Repetition{ + { + Min: 1, + Max: inf, + Element: ElemGroup{ + Alternation: Alternation{ + Concatenations: []Concatenation{ + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemCharVal{ + Sensitive: false, + Values: []rune{'.'}, + }, + }, + { + Min: 1, + Max: inf, + Element: ElemRulename{ + Name: "BIT", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemGroup{ + Alternation: Alternation{ + Concatenations: []Concatenation{ + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemCharVal{ + Sensitive: false, + Values: []rune{'-'}, + }, + }, + { + Min: 1, + Max: inf, + Element: ElemRulename{ + Name: "BIT", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + abnfDecVal = &Rule{ + Name: "dec-val", + Alternation: Alternation{ + Concatenations: []Concatenation{ + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemCharVal{ + Sensitive: false, + Values: []rune{'d'}, + }, + }, + { + Min: 1, + Max: inf, + Element: ElemRulename{ + Name: "DIGIT", + }, + }, + { + Min: 1, + Max: 1, + Element: ElemOption{ + Alternation: Alternation{ + Concatenations: []Concatenation{ + { + Repetitions: []Repetition{ + { + Min: 1, + Max: inf, + Element: ElemGroup{ + Alternation: Alternation{ + Concatenations: []Concatenation{ + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemCharVal{ + Sensitive: false, + Values: []rune{'.'}, + }, + }, + { + Min: 1, + Max: inf, + Element: ElemRulename{ + Name: "DIGIT", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemGroup{ + Alternation: Alternation{ + Concatenations: []Concatenation{ + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemCharVal{ + Sensitive: false, + Values: []rune{'-'}, + }, + }, + { + Min: 1, + Max: inf, + Element: ElemRulename{ + Name: "DIGIT", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + abnfHexVal = &Rule{ + Name: "hex-val", + Alternation: Alternation{ + Concatenations: []Concatenation{ + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemCharVal{ + Sensitive: false, + Values: []rune{'x'}, + }, + }, + { + Min: 1, + Max: inf, + Element: ElemRulename{ + Name: "HEXDIG", + }, + }, + { + Min: 1, + Max: 1, + Element: ElemOption{ + Alternation: Alternation{ + Concatenations: []Concatenation{ + { + Repetitions: []Repetition{ + { + Min: 1, + Max: inf, + Element: ElemGroup{ + Alternation: Alternation{ + Concatenations: []Concatenation{ + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemCharVal{ + Sensitive: false, + Values: []rune{'.'}, + }, + }, + { + Min: 1, + Max: inf, + Element: ElemRulename{ + Name: "HEXDIG", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemGroup{ + Alternation: Alternation{ + Concatenations: []Concatenation{ + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemCharVal{ + Sensitive: false, + Values: []rune{'-'}, + }, + }, + { + Min: 1, + Max: inf, + Element: ElemRulename{ + Name: "HEXDIG", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + abnfProseVal = &Rule{ + Name: "prose-val", + Alternation: Alternation{ + Concatenations: []Concatenation{ + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemCharVal{ + Sensitive: false, + Values: []rune{'<'}, + }, + }, + { + Min: 0, + Max: inf, + Element: ElemGroup{ + Alternation: Alternation{ + Concatenations: []Concatenation{ + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemNumVal{ + Base: "x", + Status: StatRange, + Elems: []string{"20", "3D"}, + }, + }, + }, + }, + { + Repetitions: []Repetition{ + { + Min: 1, + Max: 1, + Element: ElemNumVal{ + Base: "x", + Status: StatRange, + Elems: []string{"3F", "7E"}, + }, + }, + }, + }, + }, + }, + }, + }, + { + Min: 1, + Max: 1, + Element: ElemCharVal{ + Sensitive: false, + Values: []rune{'>'}, + }, + }, + }, + }, + }, + }, + } +) diff --git a/vendor/github.com/pandatix/go-abnf/sppf.go b/vendor/github.com/pandatix/go-abnf/sppf.go new file mode 100644 index 00000000000..d503e83a195 --- /dev/null +++ b/vendor/github.com/pandatix/go-abnf/sppf.go @@ -0,0 +1,902 @@ +package goabnf + +import ( + "fmt" + "math/big" + "strconv" + "unicode/utf8" +) + +// This file implements a GLL parser (Scott & Johnstone) producing a binarized +// Shared Packed Parse Forest (SPPF) for ABNF grammars. +// +// GLL parses ANY context-free grammar -- including left, right, hidden and +// mutual recursion, and arbitrary ambiguity -- in worst-case O(n^3) time and +// space, using a graph-structured stack (GSS) and a descriptor worklist whose +// "seen" set makes recursion terminate. The forest shares every sub-derivation +// and packs ambiguity, so it stays polynomial even when it represents +// exponentially or infinitely many trees. +// +// Pipeline: lower the ABNF AST into a flat slotted grammar (synthetic +// nonterminals for groups, options and repetitions), then run GLL over the +// slots. Recognition, ambiguity, tree counting and extraction are polynomial +// queries over the resulting forest. + +// --------------------------------------------------------------------------- +// Slotted grammar +// --------------------------------------------------------------------------- + +type symKind uint8 + +const ( + symNonterm symKind = iota + symTerm // a char-val / num-val / prose-val matcher + symEps // matches the empty string +) + +type ssym struct { + kind symKind + nt int // nonterminal id, for symNonterm + term ElemItf // terminal element, for symTerm +} + +type sgNT struct { + label string // diagnostic label + isRule bool // corresponds to an original ABNF rule (kept in the tree) + ruleName string // original rule name when isRule + alts [][]ssym // alternates; every alternate has >= 1 symbol + + // Native counted repetition. When isRep, the nonterminal stands for + // `repElem{repMin,repMax}` and its two alternates are the self-recursive + // encoding `R ::= eps | repElem R`. The [repMin,repMax] bound is enforced at + // parse time by a count carried in the GLL state (see addRepAlts), so the + // slot grammar stays O(1) in the repetition bound instead of unrolling it. + isRep bool + repMin int + repMax int // inf (== -1) means unbounded +} + +type slotGrammar struct { + nts []*sgNT + index map[string]int // dedup key -> nt id + start int + la *lookahead // selection sets guarding descriptor creation +} + +func (sg *slotGrammar) reserve(key, label string) (int, bool) { + if id, ok := sg.index[key]; ok { + return id, true + } + id := len(sg.nts) + sg.nts = append(sg.nts, &sgNT{label: label}) + sg.index[key] = id + return id, false +} + +// compileSlots lowers grammar starting at rootRulename into a slotted grammar. +const defaultMaxSlots = 1 << 16 + +func compileSlots(g *Grammar, rootRulename string, maxSlots int) (*slotGrammar, error) { + if GetRule(rootRulename, g.Rulemap) == nil { + return nil, &ErrRuleNotFound{Rulename: rootRulename} + } + sg := &slotGrammar{index: map[string]int{}} + c := &lowerer{g: g, sg: sg, max: maxSlots} + sg.start = c.rule(rootRulename) + if c.err != nil { + return nil, c.err + } + if c.over { + return nil, &ErrGrammarTooLarge{Max: maxSlots} + } + sg.la = computeLookahead(sg) + return sg, nil +} + +type lowerer struct { + g *Grammar + sg *slotGrammar + err error + max int // max nonterminals; 0 == unbounded + over bool // budget exceeded +} + +func (c *lowerer) budgetExceeded() bool { + if c.max > 0 && len(c.sg.nts) >= c.max { + c.over = true + } + return c.over +} + +func (c *lowerer) rule(name string) int { + key := "rule:" + canon(name) + id, existed := c.sg.reserve(key, name) + if existed { + return id + } + if c.budgetExceeded() { + return id + } + rule := GetRule(name, c.g.Rulemap) + if rule == nil { + // Unknown rule: a nonterminal with no alternates derives nothing. + c.sg.nts[id].label = name + return id + } + c.sg.nts[id].isRule = true + c.sg.nts[id].ruleName = rule.Name + c.sg.nts[id].alts = c.alts(rule.Alternation) + return id +} + +func (c *lowerer) alts(a Alternation) [][]ssym { + out := make([][]ssym, 0, len(a.Concatenations)) + for _, cc := range a.Concatenations { + seq := make([]ssym, 0, len(cc.Repetitions)) + for _, rep := range cc.Repetitions { + seq = append(seq, c.rep(rep)) + } + if len(seq) == 0 { + seq = []ssym{{kind: symEps}} + } + out = append(out, seq) + } + return out +} + +func (c *lowerer) rep(rep Repetition) ssym { + if rep.Min == 1 && rep.Max == 1 { + return c.elem(rep.Element) + } + key := "rep:" + rep.String() + id, existed := c.sg.reserve(key, rep.String()) + if existed { + return ssym{kind: symNonterm, nt: id} + } + if c.budgetExceeded() { + return ssym{kind: symNonterm, nt: id} + } + // A malformed bound where max < min accepts nothing: leave the rep + // nonterminal with no alternates so it derives the empty language (it is + // unsatisfiable, NOT unbounded). This matches the recognizer. + if rep.Max != inf && rep.Max < rep.Min { + return ssym{kind: symNonterm, nt: id} + } + // Lower to the self-recursive encoding R ::= eps | E R, a single nonterminal + // regardless of the bound. The [Min,Max] count is enforced at parse time + // (addRepAlts), so lowering is O(1) in the bound and there is no per-count + // nonterminal explosion (the old optTail/reqOne unrolling, which scaled with + // Max-Min and Min respectively). + esym := c.elem(rep.Element) + self := ssym{kind: symNonterm, nt: id} + c.sg.nts[id].alts = [][]ssym{ + {{kind: symEps}}, // alt 0: stop + {esym, self}, // alt 1: consume one element, then loop + } + c.sg.nts[id].label = rep.String() + c.sg.nts[id].isRep = true + c.sg.nts[id].repMin = rep.Min + c.sg.nts[id].repMax = rep.Max + return self +} + +func (c *lowerer) elem(e ElemItf) ssym { + switch v := e.(type) { + case ElemRulename: + return ssym{kind: symNonterm, nt: c.rule(v.Name)} + case ElemGroup: + return ssym{kind: symNonterm, nt: c.group(v)} + case ElemOption: + return ssym{kind: symNonterm, nt: c.option(v)} + case ElemCharVal, ElemNumVal, ElemProseVal: + return ssym{kind: symTerm, term: e} + } + return ssym{kind: symTerm, term: e} +} + +func (c *lowerer) group(v ElemGroup) int { + key := "grp:" + v.String() + id, existed := c.sg.reserve(key, v.String()) + if existed { + return id + } + if c.budgetExceeded() { + return id + } + c.sg.nts[id].alts = c.alts(v.Alternation) + return id +} + +func (c *lowerer) option(v ElemOption) int { + key := "opt:" + v.String() + id, existed := c.sg.reserve(key, v.String()) + if existed { + return id + } + if c.budgetExceeded() { + return id + } + alts := [][]ssym{{{kind: symEps}}} + alts = append(alts, c.alts(v.Alternation)...) + c.sg.nts[id].alts = alts + return id +} + +func canon(name string) string { + b := []byte(name) + for i := range b { + if b[i] >= 'A' && b[i] <= 'Z' { + b[i] += 'a' - 'A' + } + } + return string(b) +} + +// --------------------------------------------------------------------------- +// SPPF nodes +// --------------------------------------------------------------------------- + +type gnodeKind uint8 + +const ( + gSymbol gnodeKind = iota // a nonterminal node (Start..End) + gInter // a binarisation intermediate node + gTerm // a terminal / epsilon node +) + +type gnode struct { + kind gnodeKind + nt int // symbol nonterminal id, for gSymbol + Start, End int + packs []pack // >1 pack == ambiguity +} + +// pack is a packed node. It is labelled by the grammar slot it completes and +// its pivot (Scott & Johnstone, "GLL parse-tree generation", Science of +// Computer Programming 78(10), 2013, §3.1); the pivot is the left extent of the +// last child. Two alternates deriving the same span through the same children +// are distinct derivations, hence distinct packed nodes. +type pack struct { + L slot + kids []*gnode // 1 or 2 children +} + +func (n *gnode) addPack(L slot, children []*gnode) { + for _, p := range n.packs { + if p.L != L || len(p.kids) != len(children) { + continue + } + same := true + for i := range p.kids { + if p.kids[i] != children[i] { + same = false + break + } + } + if same { + return + } + } + n.packs = append(n.packs, pack{L: L, kids: children}) +} + +// --------------------------------------------------------------------------- +// GLL machinery +// --------------------------------------------------------------------------- + +type slot struct{ nt, alt, dot int } + +type gssNode struct { + ret slot // return slot + pos int + rc int // repetition count carried for native counted repetition (0 otherwise) + edges []gssEdge + edgeSet map[gssEdge]bool +} + +type gssEdge struct { + w *gnode + to *gssNode +} + +type descriptor struct { + L slot + u *gssNode + i int + w *gnode + rc int // repetition count (0 outside a counted repetition) +} + +type gllParser struct { + sg *slotGrammar + input []byte + + nodes map[nodeKey]*gnode + gss map[gssKey]*gssNode + u0 *gssNode + seen map[descriptor]bool + work []descriptor + popped map[*gssNode]map[*gnode]bool + + maxNodes int + aborted bool + + // Furthest-failure diagnostics: maxPos is the deepest input offset at which + // a terminal match was attempted and failed; expected collects the terminals + // that could have been consumed there (deduplicated via expectedSeen). They + // drive ParseError when the parse has no solution. + maxPos int + expected []string + expectedSeen map[string]bool +} + +// nodeKey interns SPPF nodes without allocating strings. Distinct kinds never +// collide: symbol nodes key on (kind,nt,start,end); intermediate nodes on +// (kind,L,start,end); terminal nodes on (kind,start,end). +type nodeKey struct { + kind gnodeKind + nt int + L slot + start, end int +} + +type gssKey struct { + ret slot + pos int + rc int +} + +// addAlt adds the descriptor starting alternate L.alt of L.nt at i when the +// lookahead test admits it. A rejected alternate would fail on its first +// terminal at i (or, when nullable, its continuation would), so its terminals +// are reported to the furthest-failure diagnostics in its place. +func (p *gllParser) addAlt(L slot, u *gssNode, i int, rc int) { + if p.admits(p.sg.la.alt(L.nt, L.alt), i) { + p.add(L, u, i, nil, rc) + } +} + +// admits applies a lookahead test at i. A rejected sequence would have failed +// on one of the set's terminals at i, so those feed the furthest-failure +// diagnostics in its place. +func (p *gllParser) admits(s *laSet, i int) bool { + if s.admits(p.input, i) { + return true + } + if i >= p.maxPos { + for _, t := range s.terms { + p.noteFail(i, t) + } + } + return false +} + +func (p *gllParser) add(L slot, u *gssNode, i int, w *gnode, rc int) { + d := descriptor{L, u, i, w, rc} + if p.seen[d] { + return + } + p.seen[d] = true + p.work = append(p.work, d) +} + +func (p *gllParser) gssNodeFor(ret slot, pos, rc int) *gssNode { + k := gssKey{ret, pos, rc} + if v, ok := p.gss[k]; ok { + return v + } + v := &gssNode{ret: ret, pos: pos, rc: rc, edgeSet: map[gssEdge]bool{}} + p.gss[k] = v + return v +} + +func (p *gllParser) create(L slot, u *gssNode, i int, w *gnode, rc int) *gssNode { + v := p.gssNodeFor(L, i, rc) + e := gssEdge{w: w, to: u} + if !v.edgeSet[e] { + v.edgeSet[e] = true + v.edges = append(v.edges, e) + for z := range p.popped[v] { + y := p.getNodeP(L, w, z) + p.add(L, u, z.End, y, rc) + } + } + return v +} + +func (p *gllParser) pop(u *gssNode, i int, z *gnode) { + if u == p.u0 { + return + } + if p.popped[u] == nil { + p.popped[u] = map[*gnode]bool{} + } + p.popped[u][z] = true + for _, e := range u.edges { + y := p.getNodeP(u.ret, e.w, z) + p.add(u.ret, e.to, i, y, u.rc) + } +} + +func (p *gllParser) findNode(k nodeKey) *gnode { + if v, ok := p.nodes[k]; ok { + return v + } + if p.maxNodes > 0 && len(p.nodes) >= p.maxNodes { + p.aborted = true + } + v := &gnode{kind: k.kind, nt: k.nt, Start: k.start, End: k.end} + p.nodes[k] = v + return v +} + +func (p *gllParser) getNodeT(start, end int) *gnode { + return p.findNode(nodeKey{kind: gTerm, nt: -1, start: start, end: end}) +} + +// getNodeP combines left node w (may be nil == $) and right node z under slot L. +func (p *gllParser) getNodeP(L slot, w, z *gnode) *gnode { + prod := p.sg.nts[L.nt].alts[L.alt] + betaEmpty := L.dot == len(prod) + if L.dot == 1 && !betaEmpty { + return z // single symbol consumed, more to come: pass straight up + } + start := z.Start + if w != nil { + start = w.Start + } + var k nodeKey + if betaEmpty { + k = nodeKey{kind: gSymbol, nt: L.nt, start: start, end: z.End} + } else { + k = nodeKey{kind: gInter, nt: -1, L: L, start: start, end: z.End} + } + y := p.findNode(k) + if w != nil { + y.addPack(L, []*gnode{w, z}) + } else { + y.addPack(L, []*gnode{z}) + } + return y +} + +// matchTerm returns the end index after matching the terminal at i, or -1. +func (p *gllParser) matchTerm(s ssym, i int) int { + if s.kind == symEps { + return i + } + switch v := s.term.(type) { + case ElemCharVal: + idx := i + for k := 0; k < len(v.Values); k++ { + if idx >= len(p.input) { + return -1 + } + r, size := utf8.DecodeRune(p.input[idx:]) + if r == utf8.RuneError && size == 1 { + return -1 + } + if !sensequal(v.Values[k], r, v.Sensitive) { + return -1 + } + idx += size + } + return idx + case ElemNumVal: + switch v.Status { + case StatRange: + if i >= len(p.input) { + return -1 + } + // Numeric comparison: a range straddling U+10FFFF matches its rune + // subset; numvalToRune never panics (saturates out-of-int32 bounds). + min := numvalToRune(v.Elems[0], v.Base) + max := numvalToRune(v.Elems[1], v.Base) + r, size := utf8.DecodeRune(p.input[i:]) + if r == utf8.RuneError && size == 1 { + return -1 + } + if min <= r && r <= max { + return i + size + } + return -1 + case StatSeries: + idx := i + for k := 0; k < len(v.Elems); k++ { + ru := numvalToRune(v.Elems[k], v.Base) + // Series elements must be real characters: an out-of-range value + // matches no UTF-8 input (and avoids a spurious U+FFFD match). + if !utf8.ValidRune(ru) { + return -1 + } + str := string(ru) + sz := len(str) + if idx+sz > len(p.input) || str != string(p.input[idx:idx+sz]) { + return -1 + } + idx += sz + } + return idx + } + } + return -1 // prose-val and anything else never matches +} + +// parse runs the GLL loop and returns the symbol node (start, 0, n) or nil. +func (p *gllParser) parse() *gnode { + startNT := p.sg.start + p.u0 = &gssNode{ret: slot{-1, -1, -1}, pos: 0} + for ai := range p.sg.nts[startNT].alts { + p.addAlt(slot{startNT, ai, 0}, p.u0, 0, 0) + } + for len(p.work) > 0 { + if p.aborted { + return nil + } + d := p.work[len(p.work)-1] + p.work = p.work[:len(p.work)-1] + p.process(d) + } + key := nodeKey{kind: gSymbol, nt: startNT, start: 0, end: len(p.input)} + return p.nodes[key] +} + +func (p *gllParser) process(d descriptor) { + L, u, i, w, rc := d.L, d.u, d.i, d.w, d.rc + for { + prod := p.sg.nts[L.nt].alts[L.alt] + if L.dot == len(prod) { + z := w + if z == nil { + z = p.getNodeT(i, i) + } + p.pop(u, i, z) + return + } + s := prod[L.dot] + if s.kind == symNonterm { + if !p.admits(p.sg.la.callAt(L), i) { + return + } + ret := slot{L.nt, L.alt, L.dot + 1} + // Preserve the current repetition frame count across the sub-parse so + // that when it returns we resume the loop with the right count. + v := p.create(ret, u, i, w, rc) + if p.sg.nts[s.nt].isRep { + // Native counted repetition: a self-reference inside the rep's own + // loop alternate continues the chain (count+1); any other reference + // starts a fresh chain at count 0. + childRC := 0 + if s.nt == L.nt { + childRC = capRC(rc, p.sg.nts[s.nt]) + } + p.addRepAlts(s.nt, v, i, childRC) + } else { + for ai := range p.sg.nts[s.nt].alts { + p.addAlt(slot{s.nt, ai, 0}, v, i, 0) + } + } + return + } + j := p.matchTerm(s, i) + if j < 0 { + p.noteFail(i, s) + return + } + cR := p.getNodeT(i, j) + next := slot{L.nt, L.alt, L.dot + 1} + w = p.getNodeP(next, w, cR) + i = j + L = next + } +} + +// noteFail records that a terminal match failed at input offset i. It maintains +// the furthest such offset and, for that offset only, the set of terminals that +// could have been consumed -- the classic "furthest-failure" position used to +// report where parsing got stuck. +func (p *gllParser) noteFail(i int, s ssym) { + if i > p.maxPos { + p.maxPos = i + p.expected = p.expected[:0] + p.expectedSeen = nil + } + if i != p.maxPos { + return + } + desc := "?" + if str, ok := s.term.(fmt.Stringer); ok { + desc = str.String() + } + if p.expectedSeen == nil { + p.expectedSeen = map[string]bool{} + } + if !p.expectedSeen[desc] { + p.expectedSeen[desc] = true + p.expected = append(p.expected, desc) + } +} + +// capRC returns the count after consuming one more element of a repetition R +// currently at count rc. For an unbounded repetition it saturates at repMin +// (once the minimum is met the exact count no longer matters), which keeps the +// reachable counts -- and hence the GLL state -- finite even when the element is +// nullable. +func capRC(rc int, nt *sgNT) int { + next := rc + 1 + if nt.repMax == inf && next > nt.repMin { + next = nt.repMin + } + return next +} + +// addRepAlts seeds the two alternates of a repetition nonterminal, gated by the +// current count rc: it may stop (alt 0) only once the minimum is met, and may +// consume another element (alt 1) only while under the maximum. This enforces +// the {repMin,repMax} bound in parser state instead of unrolling it into the +// grammar, mirroring the recognizer's c>=min / c= nt.repMin { + p.addAlt(slot{ntID, 0, 0}, v, i, rc) // stop: alt 0 == [eps] + } + if nt.repMax == inf || rc < nt.repMax { + p.addAlt(slot{ntID, 1, 0}, v, i, rc) // loop: alt 1 == [element, self] + } +} + +// --------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------- + +// Forest is a shared packed parse forest produced by ParseForest. +type Forest struct { + sg *slotGrammar + input []byte + rulename string + root *gnode + built int + + // maxPos / expected carry the furthest-failure diagnostics from the parse, + // surfaced through ParseError when the forest is invalid. + maxPos int + expected []string +} + +// ForestOption configures ParseForest. +type ForestOption func(*forestConfig) + +type forestConfig struct { + maxNodes int + maxSlots int +} + +// WithMaxForestNodes bounds the number of SPPF nodes the forest may allocate. +// Because a complete SPPF is worst-case cubic in the input length, this caps the +// time and memory ParseForest can spend on adversarial input. 0 means unbounded. +func WithMaxForestNodes(n int) ForestOption { return func(c *forestConfig) { c.maxNodes = n } } + +// WithMaxSlots bounds the number of nonterminals produced while lowering the +// grammar. A finite repetition m*n lowers to ~n nonterminals, so this caps the +// cost of an adversarial grammar such as a = 9999999999"x". 0 means unbounded; +// the default is 65536. +func WithMaxSlots(n int) ForestOption { return func(c *forestConfig) { c.maxSlots = n } } + +// ErrForestTooLarge is returned by ParseForest when the SPPF exceeds the +// configured WithMaxForestNodes budget. +type ErrForestTooLarge struct{ Max int } + +func (e *ErrForestTooLarge) Error() string { + return "abnf: parse forest exceeded " + strconv.Itoa(e.Max) + " nodes" +} + +// ErrGrammarTooLarge is returned by ParseForest when lowering the grammar would +// exceed the WithMaxSlots budget, typically because of an absurd repetition +// bound (e.g. a = 9999999999"x"). +type ErrGrammarTooLarge struct{ Max int } + +func (e *ErrGrammarTooLarge) Error() string { + return "abnf: grammar lowering exceeded " + strconv.Itoa(e.Max) + " nonterminals" +} + +func newGLLParser(sg *slotGrammar, input []byte, maxNodes int) *gllParser { + return &gllParser{ + sg: sg, + input: input, + nodes: map[nodeKey]*gnode{}, + gss: map[gssKey]*gssNode{}, + seen: map[descriptor]bool{}, + popped: map[*gssNode]map[*gnode]bool{}, + maxNodes: maxNodes, + } +} + +// ParseForest parses input with grammar starting at rootRulename using GLL, +// returning the shared packed parse forest. It runs in worst-case O(n^3) time +// and space for ANY grammar -- left/right/mutual recursion and ambiguity +// included -- so it is safe on untrusted or malformed grammars. +func ParseForest(input []byte, grammar *Grammar, rootRulename string, opts ...ForestOption) (*Forest, error) { + cfg := forestConfig{maxSlots: defaultMaxSlots} + for _, o := range opts { + o(&cfg) + } + sg, err := compileSlots(grammar, rootRulename, cfg.maxSlots) + if err != nil { + return nil, err + } + p := newGLLParser(sg, input, cfg.maxNodes) + root := p.parse() + if p.aborted { + return nil, &ErrForestTooLarge{Max: cfg.maxNodes} + } + return &Forest{sg: sg, input: input, rulename: rootRulename, root: root, built: len(p.nodes), maxPos: p.maxPos, expected: p.expected}, nil +} + +// Valid reports whether the whole input is derivable by the root rule. +func (f *Forest) Valid() bool { return f.root != nil } + +// ParseError returns the furthest-failure diagnostic for an invalid forest: +// where parsing got stuck and which terminals were expected there. It returns +// nil when the forest is valid. The returned error unwraps to +// [ErrNoSolutionFound]. +func (f *Forest) ParseError() *ParseError { + if f.Valid() { + return nil + } + return newParseError(f.input, f.maxPos, f.expected) +} + +// Nodes returns the number of forest nodes reachable from the root. +func (f *Forest) Nodes() int { + if f.root == nil { + return 0 + } + seen := map[*gnode]bool{} + var walk func(n *gnode) + walk = func(n *gnode) { + if seen[n] { + return + } + seen[n] = true + for _, pk := range n.packs { + for _, c := range pk.kids { + walk(c) + } + } + } + walk(f.root) + return len(seen) +} + +// Ambiguous reports whether the input has more than one distinct parse tree. +func (f *Forest) Ambiguous() bool { + if f.root == nil { + return false + } + seen := map[*gnode]bool{} + var walk func(n *gnode) bool + walk = func(n *gnode) bool { + if seen[n] { + return false + } + seen[n] = true + if len(n.packs) > 1 { + return true + } + for _, pk := range n.packs { + for _, c := range pk.kids { + if walk(c) { + return true + } + } + } + return false + } + return walk(f.root) +} + +// NumTrees returns the number of distinct parse trees the forest represents. +// A result of -1 means infinitely many (a cycle in the forest, i.e. an +// infinitely-ambiguous grammar such as A = A / "x"). +func (f *Forest) NumTrees() *big.Int { + if f.root == nil { + return big.NewInt(0) + } + memo := map[*gnode]*big.Int{} + onStack := map[*gnode]bool{} + infinite := false + var count func(n *gnode) *big.Int + count = func(n *gnode) *big.Int { + if v, ok := memo[n]; ok { + return v + } + if onStack[n] { + infinite = true + return big.NewInt(1) + } + onStack[n] = true + total := big.NewInt(0) + if len(n.packs) == 0 { + total = big.NewInt(1) + } + for _, pk := range n.packs { + prod := big.NewInt(1) + for _, c := range pk.kids { + prod.Mul(prod, count(c)) + } + total.Add(total, prod) + } + onStack[n] = false + memo[n] = total + return total + } + res := count(f.root) + if infinite { + return big.NewInt(-1) + } + return res +} + +// ParseTree is a concrete syntax tree keyed by grammar rules: rule nodes carry +// the rule name, terminals are leaves with empty Rule. +type ParseTree struct { + Rule string + Start, End int + Children []*ParseTree +} + +// Tree extracts a single parse tree (first packing at each node), or nil if the +// input is invalid. A visited guard keeps extraction finite on cyclic forests. +func (f *Forest) Tree() *ParseTree { + if f.root == nil { + return nil + } + return f.emitSymbol(f.root, map[*gnode]bool{}) +} + +func (f *Forest) emitSymbol(n *gnode, onStack map[*gnode]bool) *ParseTree { + t := &ParseTree{Start: n.Start, End: n.End} + if n.kind == gSymbol && n.nt >= 0 && f.sg.nts[n.nt].isRule { + t.Rule = f.sg.nts[n.nt].ruleName + } + onStack[n] = true + f.collect(n, &t.Children, onStack) + delete(onStack, n) + return t +} + +func (f *Forest) collect(n *gnode, into *[]*ParseTree, onStack map[*gnode]bool) { + if len(n.packs) == 0 { + if n.kind == gTerm && n.End > n.Start { + *into = append(*into, &ParseTree{Start: n.Start, End: n.End}) + } + return + } + for _, c := range n.packs[0].kids { + f.collectChild(c, into, onStack) + } +} + +func (f *Forest) collectChild(c *gnode, into *[]*ParseTree, onStack map[*gnode]bool) { + switch { + case c.kind == gTerm: + if c.End > c.Start { + *into = append(*into, &ParseTree{Start: c.Start, End: c.End}) + } + case c.kind == gSymbol && c.nt >= 0 && f.sg.nts[c.nt].isRule: + if onStack[c] { + *into = append(*into, &ParseTree{Rule: f.sg.nts[c.nt].ruleName, Start: c.Start, End: c.End}) + return + } + *into = append(*into, f.emitSymbol(c, onStack)) + default: + if onStack[c] { + return + } + onStack[c] = true + f.collect(c, into, onStack) + delete(onStack, c) + } +} diff --git a/vendor/github.com/pandatix/go-abnf/transition_graph.go b/vendor/github.com/pandatix/go-abnf/transition_graph.go new file mode 100644 index 00000000000..336318ee2f8 --- /dev/null +++ b/vendor/github.com/pandatix/go-abnf/transition_graph.go @@ -0,0 +1,1546 @@ +package goabnf + +import ( + "crypto/rand" + "encoding/hex" + "fmt" + "slices" + "sort" + "strings" + "unicode/utf8" +) + +// TransitionGraph represent a grammar transition graph. +// It contains the list of entrypoints and endpoints. +// You could travel through the graph starting from the entrypoints. +type TransitionGraph struct { + grammar *Grammar + rulename string + options *tgoptions + + Entrypoints []*Node + Endpoints []*Node +} + +// Node of a TransitionGraph. +// Unically identifiate the node of the transition graph along +// with its underlying element and its next nodes. +type Node struct { + ID string + Elem ElemItf + + Nexts []*Node +} + +// TransitionGraph builds a transition graph out of a grammar +// and the given rulename. +// TODO it is possible to build transition graph out of cylic rules iff it is not concatenated to another repetition (can't pipe O->I as there is no O). For instance, `a = "a" a` can exist. +func (g *Grammar) TransitionGraph(rulename string, opts ...TGOption) (*TransitionGraph, error) { + // Build transition graph machine + options := &tgoptions{ + deflateRules: false, + deflateNumVals: false, + deflateCharVals: false, + repetitionThreshold: 256, + maxNodes: 0, // 0 == unbounded (backward compatible) + } + for _, opt := range opts { + opt.apply(options) + } + m := &tgmachine{ + options: options, + grammar: g, + buf: map[string][2][]*Node{}, + } + + // Find the rule + rule := GetRule(rulename, g.Rulemap) + if rule == nil { + return nil, &ErrRuleNotFound{ + Rulename: rulename, + } + } + + // Ensure the rule is semantically valid and does not contain cycle. + // Semantic validity is required to ensure rule dependencies actually + // exist, while no cycle is required because the algorithm is recursive + // and may not stop else. + if options.deflateRules { + if err := SemvalABNF(g); err != nil { + return nil, err + } + } + cycle, err := g.RuleContainsCycle(rulename) + if err != nil { + return nil, err + } + if cycle { + return nil, &ErrCyclicRule{ + Rulename: rulename, + } + } + + // Build transition graph + entrypoints, endpoints, err := m.altGraph(rule.Alternation) + if err != nil { + return nil, err + } + return &TransitionGraph{ + grammar: g, + rulename: rulename, + options: options, + Entrypoints: entrypoints, + Endpoints: endpoints, + }, nil +} + +var ( + // The empty node enable to bridge the no-entrypoint case of a repetition. + // This occurs when the minimum is 0. + // It disappear as soon as the repetition is piped as an input from lower + // inputs to upper outputs. + emptyNode = (*Node)(nil) +) + +type tgoptions struct { + deflateRules bool + deflateNumVals bool + deflateCharVals bool + repetitionThreshold int + maxNodes int +} + +type TGOption interface { + apply(opts *tgoptions) +} + +type deflateRulesOption bool + +func (opt deflateRulesOption) apply(opts *tgoptions) { + opts.deflateRules = bool(opt) +} + +// WithDeflateRules when passed to true, will recursively build the transition +// graphs of the rules it uses (reuse them if possible) and concatenate the +// sub transition graphs. +// It generates a more complex but exhaustive transition graph. +func WithDeflateRules(deflate bool) TGOption { + return deflateRulesOption(deflate) +} + +var _ TGOption = deflateRulesOption(false) + +type deflateNumValsOption bool + +func (opt deflateNumValsOption) apply(opts *tgoptions) { + opts.deflateNumVals = bool(opt) +} + +// WithDeflateNumVals when passed to true, will replace a single node representing +// the ranges or series of multiple numeric values to individual nodes. +// It generates a more complex but exhaustive transition graph. +func WithDeflateNumVals(deflate bool) TGOption { + return deflateNumValsOption(deflate) +} + +var _ TGOption = deflateNumValsOption(false) + +type deflateCharVals bool + +func (opt deflateCharVals) apply(opts *tgoptions) { + opts.deflateCharVals = bool(opt) +} + +// WithDeflateCharVals when passed to true, will replace a single node representing +// the char value to an exhaustive concatenation of all characters (lower and upper +// case) and the transitions between them. +func WithDeflateCharVals(deflate bool) TGOption { + return deflateCharVals(deflate) +} + +var _ TGOption = deflateCharVals(false) + +type repetitionThreshold int + +func (opt repetitionThreshold) apply(opts *tgoptions) { + opts.repetitionThreshold = int(opt) +} + +// WithRepetitionThreshold defines the threshold to block a large repetition +// to occur, elseway it may consum all memory to build the transition graph. +// For instance, a rule defined as follows may self-DoS go-abnf. +// +// a = 9555("a") +// +// WARNING it does not avoid such memory consumption on chained repetitions. +// For instance, the two following cases could produce large transition graphs +// without control with this functional option. +// +// a = 10( 10( 10("a") ) ) +// +// a = 10 ( 10*"a" ( 10"a" 9"a" 8"a" ) / ( *10"a" *10"a" *10"a" ) ) 10"a" +// +// Defaults to 256. +func WithRepetitionThreshold(threshold int) TGOption { + return repetitionThreshold(threshold) +} + +type maxNodesOption int + +func (opt maxNodesOption) apply(opts *tgoptions) { + opts.maxNodes = int(opt) +} + +// WithMaxNodes bounds the total number of nodes the transition graph may +// allocate during construction. Unlike WithRepetitionThreshold, which only caps +// a single repetition, this bounds the *whole* graph and so also defends against +// the combinatorial blow-up of nested repetitions (e.g. 10( 10( 10("a") ) )) and +// of wide num-val ranges deflated with WithDeflateNumVals. When the bound would +// be exceeded, construction stops with *ErrMaxNodesExceeded before the nodes are +// allocated. +// +// A value <= 0 (the default) means unbounded, preserving the previous behaviour. +// Set a positive bound when building transition graphs from untrusted grammars +// to keep the work within a controlled domain. +func WithMaxNodes(max int) TGOption { + return maxNodesOption(max) +} + +// reserve accounts for n nodes about to be created, failing fast (before any +// allocation) when the WithMaxNodes budget would be exceeded. It is a no-op when +// the budget is unbounded. +func (m *tgmachine) reserve(n int) error { + if m.options.maxNodes <= 0 { + return nil // unbounded + } + if n < 0 || m.nodeCount+n < 0 || m.nodeCount+n > m.options.maxNodes { + return &ErrMaxNodesExceeded{Max: m.options.maxNodes} + } + m.nodeCount += n + return nil +} + +// reserveClones accounts for cloning the subgraph reachable from entries reps +// times. When the budget is unbounded it does no work at all (in particular it +// skips the reachability count), so the default path keeps its previous cost. +func (m *tgmachine) reserveClones(reps int, entries []*Node) error { + if m.options.maxNodes <= 0 { + return nil + } + return m.reserve(reps * countReachable(entries)) +} + +// countReachable counts the distinct non-empty nodes reachable from entries, +// i.e. how many nodes cloneTransitionGraph would clone. +func countReachable(entries []*Node) int { + seen := map[string]bool{} + stack := make([]*Node, 0, len(entries)) + for _, e := range entries { + if e != emptyNode { + stack = append(stack, e) + } + } + for len(stack) > 0 { + n := stack[len(stack)-1] + stack = stack[:len(stack)-1] + if n == emptyNode || seen[n.ID] { + continue + } + seen[n.ID] = true + for _, sc := range n.Nexts { + if sc != emptyNode && !seen[sc.ID] { + stack = append(stack, sc) + } + } + } + return len(seen) +} + +// CoverageMode selects what set of strings a TransitionGraphReader produces. +type CoverageMode int + +const ( + // CoverageAllTrails (mode A, the default) emits every entrypoint->endpoint + // trail (a walk using each directed edge at most once) and, for each trail, + // every combination of its nodes' terminal variations. It is finite and + // terminating for any graph, but on dense graphs or wide range/charval nodes + // the number of trail x variation combinations can be very large. + CoverageAllTrails CoverageMode = iota + + // CoverageCompact (mode B) emits a small set of strings that still covers + // every reachable vertex, every edge, and every terminal value (each charval + // casing, each numval range value) at least once, then stops. Its size is + // bounded by roughly (#edges + #vertices + sum of per-node variations), so it + // stays small even for grammars where CoverageAllTrails explodes. + CoverageCompact +) + +// ReaderOption configures a TransitionGraphReader. +type ReaderOption interface { + apply(*readerOptions) +} + +type readerOptions struct { + mode CoverageMode +} + +type coverageModeOption CoverageMode + +func (o coverageModeOption) apply(opts *readerOptions) { + opts.mode = CoverageMode(o) +} + +// WithCoverageMode selects the coverage strategy of the reader (CoverageAllTrails +// by default, or CoverageCompact for a bounded covering set). +func WithCoverageMode(mode CoverageMode) ReaderOption { + return coverageModeOption(mode) +} + +func (tg *TransitionGraph) Reader(opts ...ReaderOption) *TransitionGraphReader { + ro := readerOptions{mode: CoverageAllTrails} + for _, o := range opts { + o.apply(&ro) + } + return &TransitionGraphReader{tg: tg, mode: ro.mode} +} + +// TransitionGraphReader produces a finite set of strings that COVERS the +// transition graph: every reachable vertex and edge that lies on some +// entrypoint->endpoint walk appears in at least one produced string. It does so +// by enumerating every entrypoint->endpoint *trail* (a walk that uses each +// directed edge at most once) in order of increasing length, and for each trail +// every combination of its nodes' terminal variations (char-val casing, +// num-val ranges). Trails are bounded by the number of edges, so the set is +// finite and enumeration always terminates -- for any graph, cyclic or not. +type TransitionGraphReader struct { + tg *TransitionGraph + + mode CoverageMode + all map[string]*Node // every node reachable from the entrypoints + + // CoverageCompact precomputed output. + prods [][]byte + prodIdx int + builtCompact bool + + prepared bool + entries []*Node // entrypoints that can reach an endpoint via producing nodes + endSet map[string]bool // endpoint node IDs + canReach map[string]bool // node can reach an endpoint via producing nodes + + // Trail enumeration state. + length int // current trail length (number of nodes) + stack []pathFrame // current trail being walked + usedEdges map[string]int // edges currently on the trail (>0 == used) + levelHadAnyPath bool // any length-`length` trail seen this level + exhausted bool // no more trails (coverage complete) + + // Variation odometer for the current trail. + havePath bool + varIdx []int32 + varTot []int32 + + // Staged production. + staged []byte + hasStaged bool +} + +type pathFrame struct { + node *Node + cursor int + allowed []*Node // children selectable at this depth given the trail prefix +} + +func tgNodeID(n *Node) string { + if n == nil { + return "" + } + return n.ID +} + +// producing reports whether a node can emit terminal bytes. Rulename nodes +// (present only when rules are not deflated) cannot, so no trail may pass +// through them. +func producing(n *Node) bool { + if n == emptyNode { + return true + } + switch n.Elem.(type) { + case ElemCharVal, ElemNumVal: + return true + default: + return false + } +} + +func (tgr *TransitionGraphReader) prepare() { + if tgr.prepared { + return + } + tgr.prepared = true + tgr.endSet = map[string]bool{} + for _, e := range tgr.tg.Endpoints { + tgr.endSet[tgNodeID(e)] = true + } + tgr.usedEdges = map[string]int{} + + // Collect every node reachable from the entrypoints. + all := map[string]*Node{} + var collect func(n *Node) + collect = func(n *Node) { + id := tgNodeID(n) + if _, ok := all[id]; ok { + return + } + all[id] = n + if n == emptyNode { + return // the empty sentinel ((*Node)(nil)) has no successors + } + for _, s := range n.Nexts { + collect(s) + } + } + for _, e := range tgr.tg.Entrypoints { + collect(e) + } + tgr.all = all + + // canReach[n] = n can produce AND (n is an endpoint OR some producing + // successor of n can reach an endpoint). Monotonic fixpoint. + tgr.canReach = map[string]bool{} + for changed := true; changed; { + changed = false + for id, n := range all { + if tgr.canReach[id] || !producing(n) { + continue + } + ok := tgr.endSet[id] + if !ok && n != emptyNode { + for _, sc := range n.Nexts { + if producing(sc) && tgr.canReach[tgNodeID(sc)] { + ok = true + break + } + } + } + if ok { + tgr.canReach[id] = true + changed = true + } + } + } + + for _, e := range tgr.tg.Entrypoints { + if producing(e) && tgr.canReach[tgNodeID(e)] { + tgr.entries = append(tgr.entries, e) + } + } +} + +func edgeStr(from, to *Node) string { + return tgNodeID(from) + ">" + tgNodeID(to) +} + +// allowedAt returns the children selectable at depth d given the current trail +// prefix: successors that can still reach an endpoint and whose edge is not yet +// used on this trail. At depth 0 the "children" are the entrypoints. +func (tgr *TransitionGraphReader) allowedAt(d int) []*Node { + if d == 0 { + return tgr.entries + } + parent := tgr.stack[d-1].node + if parent == emptyNode { + return nil // empty sentinel has no successors + } + var out []*Node + for _, sc := range parent.Nexts { + if !tgr.canReach[tgNodeID(sc)] { + continue + } + if tgr.usedEdges[edgeStr(parent, sc)] > 0 { + continue + } + out = append(out, sc) + } + return out +} + +func (tgr *TransitionGraphReader) pushFirst(d int) bool { + allowed := tgr.allowedAt(d) + if len(allowed) == 0 { + return false + } + node := allowed[0] + if d >= 1 { + tgr.usedEdges[edgeStr(tgr.stack[d-1].node, node)]++ + } + tgr.stack = append(tgr.stack, pathFrame{node: node, cursor: 0, allowed: allowed}) + return true +} + +func (tgr *TransitionGraphReader) bump() bool { + for len(tgr.stack) > 0 { + d := len(tgr.stack) - 1 + f := &tgr.stack[d] + if d >= 1 { + tgr.usedEdges[edgeStr(tgr.stack[d-1].node, f.node)]-- + } + if f.cursor+1 < len(f.allowed) { + f.cursor++ + f.node = f.allowed[f.cursor] + if d >= 1 { + tgr.usedEdges[edgeStr(tgr.stack[d-1].node, f.node)]++ + } + return true + } + tgr.stack = tgr.stack[:d] + } + return false +} + +// nextLeaf advances the trail to the next walk of exactly tgr.length nodes. +func (tgr *TransitionGraphReader) nextLeaf() bool { + L := tgr.length + for { + switch { + case len(tgr.stack) == L: + if !tgr.bump() { + return false + } + case len(tgr.stack) < L: + if !tgr.pushFirst(len(tgr.stack)) { + if !tgr.bump() { + return false + } + } + } + if len(tgr.stack) == L { + tgr.levelHadAnyPath = true + return true + } + } +} + +func (tgr *TransitionGraphReader) resetLevel() { + tgr.stack = tgr.stack[:0] + for k := range tgr.usedEdges { + delete(tgr.usedEdges, k) + } + tgr.levelHadAnyPath = false +} + +// advancePath sets tgr.stack to the next entrypoint->endpoint trail, returning +// false once every trail has been produced (coverage complete). +func (tgr *TransitionGraphReader) advancePath() bool { + for { + if tgr.length == 0 { + tgr.length = 1 + tgr.resetLevel() + } + for tgr.nextLeaf() { + if tgr.endSet[tgNodeID(tgr.stack[len(tgr.stack)-1].node)] { + return true + } + } + if !tgr.levelHadAnyPath { + return false // no trail this long exists => done + } + tgr.length++ + tgr.resetLevel() + } +} + +func (tgr *TransitionGraphReader) ensureStaged() { + if tgr.hasStaged || tgr.exhausted { + return + } + if !tgr.havePath { + if !tgr.advancePath() { + tgr.exhausted = true + return + } + tgr.havePath = true + tgr.varTot = tgr.varTot[:0] + tgr.varIdx = tgr.varIdx[:0] + for i := range tgr.stack { + _, vt := nodeEmit(tgr.stack[i].node, 0) + if vt < 1 { + vt = 1 + } + tgr.varTot = append(tgr.varTot, vt) + tgr.varIdx = append(tgr.varIdx, 0) + } + } + tgr.staged = tgr.staged[:0] + for i := range tgr.stack { + b, _ := nodeEmit(tgr.stack[i].node, tgr.varIdx[i]) + tgr.staged = append(tgr.staged, b...) + } + tgr.hasStaged = true +} + +func (tgr *TransitionGraphReader) consumeStaged() { + tgr.hasStaged = false + i := len(tgr.varIdx) - 1 + for i >= 0 { + tgr.varIdx[i]++ + if tgr.varIdx[i] < tgr.varTot[i] { + break + } + tgr.varIdx[i] = 0 + i-- + } + if i < 0 { + tgr.havePath = false // variations exhausted -> move to next trail + } +} + +// Next reports whether another covering string is available. +func (tgr *TransitionGraphReader) Next() bool { + tgr.prepare() + if tgr.mode == CoverageCompact { + tgr.buildCompact() + return tgr.prodIdx < len(tgr.prods) + } + tgr.ensureStaged() + return tgr.hasStaged +} + +// Scan returns the next covering string, or nil once coverage is complete. +func (tgr *TransitionGraphReader) Scan() []byte { + tgr.prepare() + if tgr.mode == CoverageCompact { + tgr.buildCompact() + if tgr.prodIdx >= len(tgr.prods) { + return nil + } + out := tgr.prods[tgr.prodIdx] + tgr.prodIdx++ + return out + } + tgr.ensureStaged() + if !tgr.hasStaged { + return nil + } + out := make([]byte, len(tgr.staged)) + copy(out, tgr.staged) + tgr.consumeStaged() + return out +} + +// buildCompact precomputes the CoverageCompact output: a bounded set of strings +// that together visit every reachable vertex, traverse every edge, and emit every +// terminal value (each charval casing, each numval range value) at least once. +// It is greedy, not minimal, but its size is bounded by roughly +// (#edges + #vertices + sum of per-node variation counts). +func (tgr *TransitionGraphReader) buildCompact() { + if tgr.builtCompact { + return + } + tgr.builtCompact = true + + viable := func(n *Node) []*Node { + if n == emptyNode { + return nil + } + var out []*Node + for _, sc := range n.Nexts { + if producing(sc) && tgr.canReach[tgNodeID(sc)] { + out = append(out, sc) + } + } + return out + } + + // Forward BFS from entrypoints: coverable vertices, edges, and entry->node + // shortest-path predecessors. + predEntry := map[string]*Node{} + nodeByID := map[string]*Node{} + discOrder := map[string]int{} // stable BFS discovery index per node + var disc []*Node // nodes in discovery order + type edge struct{ u, w *Node } + var allEdges []edge + seenEdge := map[string]bool{} + var queue []*Node + see := func(n *Node) { + id := tgNodeID(n) + if _, ok := predEntry[id]; ok { + return + } + predEntry[id] = n + nodeByID[id] = n + discOrder[id] = len(disc) + disc = append(disc, n) + queue = append(queue, n) + } + for _, e := range tgr.entries { + see(e) + } + for len(queue) > 0 { + n := queue[0] + queue = queue[1:] + for _, sc := range viable(n) { + sid := tgNodeID(sc) + ek := tgNodeID(n) + ">" + sid + if !seenEdge[ek] { + seenEdge[ek] = true + allEdges = append(allEdges, edge{n, sc}) + } + if _, ok := predEntry[sid]; !ok { + predEntry[sid] = n + nodeByID[sid] = sc + discOrder[sid] = len(disc) + disc = append(disc, sc) + queue = append(queue, sc) + } + } + } + + // Backward BFS from endpoints: node->endpoint shortest-path successors. + // Node IDs are random UUIDs, so we order everything by stable BFS discovery + // index instead -- otherwise the chosen shortest paths (and thus the covering + // set) would vary between builds. + revAdj := map[string][]*Node{} + for _, n := range disc { + for _, sc := range viable(n) { + sid := tgNodeID(sc) + revAdj[sid] = append(revAdj[sid], n) + } + } + succEnd := map[string]*Node{} + var q2 []*Node + for _, n := range disc { + if tgr.endSet[tgNodeID(n)] { + succEnd[tgNodeID(n)] = n + q2 = append(q2, n) + } + } + for len(q2) > 0 { + n := q2[0] + q2 = q2[1:] + for _, p := range revAdj[tgNodeID(n)] { + pid := tgNodeID(p) + if _, ok := succEnd[pid]; !ok { + succEnd[pid] = n + q2 = append(q2, p) + } + } + } + + entryPath := func(u *Node) []*Node { + var rev []*Node + cur := u + for { + rev = append(rev, cur) + id := tgNodeID(cur) + p := predEntry[id] + if tgNodeID(p) == id { + break + } + cur = p + } + for i, j := 0, len(rev)-1; i < j; i, j = i+1, j-1 { + rev[i], rev[j] = rev[j], rev[i] + } + return rev + } + endPath := func(w *Node) []*Node { + var out []*Node + cur := w + for { + out = append(out, cur) + id := tgNodeID(cur) + sc := succEnd[id] + if tgNodeID(sc) == id { + break + } + cur = sc + } + return out + } + + coveredV := map[string]bool{} + coveredE := map[string]bool{} + // nextVal[id] is the smallest variation index of a node not yet emitted. + // Values are always taken in increasing order, so a forward cursor per node + // makes value coverage O(total values) instead of O(sum of width^2). + nextVal := map[string]int32{} + + markWalk := func(w []*Node) { + for i, n := range w { + coveredV[tgNodeID(n)] = true + if i+1 < len(w) { + coveredE[tgNodeID(n)+">"+tgNodeID(w[i+1])] = true + } + } + } + emit := func(w []*Node, force bool) bool { + va := make([]int32, len(w)) + claimed := map[string]int32{} // fresh values taken for each node in THIS emit + newCount := 0 + for p, n := range w { + id := tgNodeID(n) + _, vt := nodeEmit(n, 0) + if vt < 1 { + vt = 1 + } + claim := nextVal[id] + claimed[id] + if claim < vt { + va[p] = claim + claimed[id]++ + newCount++ + } else { + va[p] = 0 // all of this node's values already covered; reuse 0 + } + } + if !force && newCount == 0 { + return false + } + for id, c := range claimed { + nextVal[id] += c + } + var bs []byte + for p, n := range w { + b, _ := nodeEmit(n, va[p]) + bs = append(bs, b...) + } + tgr.prods = append(tgr.prods, bs) + return true + } + + var walks [][]*Node + + sort.SliceStable(allEdges, func(i, j int) bool { + ui, uj := discOrder[tgNodeID(allEdges[i].u)], discOrder[tgNodeID(allEdges[j].u)] + if ui != uj { + return ui < uj + } + return discOrder[tgNodeID(allEdges[i].w)] < discOrder[tgNodeID(allEdges[j].w)] + }) + for _, e := range allEdges { + ek := tgNodeID(e.u) + ">" + tgNodeID(e.w) + if coveredE[ek] { + continue + } + if _, ok := succEnd[tgNodeID(e.w)]; !ok { + continue + } + w := append(entryPath(e.u), endPath(e.w)...) + walks = append(walks, w) + emit(w, true) + markWalk(w) + } + + for _, v := range disc { + id := tgNodeID(v) + if coveredV[id] { + continue + } + if _, ok := succEnd[id]; !ok { + continue + } + var w []*Node + if tgr.endSet[id] { + w = entryPath(v) + } else { + ep := endPath(v) + w = append(entryPath(v), ep[1:]...) + } + walks = append(walks, w) + emit(w, true) + markWalk(w) + } + + for _, w := range walks { + for emit(w, false) { + } + } +} + +// nodeEmit returns the bytes a node emits for variation index tpos, and the +// node's total number of variations. For a non-sensitive char-val the variations +// are the 2^k casings of its k cased letters; for a num-val range, one byte per +// value in the range. +func nodeEmit(node *Node, tpos int32) (prod []byte, vtotal int32) { + if node == emptyNode { + return nil, 1 + } + switch v := node.Elem.(type) { + case ElemCharVal: + if v.Sensitive { + for _, val := range v.Values { + prod = append(prod, string(val)...) + } + return prod, 1 + } + var numVar int + for _, r := range v.Values { + if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') { + numVar++ + } + } + // 2^numVar casings; cap to avoid int32 overflow on pathologically long + // case-insensitive literals (2^31 casings is unenumerable anyway). Letters + // beyond the cap are emitted lower-case only. + // XXX This is an arbitrary bound + if numVar > 30 { + numVar = 30 + } + vtotal = int32(1) << uint(numVar) + bit := uint(0) + for _, r := range v.Values { + isLower := r >= 'a' && r <= 'z' + isUpper := r >= 'A' && r <= 'Z' + if !isLower && !isUpper { + prod = append(prod, string(r)...) + continue + } + lower := r + if isUpper { + lower = lower - 'A' + 'a' + } + upper := r + if isLower { + upper = upper - 'a' + 'A' + } + if (tpos>>bit)&1 == 1 { + prod = append(prod, string(upper)...) + } else { + prod = append(prod, string(lower)...) + } + bit++ + } + return prod, vtotal + + case ElemNumVal: + switch v.Status { + case StatSeries: + for _, elem := range v.Elems { + r := numvalToRune(elem, v.Base) + prod = append(prod, []byte(string(r))...) + } + return prod, 1 + case StatRange: + min, max := numvalToInt32(v.Elems[0], v.Base), numvalToInt32(v.Elems[1], v.Base) + // The graph operates over runes; clamp to the Unicode range so an + // out-of-range bound cannot overflow the int32 count below. + if max > utf8.MaxRune { + max = utf8.MaxRune + } + if min > utf8.MaxRune || min > max { + return nil, 1 // no representable character in this range + } + r := rune(min + tpos) + return []byte(string(r)), max - min + 1 + } + } + return nil, 1 +} + +type tgmachine struct { + options *tgoptions + grammar *Grammar + buf map[string][2][]*Node + + nodeCount int // nodes reserved so far, checked against options.maxNodes +} + +func (m *tgmachine) altGraph(alt Alternation) (entrypoints []*Node, endpoints []*Node, err error) { + for _, concat := range alt.Concatenations { + ci, co, err := m.concatGraph(concat) + if err != nil { + return nil, nil, err + } + + // Group concatenation inputs, but avoid duplicating the emptyNode + entryHasEmpty := slices.Contains(entrypoints, emptyNode) + for _, vci := range ci { + if vci == emptyNode && entryHasEmpty { + continue + } + entrypoints = append(entrypoints, vci) + } + + // Group concatenation outputs, but avoid duplicating the emptyNode + endHasEmpty := slices.Contains(endpoints, emptyNode) + for _, vco := range co { + if vco == emptyNode && endHasEmpty { + continue + } + endpoints = append(endpoints, vco) + } + } + return +} + +func (m *tgmachine) concatGraph(concat Concatenation) (entrypoints []*Node, endpoints []*Node, err error) { + ci, co, err := m.repGraph(concat.Repetitions[0]) + if err != nil { + return nil, nil, err + } + for i := 1; i < len(concat.Repetitions); i++ { + repi, repo, err := m.repGraph(concat.Repetitions[i]) + if err != nil { + return nil, nil, err + } + ci, co = concatTransitionGraphs(ci, co, repi, repo) + } + return ci, co, nil +} + +func concatTransitionGraphs(prevI, prevO, currI, currO []*Node) (entrypoints []*Node, endpoints []*Node) { + // If prevI contains the emptyNode it means currI (possibly with + // the emptyNode) is also part of the entrypoints (without prevI's + // emptyNode). + if slices.Contains(prevI, emptyNode) { + for _, vprevI := range prevI { + if vprevI == emptyNode { + continue + } + entrypoints = append(entrypoints, vprevI) + } + entrypoints = append(entrypoints, currI...) + } else { + entrypoints = prevI + } + + // plug previous outputs (prevO) to current inputs (currI). + for _, vprevO := range prevO { + if vprevO == emptyNode { + continue + } + for _, vrepi := range currI { + if vrepi == emptyNode { + continue + } + vprevO.Nexts = append(vprevO.Nexts, vrepi) + } + } + + // new concatenation output is the previous ones if current + // inputs contains the emptyNode (repetition is skippable), + // and the current outputs ones. + if slices.Contains(currI, emptyNode) { + endpoints = append(endpoints, prevO...) + } + for _, vrepo := range currO { + if vrepo == emptyNode { + continue + } + endpoints = append(endpoints, vrepo) + } + + return +} + +func (m *tgmachine) repGraph(rep Repetition) (entrypoints []*Node, endpoints []*Node, err error) { + // element entry-/end-points (resp. I/O) + elemi, elemo, err := m.elemGraph(rep.Element) + if err != nil { + return nil, nil, err + } + + if rep.Min == 0 { + entrypoints = appendNodes(entrypoints, emptyNode) + endpoints = appendNodes(endpoints, emptyNode) + + switch rep.Max { + case 1: + // min = 0 & max = 1: optional v + entrypoints = appendNodes(entrypoints, elemi...) + endpoints = appendNodes(endpoints, elemo...) + case inf: + // min = 0 & max = inf: optional infinity + elemiNoEmpty := []*Node{} + for _, velemi := range elemi { + if velemi == emptyNode { + continue + } + elemiNoEmpty = appendNodes(elemiNoEmpty, velemi) + } + for _, velemo := range elemo { + if velemo == emptyNode { + continue + } + velemo.Nexts = appendNodes(velemo.Nexts, elemiNoEmpty...) + } + entrypoints = appendNodes(entrypoints, elemi...) + endpoints = appendNodes(endpoints, elemo...) + + default: + // min = 0 & 1 < max < inf: optional to n + if rep.Max > m.options.repetitionThreshold { + return nil, nil, ErrRepetitionThreshold + } + if err := m.reserveClones(rep.Max, elemi); err != nil { + return nil, nil, err + } + tgs, chi, _ := chainTransitionGraph(elemi, elemo, rep.Max) + entrypoints = appendNodes(entrypoints, chi...) + for _, tg := range tgs { + endpoints = appendNodes(endpoints, tg.Endpoints...) + } + } + } else { + switch rep.Max { + case 1: + // min == 1 & max == 1: mandatory + if rep.Min != 1 { + return nil, nil, ErrRepetitionMinNotOne + } + entrypoints = appendNodes(entrypoints, elemi...) + endpoints = appendNodes(endpoints, elemo...) + + case inf: + // min >= 1 && max = inf: n to infinity + if rep.Min > m.options.repetitionThreshold { + return nil, nil, ErrRepetitionThreshold + } + if err := m.reserveClones(rep.Min, elemi); err != nil { + return nil, nil, err + } + tgs, chi, cho := chainTransitionGraph(elemi, elemo, rep.Min) + entrypoints = appendNodes(entrypoints, chi...) + endpoints = appendNodes(endpoints, cho...) + + last := tgs[len(tgs)-1] + for _, vo := range last.Endpoints { + if vo == emptyNode { + continue + } + for _, etp := range last.Entrypoints { + // Make sure to not append the empty node as a next node, this is not possible + if etp == emptyNode { + continue + } + vo.Nexts = appendNodes(vo.Nexts, etp) + } + } + + default: + // min >= 1 && max < inf: n to m + if rep.Min > rep.Max { + return nil, nil, ErrRepetitionMinGtMax + } + if rep.Max > m.options.repetitionThreshold { + return nil, nil, ErrRepetitionThreshold + } + + // Build the flat chain (not endpoints, except last) + firstI, lastO := elemi, elemo + if rep.Min > 1 { + if err := m.reserveClones(rep.Min, elemi); err != nil { + return nil, nil, err + } + _, firstI, lastO = chainTransitionGraph(elemi, elemo, rep.Min) + } + entrypoints = appendNodes(entrypoints, firstI...) + endpoints = appendNodes(endpoints, lastO...) + + // Build remaining endpoints + remaining := rep.Max - rep.Min + if remaining > 0 { + if err := m.reserveClones(rep.Max-rep.Min, elemi); err != nil { + return nil, nil, err + } + tgs, chi, _ := chainTransitionGraph(elemi, elemo, rep.Max-rep.Min) + for _, tg := range tgs { + endpoints = appendNodes(endpoints, tg.Endpoints...) + } + for _, vlasto := range lastO { + if vlasto == emptyNode { + continue + } + for _, nchi := range chi { + if nchi == emptyNode { + continue + } + vlasto.Nexts = appendNodes(vlasto.Nexts, nchi) + } + } + } + } + } + + return +} + +func chainTransitionGraph(i, o []*Node, n int) (tgs []TransitionGraph, entrypoints []*Node, endpoints []*Node) { + // Make sure to not chain... nothing ¯\_(ツ)_/¯ + if n == 0 { + return + } + tgs = make([]TransitionGraph, n) + + // Init chaining + entrypoints, endpoints = cloneTransitionGraph(i, o) + tgs[0] = TransitionGraph{ + Entrypoints: entrypoints, + Endpoints: endpoints, + } + + // Gotta chain them all + for j := 1; j < n; j++ { + nextI, nextO := cloneTransitionGraph(i, o) + tgs[j] = TransitionGraph{ + Entrypoints: nextI, + Endpoints: nextO, + } + entrypoints, endpoints = concatTransitionGraphs(entrypoints, endpoints, nextI, nextO) + } + return +} + +func cloneTransitionGraph(i, o []*Node) (entrypoints []*Node, endpoints []*Node) { + // Traverse graph to map all cloned originToNewNodes by their origin IDs + originToNewNodes := map[string]cnode{} + for _, vi := range i { + if vi == emptyNode { + continue + } + cloneTG(originToNewNodes, vi) + } + + // Rebuild links + for _, cnode := range originToNewNodes { + nnxts := make([]*Node, 0, len(cnode.origin.Nexts)) + for _, onext := range cnode.origin.Nexts { + nnxts = append(nnxts, originToNewNodes[onext.ID].cloned) + } + cnode.cloned.Nexts = nnxts + } + + // Get cloned I/O + entrypoints = make([]*Node, 0, len(i)) + for _, vi := range i { + if vi == emptyNode { + entrypoints = append(entrypoints, emptyNode) + continue + } + entrypoints = append(entrypoints, originToNewNodes[vi.ID].cloned) + } + endpoints = make([]*Node, 0, len(o)) + for _, vo := range o { + if vo == emptyNode { + endpoints = append(endpoints, emptyNode) + continue + } + endpoints = append(endpoints, originToNewNodes[vo.ID].cloned) + } + return +} + +type cnode struct { + origin, cloned *Node +} + +func cloneTG(originToNewNodes map[string]cnode, origin *Node) { + id := newID() + + originToNewNodes[origin.ID] = cnode{ + origin: origin, + cloned: &Node{ + ID: id, + Elem: origin.Elem, + Nexts: nil, // will map it later, requires all origins to be cloned first + }, + } + + // recurse iff not known yet + for _, n := range origin.Nexts { + if _, ok := originToNewNodes[n.ID]; ok { + continue + } + cloneTG(originToNewNodes, n) + } +} + +func appendNodes(existing []*Node, news ...*Node) []*Node { + for _, n := range news { + if slices.Contains(existing, n) { + continue + } + existing = append(existing, n) + } + return existing +} + +func (m *tgmachine) elemGraph(elem ElemItf) (entrypoints []*Node, endpoints []*Node, err error) { + switch v := elem.(type) { + // Final elements => create the node, no need to pipe I/O + case ElemCharVal: + if len(v.Values) == 0 { + entrypoints = append(entrypoints, emptyNode) + endpoints = append(endpoints, emptyNode) + return + } + + if !m.options.deflateCharVals { + if err := m.reserve(1); err != nil { + return nil, nil, err + } + n := newNode(v) + entrypoints = append(entrypoints, n) + endpoints = append(endpoints, n) + return + } + + if err := m.reserve(2 * len(v.Values)); err != nil { + return nil, nil, err + } + + var prevs []*Node = nil + var curr []*Node = nil + for _, r := range v.Values { + isLower := r >= 'a' && r <= 'z' + isUpper := r >= 'A' && r <= 'Z' + requireBoth := (isLower || isUpper) && !v.Sensitive + + if !requireBoth { + n := newNode(ElemCharVal{ + Sensitive: true, + Values: []rune{r}, + }) + curr = []*Node{n} + + if prevs == nil { + entrypoints = append(entrypoints, n) + } + for _, v := range prevs { + v.Nexts = append(v.Nexts, curr...) + } + prevs = []*Node{n} + } else { + nlv := r + if isUpper { + nlv = runeMin(nlv) + } + nuv := r + if isLower { + nuv = runeMax(nuv) + } + + nl := newNode(ElemCharVal{ + Sensitive: true, + Values: []rune{nlv}, + }) + nu := newNode(ElemCharVal{ + Sensitive: true, + Values: []rune{nuv}, + }) + curr = []*Node{nl, nu} + + if prevs == nil { + entrypoints = append(entrypoints, nl, nu) + } + for _, v := range prevs { + v.Nexts = append(v.Nexts, curr...) + } + prevs = []*Node{nl, nu} + } + } + endpoints = append(endpoints, curr...) + return + + case ElemNumVal: + if !m.options.deflateNumVals { + if err := m.reserve(1); err != nil { + return nil, nil, err + } + n := newNode(v) + entrypoints = append(entrypoints, n) + endpoints = append(endpoints, n) + return + } + switch v.Status { + case StatRange: + min, max := numvalToInt32(v.Elems[0], v.Base), numvalToInt32(v.Elems[1], v.Base) + // One node is enumerated per rune; clamp to the Unicode range so an + // out-of-range bound cannot overflow the count (int32) and bypass the + // node budget, which would turn this loop into a DoS. A range wholly + // above U+10FFFF contributes no node (it matches nothing). + if max > utf8.MaxRune { + max = utf8.MaxRune + } + if min > utf8.MaxRune || min > max { + return + } + if err := m.reserve(int(max - min + 1)); err != nil { + return nil, nil, err + } + for i := min; i <= max; i++ { + n := newNode(ElemNumVal{ + Base: v.Base, + Status: StatSeries, + Elems: []string{int32ToNumval(i, v.Base)}, + }) + entrypoints = append(entrypoints, n) + endpoints = append(endpoints, n) + } + + case StatSeries: + if err := m.reserve(len(v.Elems)); err != nil { + return nil, nil, err + } + for _, s := range v.Elems { + n := newNode(ElemNumVal{ + Base: v.Base, + Status: StatSeries, + Elems: []string{s}, + }) + entrypoints = append(entrypoints, n) + endpoints = append(endpoints, n) + } + } + return + + // Recursive elements => pipe I/O + case ElemRulename: + if m.options.deflateRules { + name := strings.ToLower(v.Name) + if n, ok := m.buf[name]; ok { + if err := m.reserveClones(1, n[0]); err != nil { + return nil, nil, err + } + entrypoints, endpoints := cloneTransitionGraph(n[0], n[1]) + return entrypoints, endpoints, nil + } + rule := GetRule(v.Name, m.grammar.Rulemap) + if rule == nil { + return nil, nil, &ErrRuleNotFound{ + Rulename: v.Name, + } + } + i, o, err := m.altGraph(rule.Alternation) + if err != nil { + return nil, nil, err + } + if err := m.reserveClones(1, i); err != nil { + return nil, nil, err + } + ni, no := cloneTransitionGraph(i, o) + m.buf[name] = [2][]*Node{ni, no} + return i, o, nil + } + if err := m.reserve(1); err != nil { + return nil, nil, err + } + n := newNode(v) + entrypoints = append(entrypoints, n) + endpoints = append(endpoints, n) + return + + case ElemOption: + elemi, elemo, err := m.altGraph(v.Alternation) + if err != nil { + return nil, nil, err + } + elemi = appendNodes(elemi, emptyNode) + elemo = appendNodes(elemo, emptyNode) + return elemi, elemo, nil + + case ElemGroup: + return m.altGraph(v.Alternation) + case ElemProseVal: + return nil, nil, ErrProseValInTransitionGraph + } + + panic("unsupported element") +} + +// newID returns a random identifier for a graph node, the way containerd and +// containers/storage build container IDs: random bytes from crypto/rand, hex +// encoded. Node IDs only have to be unique within one graph, so 16 bytes are +// plenty. crypto/rand.Read never returns an error since Go 1.24. +func newID() string { + b := make([]byte, 16) + rand.Read(b) + return hex.EncodeToString(b) +} + +func newNode(elem ElemItf) *Node { + id := newID() + return &Node{ + ID: id, + Elem: elem, + // Final node, no need to pipe I/O + } +} + +// ToMermaid produces a mermaid-encoded representation of the transition graph. +func (tg *TransitionGraph) ToMermaid() string { + // Map all nodes + mp := map[string]*Node{} + for _, node := range tg.Entrypoints { + if node == emptyNode { + continue + } + mapNodes(mp, node) + } + + // Write them all + str := "flowchart LR\n classDef entrypoint fill:#479abf\n\n" + for _, node := range mp { + // Write down the node + nodeStr := " " + node.ID + elemStr := elemToString(node.Elem) + if slices.Contains(tg.Endpoints, node) { + nodeStr += "(((\"" + elemStr + "\")))" + } else { + nodeStr += "[\"" + elemStr + "\"]" + } + str += nodeStr + if slices.Contains(tg.Entrypoints, node) { + str += ":::entrypoint" + } + str += "\n" + + // Write down its links + for _, next := range node.Nexts { + str += fmt.Sprintf(" %s --> %s\n", node.ID, next.ID) + } + str += "\n" + } + + // Don't forget the empty node + if slices.Contains(tg.Entrypoints, emptyNode) { + str += " emptyNode(((\u2205))):::entrypoint\n" + } + + return str +} + +func mapNodes(mp map[string]*Node, node *Node) { + if _, ok := mp[node.ID]; ok { + return + } + mp[node.ID] = node + for _, next := range node.Nexts { + mapNodes(mp, next) + } +} + +func elemToString(elem ElemItf) string { + switch v := elem.(type) { + case ElemRulename: + return v.Name + case ElemCharVal: + str := "" + for _, b := range v.Values { + if b == '`' { + str += "backquote" + } else if b == '"' { + str += "dquote" + } else if 33 <= b && b <= 127 { + str += string(b) + } + } + return str + case ElemNumVal: + if v.Status == StatRange { + return fmt.Sprintf("0%s%s-%s", v.Base, v.Elems[0], v.Elems[1]) + } + return fmt.Sprintf("0%s%s", v.Base, strings.Join(v.Elems, ".")) + } + panic("not implemented yet") +} diff --git a/vendor/github.com/pandatix/go-abnf/utils.go b/vendor/github.com/pandatix/go-abnf/utils.go new file mode 100644 index 00000000000..dcc9e6d5302 --- /dev/null +++ b/vendor/github.com/pandatix/go-abnf/utils.go @@ -0,0 +1,152 @@ +package goabnf + +import ( + "math" + "strconv" + "strings" +) + +// int32ToNumval converts an integer value into its numeric-string representation +// in the given base. It is the inverse of numvalToInt32 and produces the form +// stored in an ElemNumVal's Elems (e.g. 97 in base "x" -> "61"). +func int32ToNumval(v int32, base string) string { + switch base { + case "B", "b": + return strconv.FormatInt(int64(v), 2) + case "D", "d": + return strconv.FormatInt(int64(v), 10) + case "X", "x": + return strings.ToUpper(strconv.FormatInt(int64(v), 16)) + } + return strconv.FormatInt(int64(v), 10) +} + +// baseRadix maps an ABNF num-val base marker (bin/dec/hex) to its integer radix. +func baseRadix(base string) int { + switch base { + case "B", "b": + return 2 + case "D", "d": + return 10 + case "X", "x": + return 16 + } + return 10 +} + +// numvalToUint64 parses a num-val literal in the given base into its numeric +// value. ok is false only when the literal does not fit in 64 bits (or is +// malformed), which lets callers degrade gracefully instead of overflowing +// silently. This is the single overflow-safe entry point every other conversion +// builds on. +// +// A num-val is a plain non-negative integer, not intrinsically a Unicode scalar: +// values beyond U+10FFFF are legitimate (e.g. for non-textual / on-wire +// grammars). Unicode is handled as the common case at match time; this layer is +// no longer Unicode-bounded. +func numvalToUint64(str, base string) (uint64, bool) { + v, err := strconv.ParseUint(str, baseRadix(base), 64) + if err != nil { + return 0, false + } + return v, true +} + +// numvalToInt32 converts a num-val into an int32 codepoint. It never panics: a +// value that does not fit a positive int32 is saturated to math.MaxInt32, which +// is above the maximum Unicode scalar (U+10FFFF) and therefore matches no decoded +// rune. A range bound that merely straddles the Unicode ceiling still covers its +// representable (Unicode) subset, because the saturated upper bound stays above +// every rune. +func numvalToInt32(str, base string) int32 { + v, ok := numvalToUint64(str, base) + if !ok || v > math.MaxInt32 { + return math.MaxInt32 + } + return int32(v) +} + +// numvalToRune converts a num-val into the corresponding rune. It never panics; +// a value beyond the Unicode range yields a rune that no UTF-8 input can produce, +// so callers matching against decoded runes simply find no match. Callers that +// require text validity (e.g. a num-val series, which must denote real +// characters) should additionally check utf8.ValidRune. +func numvalToRune(str, base string) rune { + return rune(numvalToInt32(str, base)) +} + +// checkBounds enforces the Unicode-range invariant (value <= U+10FFFF). It is +// the strict-validation path used by SemvalABNF: with validation enabled (the +// default, and the common textual case) a num-val outside the Unicode range is +// rejected. The conversion helpers above are deliberately broader -- a num-val is +// a plain integer and larger values are legitimate for non-textual grammars -- so +// without validation such values are accepted and handled (matched as no rune). +func checkBounds(str, base string) error { + str = strings.TrimLeft(str, "0") + switch base { + // Whatever the base, the higher value we arbitrary decide to support is + // the maximal Unicode character i.e. U+10FFFF. + // + // Every value (represented as integer) is then in interval [0;1_114_111]. + // For each base, we can compute the number of characters of an acceptable + // num-val as ceil(log(base,1_114_111)). + // + // We also have to ensure that it does not go over the boundaries even if + // of an acceptable length, e.g., 0xFFFFFF is not. + + case "B", "b": + // 21 = ceil(log(2, max_unicode)) + // U+10FFFF = Ob100001111111111111111 + if len(str) > 21 || + (len(str) == 21 && (str[1] != '0' || str[2] != '0' || str[3] != '0' || str[4] != '0')) { + return &ErrTooLargeNumeral{ + Base: base, + Value: str, + } + } + + case "D", "d": + // 7 = ceil(log(10, max_unicode)) + // U+10FFFF = 0d1114111 + if len(str) > 7 || + (len(str) == 7 && (str[0] > '1' || (str[0] == '1' && (str[1] > '1' || (str[1] == '1' && (str[2] > '1' || (str[2] == '1' && (str[3] > '4' || (str[3] == '4' && (str[4] > '1' || (str[4] == '1' && (str[5] > '1' || (str[5] == '1' && str[6] > '1'))))))))))))) { + return &ErrTooLargeNumeral{ + Base: base, + Value: str, + } + } + + case "X", "x": + // 6 = ceil(log(16, max_unicode)) + // U+10FFFF = 0x10FFFF + if len(str) > 6 || + (len(str) == 6 && (str[0] > '1' || (str[0] == '1' && (str[1] > '0')))) { + return &ErrTooLargeNumeral{ + Base: base, + Value: str, + } + } + } + + return nil +} + +// GetRule returns the rule by the given rulename, whether +// it is a core rule or present in the grammar, or nil if not found. +// It validates the RFC 5234 Section 2.1 "rule names are case insensitive". +func GetRule(rulename string, rulemap map[string]*Rule) *Rule { + // First look in the newly defined rules, as we could override definitions + if rule := getRuleIn(rulename, rulemap); rule != nil { + return rule + } + return getRuleIn(rulename, coreRules) +} + +func getRuleIn(rulename string, rulemap map[string]*Rule) *Rule { + for _, rule := range rulemap { + if strings.EqualFold(rulename, rule.Name) { + return rule + } + } + return nil +} diff --git a/vendor/github.com/prometheus/common/expfmt/decode.go b/vendor/github.com/prometheus/common/expfmt/decode.go index 0339d8c1d0f..dbff6cb38ec 100644 --- a/vendor/github.com/prometheus/common/expfmt/decode.go +++ b/vendor/github.com/prometheus/common/expfmt/decode.go @@ -65,6 +65,16 @@ func ResponseFormat(h http.Header) Format { return FmtUnknown } return FmtText + + case OpenMetricsType: + switch params["version"] { + case "", OpenMetricsVersion_0_0_1: + return FmtOpenMetrics_0_0_1 + case OpenMetricsVersion_1_0_0: + return FmtOpenMetrics_1_0_0 + default: + return FmtUnknown + } } return FmtUnknown @@ -73,14 +83,12 @@ func ResponseFormat(h http.Header) Format { // NewDecoder returns a new decoder based on the given input format. Metric // names are validated based on the provided Format -- if the format requires // escaping, traditional Prometheus validity checking is used. Otherwise, names -// are checked for UTF-8 validity. Supported formats include delimited protobuf -// and Prometheus text format. For historical reasons, this decoder falls back -// to classic text decoding for other legacy formats, but returns an error for -// unsupported formats such as OpenMetrics 2.0. This decoder does not fully -// support OpenMetrics although it may often succeed for OpenMetrics 1.0 due to -// the similarities between the formats. This decoder may not support the latest -// features of Prometheus text format and is not intended for high-performance -// applications. +// are checked for UTF-8 validity. Supported formats include delimited protobuf, +// Prometheus text format, and OpenMetrics 0.0.1 / 1.0.0. For historical +// reasons, this decoder falls back to classic text decoding for other legacy +// formats, but returns an error for unsupported formats such as OpenMetrics 2.0. +// This decoder may not support the latest features of Prometheus text format +// and is not intended for high-performance applications. // See: https://github.com/prometheus/common/issues/812 func NewDecoder(r io.Reader, format Format) Decoder { scheme := model.LegacyValidation @@ -94,7 +102,13 @@ func NewDecoder(r io.Reader, format Format) Decoder { return &errDecoder{err: fmt.Errorf("format %s not supported for decoding", format)} case TypeOpenMetrics: _, params, err := mime.ParseMediaType(string(format)) - if err == nil && params["version"] == OpenMetricsVersion_2_0_0 { + if err != nil { + return &errDecoder{err: fmt.Errorf("format %s not supported for decoding", format)} + } + switch params["version"] { + case "", OpenMetricsVersion_0_0_1, OpenMetricsVersion_1_0_0: + return &openMetricsDecoder{p: newOpenMetricsParser(r, scheme)} + default: return &errDecoder{err: fmt.Errorf("format %s not supported for decoding", format)} } } @@ -146,6 +160,24 @@ func (d *errDecoder) Decode(*dto.MetricFamily) error { return d.err } +// openMetricsDecoder implements the Decoder interface for the OpenMetrics 1.0 protocol. +type openMetricsDecoder struct { + p *openMetricsParser + err error +} + +// Decode implements the Decoder interface. +func (d *openMetricsDecoder) Decode(v *dto.MetricFamily) error { + if d.err != nil { + return d.err + } + if err := d.p.next(v); err != nil { + d.err = err + return err + } + return nil +} + // textDecoder implements the Decoder interface for the text protocol. type textDecoder struct { r io.Reader diff --git a/vendor/github.com/prometheus/common/expfmt/encode.go b/vendor/github.com/prometheus/common/expfmt/encode.go index 651e69a2c89..c6ef91eb0c2 100644 --- a/vendor/github.com/prometheus/common/expfmt/encode.go +++ b/vendor/github.com/prometheus/common/expfmt/encode.go @@ -37,6 +37,7 @@ func init() { FmtProtoText, FmtProtoCompact, FmtOpenMetrics_1_0_0, + FmtOpenMetrics_2_0_0_rc, FmtOpenMetrics_2_0_0, FmtOpenMetrics_0_0_1, } { @@ -130,21 +131,14 @@ func NegotiateIncludingOpenMetrics(h http.Header) Format { // If no accepted format matches the Accept header, it falls back to the text // format if present in the accepted list, or the first accepted format (or FmtText // if accepted is empty). +// The escaping scheme is taken from the matching Accept clause. Without one, +// OpenMetrics 2.0 gets allow-utf-8 and all other formats model.NameEscapingScheme. func NegotiateAccept(h http.Header, accepted ...Format) Format { escapingScheme := Format("; escaping=" + model.NameEscapingScheme.String()) for _, ac := range goautoneg.ParseAccept(h.Get(hdrAccept)) { - if escapeParam := ac.Params[model.EscapingKey]; escapeParam != "" { - switch Format(escapeParam) { - case model.AllowUTF8, model.EscapeUnderscores, model.EscapeDots, model.EscapeValues: - escapingScheme = Format("; escaping=" + escapeParam) - default: - // If the escaping parameter is unknown, ignore it. - } - } - for _, f := range accepted { if matchFormat(ac, f) { - return f + escapingScheme + return f + escapingFor(ac, f) } } } @@ -159,6 +153,20 @@ func NegotiateAccept(h http.Header, accepted ...Format) Format { return FmtText + escapingScheme } +// escapingFor returns the escaping term for the Accept clause that matched f. +func escapingFor(ac goautoneg.Accept, f Format) Format { + switch p := ac.Params[model.EscapingKey]; p { + case model.AllowUTF8, model.EscapeUnderscores, model.EscapeDots, model.EscapeValues: + // An explicit request always wins, also for OpenMetrics 2.0. + return Format("; escaping=" + p) + } + if f.FormatType() == TypeOpenMetrics && isOpenMetrics20Version(f.Version()) { + // OpenMetrics 2.0 is UTF-8 native, so asking for it implies UTF-8 name support. + return Format("; escaping=" + model.AllowUTF8) + } + return Format("; escaping=" + model.NameEscapingScheme.String()) +} + // matchFormat checks if a parsed accept clause matches a given Format. func matchFormat(ac goautoneg.Accept, f Format) bool { target, ok := formatToAccept[f] @@ -255,7 +263,7 @@ func NewEncoder(w io.Writer, format Format, options ...EncoderOption) Encoder { } case TypeOpenMetrics: _, params, err := mime.ParseMediaType(string(format)) - if err == nil && params["version"] == OpenMetricsVersion_2_0_0 { + if err == nil && isOpenMetrics20Version(params["version"]) { return encoderCloser{ encode: func(v *dto.MetricFamily) error { _, err := MetricFamilyToOpenMetrics20(w, model.EscapeMetricFamily(v, escapingScheme), options...) diff --git a/vendor/github.com/prometheus/common/expfmt/expfmt.go b/vendor/github.com/prometheus/common/expfmt/expfmt.go index ea9a0a2a041..e58c4b09dda 100644 --- a/vendor/github.com/prometheus/common/expfmt/expfmt.go +++ b/vendor/github.com/prometheus/common/expfmt/expfmt.go @@ -36,6 +36,8 @@ const ( //nolint:revive // Allow for underscores. OpenMetricsVersion_1_0_0 = "1.0.0" //nolint:revive // Allow for underscores. + OpenMetricsVersion_2_0_0_rc = "2.0.0-rc" + //nolint:revive // Allow for underscores. OpenMetricsVersion_2_0_0 = "2.0.0" ) @@ -57,6 +59,8 @@ const ( //nolint:revive // Allow for underscores. FmtOpenMetrics_2_0_0 Format = OpenMetricsType + `; version=` + OpenMetricsVersion_2_0_0 + `; charset=utf-8` //nolint:revive // Allow for underscores. + FmtOpenMetrics_2_0_0_rc Format = OpenMetricsType + `; version=` + OpenMetricsVersion_2_0_0_rc + `; charset=utf-8` + //nolint:revive // Allow for underscores. FmtOpenMetrics_1_0_0 Format = OpenMetricsType + `; version=` + OpenMetricsVersion_1_0_0 + `; charset=utf-8` //nolint:revive // Allow for underscores. FmtOpenMetrics_0_0_1 Format = OpenMetricsType + `; version=` + OpenMetricsVersion_0_0_1 + `; charset=utf-8` @@ -106,20 +110,27 @@ func NewFormat(t FormatType) Format { // NewOpenMetricsFormat generates a new OpenMetrics format matching the // specified version number. // -// Note: OpenMetrics version 2.0.0 is experimental and encode-only (supporting +// Note: OpenMetrics versions 2.0.0-rc and 2.0.0 are experimental and encode-only (supporting // counter, gauge, summary, untyped, histogram, and gaugehistogram metric types). func NewOpenMetricsFormat(version string) (Format, error) { - if version == OpenMetricsVersion_0_0_1 { + switch version { + case OpenMetricsVersion_0_0_1: return FmtOpenMetrics_0_0_1, nil - } - if version == OpenMetricsVersion_1_0_0 { + case OpenMetricsVersion_1_0_0: return FmtOpenMetrics_1_0_0, nil - } - if version == OpenMetricsVersion_2_0_0 { + case OpenMetricsVersion_2_0_0_rc: + // OpenMetrics 2.0.0-rc is experimental and encode-only. + return FmtOpenMetrics_2_0_0_rc, nil + case OpenMetricsVersion_2_0_0: // OpenMetrics 2.0.0 is experimental and encode-only. return FmtOpenMetrics_2_0_0, nil + default: + return FmtUnknown, errors.New("unknown open metrics version string") } - return FmtUnknown, errors.New("unknown open metrics version string") +} + +func isOpenMetrics20Version(v string) bool { + return v == OpenMetricsVersion_2_0_0 || v == OpenMetricsVersion_2_0_0_rc } // Version returns the version parameter without validating whether it is supported. diff --git a/vendor/github.com/prometheus/common/expfmt/openmetrics_parse.go b/vendor/github.com/prometheus/common/expfmt/openmetrics_parse.go new file mode 100644 index 00000000000..28b68a89394 --- /dev/null +++ b/vendor/github.com/prometheus/common/expfmt/openmetrics_parse.go @@ -0,0 +1,1118 @@ +// Copyright The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package expfmt + +import ( + "bufio" + "errors" + "fmt" + "io" + "math" + "strconv" + "strings" + "sync" + "time" + "unicode/utf8" + + goabnf "github.com/pandatix/go-abnf" + dto "github.com/prometheus/client_model/go" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/prometheus/common/model" +) + +const openMetricsExemplarMaxRunes = 128 + +// openMetrics10ABNF is the OpenMetrics 1.0 ABNF grammar (RFC 5234) from +// https://github.com/prometheus/OpenMetrics/blob/v1.0.0/specification/OpenMetrics.md#abnf, +// extended with UTF-8 quoted metric and label names. When legacy validation is +// active on the decoder, quoted names that fail legacy validation are rejected +// during semantic validation. +// +// NOTE: Every OpenMetrics 1.0 metric-descriptor, sample, and eof construct is +// strictly single-line (normal-char excludes LF %x0A). Adding the `line` rule +// allows streaming line-by-line from an io.Reader via goabnf.ParseForest per +// line while keeping the full exposition grammar intact. +const openMetrics10ABNF = `exposition = metricset eof-line + +metricset = *(metric-descriptor / sample) + +line = metric-descriptor / sample / eof-line +eof-line = HASH SP eof [ LF ] + +metric-descriptor = HASH SP type SP metric-descriptor-name SP metric-type LF +metric-descriptor =/ HASH SP help SP metric-descriptor-name SP help-value LF +metric-descriptor =/ HASH SP unit SP metric-descriptor-name SP unit-value LF + +metric-descriptor-name = metricname / metricname-utf8 +help-value = escaped-string +unit-value = *metricname-char + +metric-type = counter / gauge / histogram / gaugehistogram / stateset +metric-type =/ info / summary / unknown + +sample = metricname-and-labels SP sample-value [SP sample-timestamp] [exemplar] LF + +sample-value = number +sample-timestamp = timestamp + +exemplar = SP HASH SP labels SP exemplar-value [SP exemplar-timestamp] +exemplar-value = number +exemplar-timestamp = timestamp + +metricname-and-labels = (metricname / metricname-utf8) [labels] / labels-with-metricname +labels = "{" [label *(COMMA label)] "}" +labels-with-metricname = "{" [label *(COMMA label) COMMA] metricname-utf8 *(COMMA label) "}" + +label = label-key EQ DQUOTE label-value DQUOTE +label-key = label-name / label-name-utf8 +label-name-utf8 = DQUOTE escaped-string-non-empty DQUOTE +label-value = escaped-string + +number = realnumber +; Case insensitive +number =/ [SIGN] ("inf" / "infinity") +number =/ "nan" + +timestamp = realnumber + +; Leading 0s explicitly okay +realnumber = [SIGN] 1*DIGIT +realnumber =/ [SIGN] 1*DIGIT ["." *DIGIT] [ "e" [SIGN] 1*DIGIT ] +realnumber =/ [SIGN] *DIGIT "." 1*DIGIT [ "e" [SIGN] 1*DIGIT ] + +; RFC 5234 is case insensitive. +; Uppercase +eof = %d69.79.70 +type = %d84.89.80.69 +help = %d72.69.76.80 +unit = %d85.78.73.84 +; Lowercase +counter = %d99.111.117.110.116.101.114 +gauge = %d103.97.117.103.101 +histogram = %d104.105.115.116.111.103.114.97.109 +gaugehistogram = gauge histogram +stateset = %d115.116.97.116.101.115.101.116 +info = %d105.110.102.111 +summary = %d115.117.109.109.97.114.121 +unknown = %d117.110.107.110.111.119.110 + +BS = "\" +EQ = "=" +COMMA = "," +HASH = "#" +SIGN = "-" / "+" + +metricname = metricname-initial-char 0*metricname-char + +metricname-char = metricname-initial-char / DIGIT +metricname-initial-char = ALPHA / "_" / ":" +metricname-utf8 = DQUOTE escaped-string-non-empty DQUOTE + +label-name = label-name-initial-char *label-name-char + +label-name-char = label-name-initial-char / DIGIT +label-name-initial-char = ALPHA / "_" + +escaped-string = *escaped-char +escaped-string-non-empty = 1*escaped-char + +escaped-char = normal-char +escaped-char =/ BS (DQUOTE / BS / normal-char) + +; Any unicode character, except newline, double quote, and backslash +normal-char = %x00-09 / %x0B-21 / %x23-5B / %x5D-D7FF / %xE000-10FFFF +` + +var ( + om10GrammarOnce sync.Once + om10Grammar *goabnf.Grammar + om10GrammarErr error + + omEscapedReplacer = strings.NewReplacer( + `\"`, `"`, + `\\`, `\`, + `\n`, "\n", + ) +) + +func getOpenMetrics10Grammar() (*goabnf.Grammar, error) { + om10GrammarOnce.Do(func() { + raw := strings.ReplaceAll(openMetrics10ABNF, "\r\n", "\n") + raw = strings.ReplaceAll(raw, "\n", "\r\n") + om10Grammar, om10GrammarErr = goabnf.ParseABNF([]byte(raw)) + }) + return om10Grammar, om10GrammarErr +} + +type openMetricsFamilyState struct { + mf *dto.MetricFamily + hasType bool + hasHelp bool + hasUnit bool + hasSamples bool + emitted bool + + counters map[uint64]*dto.Metric + summaries map[uint64]*dto.Metric + histograms map[uint64]*dto.Metric +} + +// openMetricsParser streams OpenMetrics 1.0 text exposition format line-by-line +// using the OpenMetrics 1.0 ABNF grammar. +type openMetricsParser struct { + br *bufio.Reader + g *goabnf.Grammar + initErr error + scheme model.ValidationScheme + families map[string]*openMetricsFamilyState + curFamily *openMetricsFamilyState + scratchLbl map[string]string + lineNum int + pendingLine []byte + pendingNode *goabnf.ParseTree + done bool +} + +func newOpenMetricsParser(r io.Reader, scheme model.ValidationScheme) *openMetricsParser { + br, ok := r.(*bufio.Reader) + if !ok { + br = bufio.NewReader(r) + } + g, err := getOpenMetrics10Grammar() + if err != nil { + err = fmt.Errorf("failed to initialize OpenMetrics 1.0 ABNF grammar: %w", err) + } + return &openMetricsParser{ + br: br, + g: g, + initErr: err, + scheme: scheme, + families: make(map[string]*openMetricsFamilyState), + scratchLbl: make(map[string]string), + } +} + +func (p *openMetricsParser) validationScheme() model.ValidationScheme { + if p.scheme == model.UnsetValidation { + return model.LegacyValidation + } + return p.scheme +} + +func emitOpenMetricsFamily(st *openMetricsFamilyState, v *dto.MetricFamily, lineNum int) error { + if st.mf.Type == nil { + st.mf.Type = dto.MetricType_UNTYPED.Enum() + } + name := st.mf.Name + if st.mf.GetType() == dto.MetricType_COUNTER { + for _, m := range st.mf.Metric { + if m.GetCounter().Value == nil { + return ParseError{ + Line: lineNum, + Msg: fmt.Sprintf("counter %q missing _total value", st.mf.GetName()), + } + } + } + name = proto.String(st.mf.GetName() + "_total") + } + for _, m := range st.histograms { + normalizeHistogram(m.GetHistogram()) + } + v.Name = name + v.Help = st.mf.Help + v.Type = st.mf.Type + v.Metric = st.mf.Metric + v.Unit = st.mf.Unit + st.emitted = true + st.mf.Metric = nil + st.counters = nil + st.summaries = nil + st.histograms = nil + return nil +} + +// next reads from the input stream until the next MetricFamily is complete and +// populates v, or returns io.EOF when # EOF is reached. +func (p *openMetricsParser) next(v *dto.MetricFamily) error { + if p.initErr != nil { + return p.initErr + } + for { + if p.done { + return io.EOF + } + + var ( + line []byte + node *goabnf.ParseTree + ) + if p.pendingNode != nil { + line = p.pendingLine + node = p.pendingNode + p.pendingLine = nil + p.pendingNode = nil + } else { + rawLine, readErr := p.br.ReadBytes('\n') + if len(rawLine) == 0 { + if errors.Is(readErr, io.EOF) { + return ParseError{ + Line: max(1, p.lineNum+1), + Msg: "data does not end with # EOF", + } + } + return readErr + } + if readErr != nil && !errors.Is(readErr, io.EOF) { + return readErr + } + p.lineNum++ + line = rawLine + + forest, err := goabnf.ParseForest(line, p.g, "line") + if err != nil { + return err + } + if !forest.Valid() { + return formatABNFLineParseError(p.lineNum, forest.ParseError()) + } + tree := forest.Tree() + if tree == nil || len(tree.Children) == 0 { + return ParseError{Line: p.lineNum, Msg: "failed to extract parse tree"} + } + node = tree.Children[0] + } + + switch node.Rule { + case "eof-line": + if _, peekErr := p.br.Peek(1); !errors.Is(peekErr, io.EOF) { + if peekErr != nil { + return peekErr + } + return ParseError{ + Line: p.lineNum + 1, + Msg: "unexpected data after # EOF", + } + } + p.done = true + if p.curFamily != nil && p.curFamily.hasSamples && !p.curFamily.emitted { + st := p.curFamily + p.curFamily = nil + return emitOpenMetricsFamily(st, v, p.lineNum) + } + return io.EOF + + case "metric-descriptor": + if err := p.handleDescriptor(line, node); err != nil { + return err + } + + case "sample": + if p.curFamily != nil && p.curFamily.hasSamples && !p.curFamily.emitted { + sampleName := extractSampleMetricName(line, node) + if _, ok := matchFamilySample(p.curFamily, sampleName); !ok && sampleName != p.curFamily.mf.GetName() { + p.pendingLine = line + p.pendingNode = node + st := p.curFamily + p.curFamily = nil + return emitOpenMetricsFamily(st, v, p.lineNum) + } + } + if err := p.handleSample(line, node); err != nil { + return err + } + } + } +} + +func (p *openMetricsParser) getOrCreateDescriptorFamily(name string) (*openMetricsFamilyState, error) { + if !p.validationScheme().IsValidMetricName(name) { + return nil, ParseError{Line: p.lineNum, Msg: fmt.Sprintf("invalid metric name %q", name)} + } + if p.curFamily != nil && !p.curFamily.emitted && p.curFamily.mf.GetName() == name { + if p.curFamily.hasSamples { + return nil, ParseError{Line: p.lineNum, Msg: fmt.Sprintf("metadata for metric %q after samples", name)} + } + return p.curFamily, nil + } + if prev, exists := p.families[name]; exists { + if prev.hasSamples { + return nil, ParseError{Line: p.lineNum, Msg: fmt.Sprintf("metadata for metric %q after samples", name)} + } + return prev, nil + } + st := &openMetricsFamilyState{ + mf: &dto.MetricFamily{ + Name: proto.String(name), + }, + } + p.families[name] = st + if p.curFamily == nil || !p.curFamily.hasSamples { + p.curFamily = st + } + return st, nil +} + +func (p *openMetricsParser) handleDescriptor(input []byte, node *goabnf.ParseTree) error { + var ( + descKind string + mfName string + valNode *goabnf.ParseTree + ) + for _, c := range node.Children { + switch c.Rule { + case "type", "help", "unit": + descKind = c.Rule + case "metric-descriptor-name": + mfName = extractMetricNameNode(input, c) + case "metric-type", "help-value", "unit-value": + valNode = c + } + } + + st, err := p.getOrCreateDescriptorFamily(mfName) + if err != nil { + return err + } + + switch descKind { + case "type": + if st.hasType { + return ParseError{Line: p.lineNum, Msg: fmt.Sprintf("second TYPE line for metric name %q", mfName)} + } + st.hasType = true + rawType := string(input[valNode.Start:valNode.End]) + switch rawType { + case "counter": + st.mf.Type = dto.MetricType_COUNTER.Enum() + case "gauge": + st.mf.Type = dto.MetricType_GAUGE.Enum() + case "histogram": + st.mf.Type = dto.MetricType_HISTOGRAM.Enum() + case "gaugehistogram": + st.mf.Type = dto.MetricType_GAUGE_HISTOGRAM.Enum() + case "summary": + st.mf.Type = dto.MetricType_SUMMARY.Enum() + case "unknown": + st.mf.Type = dto.MetricType_UNTYPED.Enum() + case "info", "stateset": + // TODO(bwplotka): Expand client_model (dto.MetricType) to support INFO and STATESET metric types. + return ParseError{Line: p.lineNum, Msg: fmt.Sprintf("unsupported metric type %q", rawType)} + default: + return ParseError{Line: p.lineNum, Msg: fmt.Sprintf("unknown metric type %q", rawType)} + } + case "help": + if st.hasHelp { + return ParseError{Line: p.lineNum, Msg: fmt.Sprintf("second HELP line for metric name %q", mfName)} + } + st.hasHelp = true + rawHelp := string(input[valNode.Start:valNode.End]) + if strings.HasSuffix(rawHelp, "\r") { + return ParseError{Line: p.lineNum, Msg: fmt.Sprintf("unexpected carriage return in HELP for metric %q", mfName)} + } + st.mf.Help = proto.String(unescapeOpenMetricsString(rawHelp)) + case "unit": + if st.hasUnit { + return ParseError{Line: p.lineNum, Msg: fmt.Sprintf("second UNIT line for metric name %q", mfName)} + } + st.hasUnit = true + unit := string(input[valNode.Start:valNode.End]) + if unit != "" { + if !strings.HasSuffix(mfName, "_"+unit) { + return ParseError{Line: p.lineNum, Msg: fmt.Sprintf("unit %q not a suffix of metric %q", unit, mfName)} + } + st.mf.Unit = proto.String(unit) + } + } + return nil +} + +type sampleRole int + +const ( + roleValue sampleRole = iota + roleCreated + roleCount + roleSum + roleBucket + roleQuantile +) + +func (p *openMetricsParser) resolveSampleFamily(sampleName string) (*openMetricsFamilyState, sampleRole, error) { + if !p.validationScheme().IsValidMetricName(sampleName) { + return nil, roleValue, ParseError{Line: p.lineNum, Msg: fmt.Sprintf("invalid metric name %q", sampleName)} + } + + if p.curFamily != nil && !p.curFamily.emitted { + if role, ok := matchFamilySample(p.curFamily, sampleName); ok { + if p.curFamily.mf.Type == nil { + p.curFamily.mf.Type = dto.MetricType_UNTYPED.Enum() + } + p.curFamily.hasSamples = true + return p.curFamily, role, nil + } + // If curFamily was explicitly typed as counter/histogram/gaugehistogram, + // a bare sample matching the base family name is missing its required suffix. + if p.curFamily.hasType && sampleName == p.curFamily.mf.GetName() { + return nil, roleValue, ParseError{Line: p.lineNum, Msg: fmt.Sprintf("sample %q missing required suffix for type %s", sampleName, p.curFamily.mf.GetType())} + } + } + + if st, exists := p.families[sampleName]; exists { + if role, ok := matchFamilySample(st, sampleName); ok { + if st.emitted { + return nil, roleValue, ParseError{Line: p.lineNum, Msg: fmt.Sprintf("interleaved metric family %q", st.mf.GetName())} + } + if st.mf.Type == nil { + st.mf.Type = dto.MetricType_UNTYPED.Enum() + } + st.hasSamples = true + p.curFamily = st + return st, role, nil + } + if st.hasType { + return nil, roleValue, ParseError{Line: p.lineNum, Msg: fmt.Sprintf("sample %q missing required suffix for type %s", sampleName, st.mf.GetType())} + } + } + + for _, st := range p.families { + if role, ok := matchFamilySample(st, sampleName); ok { + if st.emitted { + return nil, roleValue, ParseError{Line: p.lineNum, Msg: fmt.Sprintf("interleaved metric family %q", st.mf.GetName())} + } + if st.mf.Type == nil { + st.mf.Type = dto.MetricType_UNTYPED.Enum() + } + st.hasSamples = true + p.curFamily = st + return st, role, nil + } + } + + st := &openMetricsFamilyState{ + mf: &dto.MetricFamily{ + Name: proto.String(sampleName), + Type: dto.MetricType_UNTYPED.Enum(), + }, + hasSamples: true, + } + p.families[sampleName] = st + p.curFamily = st + return st, roleValue, nil +} + +func matchFamilySample(st *openMetricsFamilyState, sampleName string) (sampleRole, bool) { + if st == nil || st.mf == nil { + return roleValue, false + } + mfName := st.mf.GetName() + if st.mf.Type == nil { + if sampleName == mfName { + return roleValue, true + } + return roleValue, false + } + + switch st.mf.GetType() { + case dto.MetricType_COUNTER: + switch sampleName { + case mfName + "_total": + return roleValue, true + case mfName + "_created": + return roleCreated, true + } + case dto.MetricType_GAUGE, dto.MetricType_UNTYPED: + if sampleName == mfName { + return roleValue, true + } + case dto.MetricType_SUMMARY: + switch sampleName { + case mfName: + return roleQuantile, true + case mfName + "_count": + return roleCount, true + case mfName + "_sum": + return roleSum, true + case mfName + "_created": + return roleCreated, true + } + case dto.MetricType_HISTOGRAM: + switch sampleName { + case mfName + "_bucket": + return roleBucket, true + case mfName + "_count": + return roleCount, true + case mfName + "_sum": + return roleSum, true + case mfName + "_created": + return roleCreated, true + } + case dto.MetricType_GAUGE_HISTOGRAM: + switch sampleName { + case mfName + "_bucket": + return roleBucket, true + case mfName + "_gcount", mfName + "_count": + return roleCount, true + case mfName + "_gsum", mfName + "_sum": + return roleSum, true + case mfName + "_created": + return roleCreated, true + } + } + return roleValue, false +} + +func (p *openMetricsParser) handleSample(input []byte, node *goabnf.ParseTree) error { + var ( + nameAndLabelsNode *goabnf.ParseTree + valStr string + hasTS bool + tsStr string + exemplarNode *goabnf.ParseTree + ) + for _, c := range node.Children { + switch c.Rule { + case "metricname-and-labels": + nameAndLabelsNode = c + case "sample-value": + valStr = string(input[c.Start:c.End]) + case "sample-timestamp": + hasTS = true + tsStr = string(input[c.Start:c.End]) + case "exemplar": + exemplarNode = c + } + } + + sampleName, rawLabels := extractNameAndLabels(input, nameAndLabelsNode) + st, role, err := p.resolveSampleFamily(sampleName) + if err != nil { + return err + } + + val, err := parseOpenMetricsFloat(valStr) + if err != nil { + return ParseError{Line: p.lineNum, Msg: fmt.Sprintf("invalid sample value %q: %v", valStr, err)} + } + + var tsMs *int64 + if hasTS { + parsedTS, err := parseSampleTimestampMs(tsStr) + if err != nil { + return ParseError{Line: p.lineNum, Msg: fmt.Sprintf("invalid timestamp %q: %v", tsStr, err)} + } + tsMs = proto.Int64(parsedTS) + } + + var ex *dto.Exemplar + if exemplarNode != nil { + ex, err = p.parseExemplar(input, exemplarNode, true) + if err != nil { + return err + } + } + + // Validate labels and extract special 'quantile' or 'le' label if applicable. + clear(p.scratchLbl) + p.scratchLbl[string(model.MetricNameLabel)] = st.mf.GetName() + var ( + labelPairs []*dto.LabelPair + quantile = math.NaN() + bucketLE = math.NaN() + ) + seenLabels := make(map[string]struct{}, len(rawLabels)) + for _, rl := range rawLabels { + if rl.name == string(model.MetricNameLabel) { + return ParseError{Line: p.lineNum, Msg: fmt.Sprintf("label name %q is reserved", model.MetricNameLabel)} + } + if !p.validationScheme().IsValidLabelName(rl.name) { + return ParseError{Line: p.lineNum, Msg: fmt.Sprintf("invalid label name %q", rl.name)} + } + if !model.LabelValue(rl.value).IsValid() { + return ParseError{Line: p.lineNum, Msg: fmt.Sprintf("invalid label value %q", rl.value)} + } + if _, dup := seenLabels[rl.name]; dup { + return ParseError{Line: p.lineNum, Msg: fmt.Sprintf("duplicate label %q for metric %q", rl.name, st.mf.GetName())} + } + seenLabels[rl.name] = struct{}{} + + if role == roleQuantile && rl.name == model.QuantileLabel { + q, err := parseOpenMetricsFloat(rl.value) + if err != nil || math.IsNaN(q) || q < 0 || q > 1 { + return ParseError{Line: p.lineNum, Msg: fmt.Sprintf("invalid quantile %q for summary %q", rl.value, st.mf.GetName())} + } + quantile = q + continue + } + if role == roleBucket && rl.name == model.BucketLabel { + le, err := parseOpenMetricsFloat(rl.value) + if err != nil || math.IsNaN(le) { + return ParseError{Line: p.lineNum, Msg: fmt.Sprintf("invalid bucket bound %q for histogram %q", rl.value, st.mf.GetName())} + } + bucketLE = le + continue + } + p.scratchLbl[rl.name] = rl.value + labelPairs = append(labelPairs, &dto.LabelPair{ + Name: proto.String(rl.name), + Value: proto.String(rl.value), + }) + } + + switch st.mf.GetType() { + case dto.MetricType_GAUGE: + if ex != nil { + return ParseError{Line: p.lineNum, Msg: fmt.Sprintf("exemplar not allowed on gauge metric %q", st.mf.GetName())} + } + st.mf.Metric = append(st.mf.Metric, &dto.Metric{ + Label: labelPairs, + Gauge: &dto.Gauge{Value: proto.Float64(val)}, + TimestampMs: tsMs, + }) + + case dto.MetricType_UNTYPED: + if ex != nil { + return ParseError{Line: p.lineNum, Msg: fmt.Sprintf("exemplar not allowed on untyped metric %q", st.mf.GetName())} + } + st.mf.Metric = append(st.mf.Metric, &dto.Metric{ + Label: labelPairs, + Untyped: &dto.Untyped{Value: proto.Float64(val)}, + TimestampMs: tsMs, + }) + + case dto.MetricType_COUNTER: + if st.counters == nil { + st.counters = make(map[uint64]*dto.Metric) + } + sig := model.LabelsToSignature(p.scratchLbl) + switch role { + case roleValue: + if math.IsNaN(val) || val < 0 { + return ParseError{Line: p.lineNum, Msg: fmt.Sprintf("counter value must not be negative or NaN for %q", st.mf.GetName())} + } + m := st.counters[sig] + if m == nil || m.Counter.Value != nil { + m = &dto.Metric{ + Label: labelPairs, + Counter: &dto.Counter{}, + } + st.counters[sig] = m + st.mf.Metric = append(st.mf.Metric, m) + } + m.Counter.Value = proto.Float64(val) + m.Counter.Exemplar = ex + m.TimestampMs = tsMs + case roleCreated: + if ex != nil { + return ParseError{Line: p.lineNum, Msg: fmt.Sprintf("exemplar not allowed on _created line for %q", st.mf.GetName())} + } + createdTS, err := parseTimestampToProto(valStr) + if err != nil { + return ParseError{Line: p.lineNum, Msg: fmt.Sprintf("invalid created timestamp %q: %v", valStr, err)} + } + m := st.counters[sig] + if m == nil || m.Counter.CreatedTimestamp != nil { + m = &dto.Metric{ + Label: labelPairs, + Counter: &dto.Counter{}, + } + st.counters[sig] = m + st.mf.Metric = append(st.mf.Metric, m) + } + m.Counter.CreatedTimestamp = createdTS + } + + case dto.MetricType_SUMMARY: + if ex != nil { + return ParseError{Line: p.lineNum, Msg: fmt.Sprintf("exemplar not allowed on summary metric %q", st.mf.GetName())} + } + if st.summaries == nil { + st.summaries = make(map[uint64]*dto.Metric) + } + sig := model.LabelsToSignature(p.scratchLbl) + m := st.summaries[sig] + if m == nil { + m = &dto.Metric{ + Label: labelPairs, + Summary: &dto.Summary{}, + } + st.summaries[sig] = m + st.mf.Metric = append(st.mf.Metric, m) + } + if role != roleCreated && tsMs != nil { + m.TimestampMs = tsMs + } + switch role { + case roleQuantile: + if math.IsNaN(quantile) { + return ParseError{Line: p.lineNum, Msg: fmt.Sprintf("missing quantile label for summary %q", st.mf.GetName())} + } + if !math.IsNaN(val) && val < 0 { + return ParseError{Line: p.lineNum, Msg: fmt.Sprintf("negative quantile value for summary %q", st.mf.GetName())} + } + m.Summary.Quantile = append(m.Summary.Quantile, &dto.Quantile{ + Quantile: proto.Float64(quantile), + Value: proto.Float64(val), + }) + case roleCount: + if math.IsNaN(val) || val < 0 || float64(uint64(val)) != val { + return ParseError{Line: p.lineNum, Msg: fmt.Sprintf("invalid count %q for summary %q", valStr, st.mf.GetName())} + } + m.Summary.SampleCount = proto.Uint64(uint64(val)) + case roleSum: + if math.IsNaN(val) || val < 0 { + return ParseError{Line: p.lineNum, Msg: fmt.Sprintf("invalid sum %q for summary %q", valStr, st.mf.GetName())} + } + m.Summary.SampleSum = proto.Float64(val) + case roleCreated: + createdTS, err := parseTimestampToProto(valStr) + if err != nil { + return ParseError{Line: p.lineNum, Msg: fmt.Sprintf("invalid created timestamp %q: %v", valStr, err)} + } + m.Summary.CreatedTimestamp = createdTS + } + + case dto.MetricType_HISTOGRAM, dto.MetricType_GAUGE_HISTOGRAM: + if st.histograms == nil { + st.histograms = make(map[uint64]*dto.Metric) + } + sig := model.LabelsToSignature(p.scratchLbl) + m := st.histograms[sig] + if m == nil { + m = &dto.Metric{ + Label: labelPairs, + Histogram: &dto.Histogram{}, + } + st.histograms[sig] = m + st.mf.Metric = append(st.mf.Metric, m) + } + if role != roleCreated && tsMs != nil { + m.TimestampMs = tsMs + } + switch role { + case roleBucket: + if math.IsNaN(bucketLE) { + return ParseError{Line: p.lineNum, Msg: fmt.Sprintf("missing le label for histogram %q", st.mf.GetName())} + } + if math.IsNaN(val) || val < 0 { + return ParseError{Line: p.lineNum, Msg: fmt.Sprintf("negative or NaN bucket count for histogram %q", st.mf.GetName())} + } + b := &dto.Bucket{ + UpperBound: proto.Float64(bucketLE), + Exemplar: ex, + } + if uintVal := uint64(val); float64(uintVal) == val { + b.CumulativeCount = proto.Uint64(uintVal) + } else { + b.CumulativeCountFloat = proto.Float64(val) + } + m.Histogram.Bucket = append(m.Histogram.Bucket, b) + case roleCount: + if ex != nil { + return ParseError{Line: p.lineNum, Msg: fmt.Sprintf("exemplar not allowed on count sample for histogram %q", st.mf.GetName())} + } + if math.IsNaN(val) || val < 0 { + return ParseError{Line: p.lineNum, Msg: fmt.Sprintf("negative or NaN count for histogram %q", st.mf.GetName())} + } + if uintVal := uint64(val); float64(uintVal) == val { + m.Histogram.SampleCount = proto.Uint64(uintVal) + } else { + m.Histogram.SampleCountFloat = proto.Float64(val) + } + case roleSum: + if ex != nil { + return ParseError{Line: p.lineNum, Msg: fmt.Sprintf("exemplar not allowed on sum sample for histogram %q", st.mf.GetName())} + } + if math.IsNaN(val) || (st.mf.GetType() == dto.MetricType_HISTOGRAM && val < 0) { + return ParseError{Line: p.lineNum, Msg: fmt.Sprintf("invalid sum %q for histogram %q", valStr, st.mf.GetName())} + } + m.Histogram.SampleSum = proto.Float64(val) + case roleCreated: + if ex != nil { + return ParseError{Line: p.lineNum, Msg: fmt.Sprintf("exemplar not allowed on _created sample for histogram %q", st.mf.GetName())} + } + createdTS, err := parseTimestampToProto(valStr) + if err != nil { + return ParseError{Line: p.lineNum, Msg: fmt.Sprintf("invalid created timestamp %q: %v", valStr, err)} + } + m.Histogram.CreatedTimestamp = createdTS + } + } + + return nil +} + +func (p *openMetricsParser) parseExemplar(input []byte, node *goabnf.ParseTree, enforceRuneLimit bool) (*dto.Exemplar, error) { + var ( + rawLabels []parsedLabel + valStr string + hasTS bool + tsStr string + ) + for _, c := range node.Children { + switch c.Rule { + case "labels", "labels-in-braces": + rawLabels = extractLabelsFromList(input, c) + case "exemplar-value", "number": + valStr = string(input[c.Start:c.End]) + case "exemplar-timestamp", "timestamp": + hasTS = true + tsStr = string(input[c.Start:c.End]) + } + } + + val, err := parseOpenMetricsFloat(valStr) + if err != nil || math.IsNaN(val) || math.IsInf(val, 0) { + return nil, ParseError{Line: p.lineNum, Msg: fmt.Sprintf("invalid exemplar value %q", valStr)} + } + + var ( + pairs = make([]*dto.LabelPair, 0, len(rawLabels)) + seenNames = make(map[string]struct{}, len(rawLabels)) + totalRune int + ) + for _, rl := range rawLabels { + if !p.validationScheme().IsValidLabelName(rl.name) { + return nil, ParseError{Line: p.lineNum, Msg: fmt.Sprintf("invalid exemplar label name %q", rl.name)} + } + if !model.LabelValue(rl.value).IsValid() { + return nil, ParseError{Line: p.lineNum, Msg: fmt.Sprintf("invalid exemplar label value %q", rl.value)} + } + if _, dup := seenNames[rl.name]; dup { + return nil, ParseError{Line: p.lineNum, Msg: fmt.Sprintf("duplicate exemplar label name %q", rl.name)} + } + seenNames[rl.name] = struct{}{} + totalRune += utf8.RuneCountInString(rl.name) + utf8.RuneCountInString(rl.value) + pairs = append(pairs, &dto.LabelPair{ + Name: proto.String(rl.name), + Value: proto.String(rl.value), + }) + } + if enforceRuneLimit && totalRune > openMetricsExemplarMaxRunes { + return nil, ParseError{Line: p.lineNum, Msg: fmt.Sprintf("exemplar label set exceeds %d runes (%d)", openMetricsExemplarMaxRunes, totalRune)} + } + + ex := &dto.Exemplar{ + Label: pairs, + Value: proto.Float64(val), + } + if hasTS { + tsProto, err := parseTimestampToProto(tsStr) + if err != nil { + return nil, ParseError{Line: p.lineNum, Msg: fmt.Sprintf("invalid exemplar timestamp %q: %v", tsStr, err)} + } + ex.Timestamp = tsProto + } + return ex, nil +} + +type parsedLabel struct { + name string + value string +} + +func extractSampleMetricName(input []byte, sampleNode *goabnf.ParseTree) string { + for _, c := range sampleNode.Children { + if c.Rule == "metricname-and-labels" { + name, _ := extractNameAndLabels(input, c) + return name + } + } + return "" +} + +func extractMetricNameNode(input []byte, node *goabnf.ParseTree) string { + for _, c := range node.Children { + switch c.Rule { + case "metricname": + return string(input[c.Start:c.End]) + case "metricname-utf8": + return unescapeOpenMetricsString(string(input[c.Start+1 : c.End-1])) + } + } + return "" +} + +func extractNameAndLabels(input []byte, node *goabnf.ParseTree) (string, []parsedLabel) { + var ( + metricName string + labels []parsedLabel + ) + var walk func(n *goabnf.ParseTree) + walk = func(n *goabnf.ParseTree) { + for _, c := range n.Children { + switch c.Rule { + case "metricname": + metricName = string(input[c.Start:c.End]) + case "metricname-utf8": + metricName = unescapeOpenMetricsString(string(input[c.Start+1 : c.End-1])) + case "label": + labels = append(labels, extractSingleLabel(input, c)) + case "labels", "labels-in-braces", "labels-with-metricname", "name-and-labels-in-braces": + walk(c) + } + } + } + walk(node) + return metricName, labels +} + +func extractLabelsFromList(input []byte, node *goabnf.ParseTree) []parsedLabel { + var labels []parsedLabel + for _, c := range node.Children { + if c.Rule == "label" { + labels = append(labels, extractSingleLabel(input, c)) + } + } + return labels +} + +func extractSingleLabel(input []byte, node *goabnf.ParseTree) parsedLabel { + var out parsedLabel + for _, c := range node.Children { + switch c.Rule { + case "label-key": + for _, kc := range c.Children { + switch kc.Rule { + case "label-name": + out.name = string(input[kc.Start:kc.End]) + case "label-name-utf8": + out.name = unescapeOpenMetricsString(string(input[kc.Start+1 : kc.End-1])) + default: + if kc.Rule == "" && kc.End > kc.Start+2 && input[kc.Start] == '"' && input[kc.End-1] == '"' { + out.name = unescapeOpenMetricsString(string(input[kc.Start+1 : kc.End-1])) + } + } + } + case "label-value": + out.value = unescapeOpenMetricsString(string(input[c.Start:c.End])) + } + } + return out +} + +func unescapeOpenMetricsString(s string) string { + if strings.IndexByte(s, '\\') < 0 { + return s + } + return omEscapedReplacer.Replace(s) +} + +func parseOpenMetricsFloat(s string) (float64, error) { + if strings.ContainsAny(s, "pP_") { + return 0, fmt.Errorf("unsupported character in float %q", s) + } + return strconv.ParseFloat(s, 64) +} + +func parseSampleTimestampMs(s string) (int64, error) { + f, err := parseOpenMetricsFloat(s) + if err != nil { + return 0, err + } + if math.IsNaN(f) || math.IsInf(f, 0) { + return 0, fmt.Errorf("invalid timestamp %v", f) + } + return int64(math.Round(f * 1000)), nil +} + +func parseTimestampToProto(s string) (*timestamppb.Timestamp, error) { + if ts, ok := parseExactDecimalTimestamp(s); ok { + return ts, nil + } + f, err := parseOpenMetricsFloat(s) + if err != nil { + return nil, err + } + if math.IsNaN(f) || math.IsInf(f, 0) { + return nil, fmt.Errorf("invalid timestamp %v", f) + } + sec, frac := math.Modf(f) + nanos := int64(math.Round(frac * 1e9)) + return timestamppb.New(time.Unix(int64(sec), nanos)), nil +} + +func parseExactDecimalTimestamp(s string) (*timestamppb.Timestamp, bool) { + neg := false + switch { + case strings.HasPrefix(s, "-"): + neg = true + s = s[1:] + case strings.HasPrefix(s, "+"): + s = s[1:] + } + var exp int + if idx := strings.IndexAny(s, "eE"); idx >= 0 { + eVal, err := strconv.Atoi(s[idx+1:]) + if err != nil || eVal < -18 || eVal > 18 { + return nil, false + } + exp = eVal + s = s[:idx] + } + secPart, fracPart, _ := strings.Cut(s, ".") + digits := secPart + fracPart + if digits == "" { + return nil, false + } + decPos := len(secPart) + exp + switch { + case decPos <= 0: + secPart = "0" + fracPart = strings.Repeat("0", -decPos) + digits + case decPos >= len(digits): + secPart = digits + strings.Repeat("0", decPos-len(digits)) + fracPart = "" + default: + secPart = digits[:decPos] + fracPart = digits[decPos:] + } + var sec int64 + if secPart != "" { + v, err := strconv.ParseInt(secPart, 10, 64) + if err != nil { + return nil, false + } + sec = v + } + var nanos int64 + if fracPart != "" { + if len(fracPart) > 9 { + fracPart = fracPart[:9] + } else { + fracPart += strings.Repeat("0", 9-len(fracPart)) + } + v, err := strconv.ParseInt(fracPart, 10, 64) + if err != nil { + return nil, false + } + nanos = v + } + if neg { + sec = -sec + nanos = -nanos + } + return timestamppb.New(time.Unix(sec, nanos)), true +} + +func formatABNFLineParseError(lineNum int, perr *goabnf.ParseError) error { + if perr == nil { + return ParseError{Line: lineNum, Msg: "syntax error"} + } + return ParseError{ + Line: lineNum, + Msg: perr.Error(), + } +} diff --git a/vendor/modules.txt b/vendor/modules.txt index d4107f7359b..67847aa333b 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -472,6 +472,9 @@ github.com/opencontainers/runtime-spec/specs-go github.com/opencontainers/selinux/go-selinux github.com/opencontainers/selinux/go-selinux/label github.com/opencontainers/selinux/pkg/pwalkdir +# github.com/pandatix/go-abnf v0.5.5 +## explicit; go 1.26.0 +github.com/pandatix/go-abnf # github.com/peterbourgon/diskv/v3 v3.0.1 ## explicit; go 1.12 github.com/peterbourgon/diskv/v3 @@ -493,7 +496,7 @@ github.com/prometheus/client_golang/prometheus/testutil/promlint/validations # github.com/prometheus/client_model v0.6.3 ## explicit; go 1.23 github.com/prometheus/client_model/go -# github.com/prometheus/common v0.72.0 +# github.com/prometheus/common v0.73.1 ## explicit; go 1.26.0 github.com/prometheus/common/expfmt github.com/prometheus/common/model