├── .github └── workflows │ └── test.yaml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── OWNERS ├── README.md ├── SECURITY_CONTACTS ├── code-of-conduct.md ├── docs └── iptables-porting.md ├── error.go ├── error_test.go ├── exec.go ├── exec_test.go ├── fake.go ├── fake_test.go ├── go.mod ├── go.sum ├── hack ├── test.sh ├── tools │ ├── .golangci.yml │ ├── README.md │ ├── go.mod │ ├── go.sum │ └── tools.go ├── update.sh └── verify.sh ├── nftables.go ├── nftables_test.go ├── objects.go ├── objects_test.go ├── transaction.go ├── types.go ├── util.go ├── util_test.go └── vendor ├── github.com ├── google │ └── go-cmp │ │ ├── LICENSE │ │ └── cmp │ │ ├── compare.go │ │ ├── export_panic.go │ │ ├── export_unsafe.go │ │ ├── internal │ │ ├── diff │ │ │ ├── debug_disable.go │ │ │ ├── debug_enable.go │ │ │ └── diff.go │ │ ├── flags │ │ │ └── flags.go │ │ ├── function │ │ │ └── func.go │ │ └── value │ │ │ ├── name.go │ │ │ ├── pointer_purego.go │ │ │ ├── pointer_unsafe.go │ │ │ └── sort.go │ │ ├── options.go │ │ ├── path.go │ │ ├── report.go │ │ ├── report_compare.go │ │ ├── report_references.go │ │ ├── report_reflect.go │ │ ├── report_slices.go │ │ ├── report_text.go │ │ └── report_value.go └── lithammer │ └── dedent │ ├── .travis.yml │ ├── LICENSE │ ├── README.md │ └── dedent.go └── modules.txt /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | on: [pull_request] 2 | name: Test 3 | jobs: 4 | test: 5 | strategy: 6 | fail-fast: false 7 | matrix: 8 | go-version: [1.20.x, 1.21.x] 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/setup-go@v4 12 | with: 13 | go-version: ${{ matrix.go-version }} 14 | - uses: actions/checkout@v4 15 | - run: make test 16 | verify: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/setup-go@v4 20 | with: 21 | go-version: 1.21.x 22 | - uses: actions/checkout@v4 23 | - run: make verify 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | hack/bin/golangci-lint 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # ChangeLog 2 | 3 | ## v0.0.18 4 | 5 | - Added locking to `Fake` to allow it to be safely used concurrently. 6 | (`@npinaeva`) 7 | 8 | - Added a `Flowtable` object, and `Fake` support for correctly parsing 9 | flowtable references. (`@aojea`) 10 | 11 | - Fixed a bug in `Fake.ParseDump`, which accidentally required the 12 | table to have a comment. (`@danwinship`) 13 | 14 | ## v0.0.17 15 | 16 | - `ListRules()` now accepts `""` for the chain name, meaning to list 17 | all rules in the table. (`@caseydavenport`) 18 | 19 | - `ListElements()` now handles elements with prefix/CIDR values (e.g., 20 | `"192.168.0.0/16"`; these are represented specially in the JSON 21 | format and the old code didn't handle them). (`@caseydavenport`) 22 | 23 | - Added `NumOperations()` to `Transaction` (which lets you figure out 24 | belatedly whether you added anything to the transaction or not, and 25 | could also be used for metrics). (`@fasaxc`) 26 | 27 | - `knftables.Interface` now reuses the same `bytes.Buffer` for each 28 | call to `nft` rather than constructing a new one each time, saving 29 | time and memory. (`@aroradaman`) 30 | 31 | - Fixed map element deletion in `knftables.Fake` to not mistakenly 32 | require that you fill in the `.Value` of the element. (`@npinaeva`) 33 | 34 | - Added `Fake.LastTransaction`, to retrieve the most-recently-executed 35 | transaction. (`@npinaeva`) 36 | 37 | ## v0.0.16 38 | 39 | - Fixed a bug in `Fake.ParseDump()` when using IPv6. (`@npinaeva`) 40 | 41 | ## v0.0.15 42 | 43 | - knftables now requires the nft binary to be v1.0.1 or later. This is 44 | because earlier versions (a) had bugs that might cause them to crash 45 | when parsing rules created by later versions of nft, and (b) always 46 | parsed the entire ruleset at startup, even if you were only trying 47 | to operate on a single table. The combination of those two factors 48 | means that older versions of nft can't reliably be used from inside 49 | a container. (`@danwinship`) 50 | 51 | - Fixed a bug that meant we were never setting comments on 52 | tables/chains/sets/etc, even if nft and the kernel were both new 53 | enough to support it. (`@tnqn`) 54 | 55 | - Added `Fake.ParseDump()`, to load a `Fake` from a `Fake.Dump()` 56 | output. (`@npinaeva`) 57 | 58 | ## v0.0.14 59 | 60 | - Renamed the package `"sigs.k8s.io/knftables"`, reflecting its new 61 | home at https://github.com/kubernetes-sigs/knftables/ 62 | 63 | - Improvements to `Fake`: 64 | 65 | - `Fake.Run()` is now properly transactional, and will have no 66 | side effects if an error occurs. 67 | 68 | - `Fake.Dump()` now outputs all `add chain`, `add set`, and `add 69 | table` commands before any `add rule` and `add element` 70 | commands, to ensure that the dumped ruleset can be passed to 71 | `nft -f` without errors. 72 | 73 | - Conversely, `Fake.Run()` now does enough parsing of rules and 74 | elements that it will notice rules that do lookups in 75 | non-existent sets/maps, and rules/verdicts that jump to 76 | non-existent chains, so it can error out in those cases. 77 | 78 | - Added `nft.Check()`, which is like `nft.Run()`, but using 79 | `nft --check`. 80 | 81 | - Fixed support for ingress and egress hooks (by adding 82 | `Chain.Device`). 83 | 84 | ## v0.0.13 85 | 86 | - Fixed a bug in `Fake.Run` where it was not properly returning "not 87 | found" / "already exists" errors. 88 | 89 | ## v0.0.12 90 | 91 | - Renamed the package from `"github.com/danwinship/nftables"` to 92 | `"github.com/danwinship/knftables"`, for less ambiguity. 93 | 94 | - Added `NameLengthMax` and `CommentLengthMax` constants. 95 | 96 | - Changed serialization of `Chain` to convert string-valued `Priority` 97 | to numeric form, if possible. 98 | 99 | - (The `v0.0.11` tag exists but is not usable due to a bad `go.mod`) 100 | 101 | ## v0.0.10 102 | 103 | - Dropped `Define`, because nft defines turned out to not work the way 104 | I thought (in particular, you can't do "$IP daddr"), so they end up 105 | not really being useful for our purposes. 106 | 107 | - Made `NewTransaction` a method on `Interface` rather than a 108 | top-level function. 109 | 110 | - Added `Transaction.String()`, for debugging 111 | 112 | - Fixed serialization of set/map elements with timeouts 113 | 114 | - Added special treament for `"@"` to `Concat` 115 | 116 | - Changed `nftables.New()` to return an `error` (doing the work that 117 | used to be done by `nft.Present()`.) 118 | 119 | - Add autodetection for "object comment" support, and have 120 | serialization just ignore comments on `Table`/`Chain`/`Set`/`Map` if 121 | nft or the kernel does not support them. 122 | 123 | - Renamed `Optional()` to `PtrTo()` 124 | 125 | ## v0.0.9 126 | 127 | - Various tweaks to `Element`: 128 | 129 | - Changed `Key` and `Value` from `string` to `[]string` to better 130 | support concatenated types (and dropped the `Join()` and 131 | `Split()` helper functions that were previously used to join and 132 | split concatenated values). 133 | 134 | - Split `Name` into separate `Set` and `Map` fields, which make it 135 | clearer what is being named, and are more consistent with 136 | `Rule.Chain`, and provide more redundancy for distinguishing set 137 | elements from map elements. 138 | 139 | - Fixed serialization of map elements with a comments. 140 | 141 | - Rewrote `ListElements` and `ListRules` to use `nft -j`, for easier / 142 | more reliable parsing. But this meant that `ListRules` no longer 143 | returns the actual text of the rule. 144 | 145 | ## v0.0.8 146 | 147 | - Fixed `Fake.List` / `Fake.ListRules` / `Fake.ListElements` to return 148 | errors that would be properly recognized by 149 | `IsNotFound`/`IsAlreadyExists`. 150 | 151 | ## v0.0.7 152 | 153 | - Implemented `tx.Create`, `tx.Insert`, `tx.Replace` 154 | 155 | - Replaced `tx.AddRule` with the `Concat` function 156 | 157 | ## v0.0.6 158 | 159 | - Added `IsNotFound` and `IsAlreadyExists` error-checking functions 160 | 161 | ## v0.0.5 162 | 163 | - Moved `Define` from `Transaction` to `Interface` 164 | 165 | ## v0.0.3, v0.0.4 166 | 167 | - Improvements to `Fake` to handle `Rule` and `Element` 168 | deletion/overwrite. 169 | 170 | - Added `ListRules` and `ListElements` 171 | 172 | - (The `v0.0.3` and `v0.0.4` tags are identical.) 173 | 174 | ## v0.0.2 175 | 176 | - Made `Interface` be specific to a single family and table. (Before, 177 | that was specified at the `Transaction` level.) 178 | 179 | ## v0.0.1 180 | 181 | - Initial "release" 182 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Welcome to Kubernetes. We are excited about the prospect of you joining our [community](https://git.k8s.io/community)! The Kubernetes community abides by the CNCF [code of conduct](code-of-conduct.md). Here is an excerpt: 4 | 5 | _As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities._ 6 | 7 | ## Getting Started 8 | 9 | We have full documentation on how to get started contributing here: 10 | 11 | 14 | 15 | - [Contributor License Agreement](https://git.k8s.io/community/CLA.md) - Kubernetes projects require that you sign a Contributor License Agreement (CLA) before we can accept your pull requests 16 | - [Kubernetes Contributor Guide](https://k8s.dev/guide) - Main contributor documentation, or you can just jump directly to the [contributing page](https://k8s.dev/docs/guide/contributing/) 17 | - [Contributor Cheat Sheet](https://k8s.dev/cheatsheet) - Common resources for existing developers 18 | 19 | ## Mentorship 20 | 21 | - [Mentoring Initiatives](https://k8s.dev/community/mentoring) - We have a diverse set of mentorship programs available that are always looking for volunteers! 22 | 23 | ## Contact Information 24 | 25 | knftables is maintained by [Kubernetes SIG Network](https://github.com/kubernetes/community/tree/master/sig-network). 26 | 27 | - [sig-network slack channel](https://kubernetes.slack.com/messages/sig-network) 28 | - [kubernetes-sig-network mailing list](https://groups.google.com/forum/#!forum/kubernetes-sig-network) 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright 2023 The Kubernetes Authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | all build: 16 | echo "Usage:" 17 | echo "make test - run unit tests" 18 | echo "make update - run gofmt, etc" 19 | echo "make verify - run golangci, etc" 20 | 21 | clean: 22 | 23 | test: 24 | ./hack/test.sh 25 | 26 | update: 27 | ./hack/update.sh 28 | 29 | verify: 30 | ./hack/verify.sh 31 | 32 | .PHONY: all build clean test update verify 33 | -------------------------------------------------------------------------------- /OWNERS: -------------------------------------------------------------------------------- 1 | # See the OWNERS docs at https://go.k8s.io/owners 2 | 3 | reviewers: 4 | - aojea 5 | - danwinship 6 | approvers: 7 | - danwinship 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # knftables: a golang nftables library 2 | 3 | This is a library for using nftables from Go. 4 | 5 | It is not intended to support arbitrary use cases, but instead 6 | specifically focuses on supporting Kubernetes components which are 7 | using nftables in the way that nftables is supposed to be used (as 8 | opposed to using nftables in a naively-translated-from-iptables way, 9 | or using nftables to do totally valid things that aren't the sorts of 10 | things Kubernetes components are likely to need to do; see the 11 | "[iptables porting](./docs/iptables-porting.md)" doc for more thoughts 12 | on porting old iptables-based components to nftables.) 13 | 14 | knftables is still under development and is not yet API stable. (See the 15 | section on "Possible future changes" below.) 16 | 17 | The library is implemented as a wrapper around the `nft` CLI, because 18 | the CLI API is the only well-documented interface to nftables. 19 | Although it would be possible to use netlink directly (and some other 20 | golang-based nftables libraries do this), that would result in an API 21 | that is quite different from all documented examples of nftables usage 22 | (e.g. the man pages and the [nftables wiki](http://wiki.nftables.org/)) 23 | because there is no easy way to convert the "standard" representation 24 | of nftables rules into the netlink form. 25 | 26 | (Actually, it's not quite true that there's no other usable API: the 27 | `nft` CLI is just a thin wrapper around `libnftables`, and it would be 28 | possible for knftables to use cgo to invoke that library instead of 29 | using an external binary. However, this would be harder to build and 30 | ship, so I'm not bothering with that for now. But this could be done 31 | in the future without needing to change knftables's API.) 32 | 33 | knftables requires nft version 1.0.1 or later, because earlier 34 | versions would download and process the entire ruleset regardless of 35 | what you were doing, which, besides being pointlessly inefficient, 36 | means that in some cases, other people using new features in _their_ 37 | tables could prevent you from modifying _your_ table. (In particular, 38 | a change in how some rules are generated starting in nft 1.0.3 39 | triggers a crash in nft 0.9.9 and earlier, _even if you aren't looking 40 | at the table containing that rule_.) 41 | 42 | ## Usage 43 | 44 | Create an `Interface` object to manage operations on a single nftables 45 | table: 46 | 47 | ```golang 48 | nft, err := knftables.New(knftables.IPv4Family, "my-table") 49 | if err != nil { 50 | return fmt.Errorf("no nftables support: %v", err) 51 | } 52 | ``` 53 | 54 | (If you want to operate on multiple tables or multiple nftables 55 | families, you will need separate `Interface` objects for each. If you 56 | need to check whether the system supports an nftables feature as with 57 | `nft --check`, use `nft.Check()`, which works the same as `nft.Run()` 58 | below.) 59 | 60 | You can use the `List`, `ListRules`, and `ListElements` methods on the 61 | `Interface` to check if objects exist. `List` returns the names of 62 | `"chains"`, `"sets"`, or `"maps"` in the table, while `ListElements` 63 | returns `Element` objects and `ListRules` returns *partial* `Rule` 64 | objects. 65 | 66 | ```golang 67 | chains, err := nft.List(ctx, "chains") 68 | if err != nil { 69 | return fmt.Errorf("could not list chains: %v", err) 70 | } 71 | 72 | FIXME 73 | 74 | elements, err := nft.ListElements(ctx, "map", "mymap") 75 | if err != nil { 76 | return fmt.Errorf("could not list map elements: %v", err) 77 | } 78 | 79 | FIXME 80 | ``` 81 | 82 | To make changes, create a `Transaction`, add the appropriate 83 | operations to the transaction, and then call `nft.Run` on it: 84 | 85 | ```golang 86 | tx := nft.NewTransaction() 87 | 88 | tx.Add(&knftables.Chain{ 89 | Name: "mychain", 90 | Comment: knftables.PtrTo("this is my chain"), 91 | }) 92 | tx.Flush(&knftables.Chain{ 93 | Name: "mychain", 94 | }) 95 | 96 | var destIP net.IP 97 | var destPort uint16 98 | ... 99 | tx.Add(&knftables.Rule{ 100 | Chain: "mychain", 101 | Rule: knftables.Concat( 102 | "ip daddr", destIP, 103 | "ip protocol", "tcp", 104 | "th port", destPort, 105 | "jump", destChain, 106 | ) 107 | }) 108 | 109 | err := nft.Run(context, tx) 110 | ``` 111 | 112 | If any operation in the transaction would fail, then `Run()` will 113 | return an error and the entire transaction will be ignored. You can 114 | use the `knftables.IsNotFound()` and `knftables.IsAlreadyExists()` 115 | methods to check for those well-known error types. In a large 116 | transaction, there is no supported way to determine exactly which 117 | operation failed. 118 | 119 | ## `knftables.Transaction` operations 120 | 121 | `knftables.Transaction` operations correspond to the top-level commands 122 | in the `nft` binary. Currently-supported operations are: 123 | 124 | - `tx.Add()`: adds an object, which may already exist, as with `nft add` 125 | - `tx.Create()`: creates an object, which must not already exist, as with `nft create` 126 | - `tx.Flush()`: flushes the contents of a table/chain/set/map, as with `nft flush` 127 | - `tx.Delete()`: deletes an object, as with `nft delete` 128 | - `tx.Insert()`: inserts a rule before another rule, as with `nft insert rule` 129 | - `tx.Replace()`: replaces a rule, as with `nft replace rule` 130 | 131 | ## Objects 132 | 133 | The `Transaction` methods take arguments of type `knftables.Object`. 134 | The currently-supported objects are: 135 | 136 | - `Table` 137 | - `Flowtable` 138 | - `Chain` 139 | - `Rule` 140 | - `Set` 141 | - `Map` 142 | - `Element` 143 | 144 | Optional fields in objects can be filled in with the help of the 145 | `PtrTo()` function, which just returns a pointer to its argument. 146 | 147 | `Concat()` can be used to concatenate a series of strings, `[]string` 148 | arrays, and other arguments (including numbers, `net.IP`s / 149 | `net.IPNet`s, and anything else that can be formatted usefully via 150 | `fmt.Sprintf("%s")`) together into a single string. This is often 151 | useful when constructing `Rule`s. 152 | 153 | ## `knftables.Fake` 154 | 155 | There is a fake (in-memory) implementation of `knftables.Interface` 156 | for use in unit tests. Use `knftables.NewFake()` instead of 157 | `knftables.New()` to create it, and then it should work mostly the 158 | same. See `fake.go` for more details of the public APIs for examining 159 | the current state of the fake nftables database. 160 | 161 | ## Missing APIs 162 | 163 | Various top-level object types are not yet supported (notably the 164 | "stateful objects" like `counter`). 165 | 166 | Most IPTables libraries have an API for "add this rule only if it 167 | doesn't already exist", but that does not seem as useful in nftables 168 | (or at least "in nftables as used by Kubernetes-ish components that 169 | aren't just blindly copying over old iptables APIs"), because chains 170 | tend to have static rules and dynamic sets/maps, rather than having 171 | dynamic rules. If you aren't sure if a chain has the correct rules, 172 | you can just `Flush` it and recreate all of the rules. 173 | 174 | The "destroy" (delete-without-ENOENT) command that exists in newer 175 | versions of `nft` is not currently supported because it would be 176 | unexpectedly heavyweight to emulate on systems that don't have it, so 177 | it is better (for now) to force callers to implement it by hand. 178 | 179 | `ListRules` returns `Rule` objects without the `Rule` field filled in, 180 | because it uses the JSON API to list the rules, but there is no easy 181 | way to convert the JSON rule representation back into plaintext form. 182 | This means that it is only useful when either (a) you know the order 183 | of the rules in the chain, but want to know their handles, or (b) you 184 | can recognize the rules you are looking for by their comments, rather 185 | than the rule bodies. 186 | 187 | ## Possible future changes 188 | 189 | ### `nft` output parsing 190 | 191 | `nft`'s output is documented and standardized, so it ought to be 192 | possible for us to extract better error messages in the event of a 193 | transaction failure. 194 | 195 | Additionally, if we used the `--echo` (`-e`) and `--handle` (`-a`) 196 | flags, we could learn the handles associated with newly-created 197 | objects in a transaction, and return these to the caller somehow. 198 | (E.g., by setting the `Handle` field in the object that had been 199 | passed to `tx.Add` when the transaction is run.) 200 | 201 | (For now, `ListRules` fills in the handles of the rules it returns, so 202 | it's possible to find out a rule's handle after the fact that way. For 203 | other supported object types, either handles don't exist (`Element`) 204 | or you don't really need to know their handles because it's possible 205 | to delete by name instead (`Table`, `Chain`, `Set`, `Map`).) 206 | 207 | ### List APIs 208 | 209 | The fact that `List` works completely differently from `ListRules` and 210 | `ListElements` is a historical artifact. 211 | 212 | I would like to have a single function 213 | 214 | ```golang 215 | List[T Object](ctx context.Context, template T) ([]T, error) 216 | ``` 217 | 218 | So you could say 219 | 220 | ```golang 221 | elements, err := nft.List(ctx, &knftables.Element{Set: "myset"}) 222 | ``` 223 | 224 | to list the elements of "myset". But this doesn't actually compile 225 | ("`syntax error: method must have no type parameters`") because 226 | allowing that would apparently introduce extremely complicated edge 227 | cases in Go generics. 228 | 229 | ### Set/map type representation 230 | 231 | There is currently an annoying asymmetry in the representation of 232 | concatenated types between `Set`/`Map` and `Element`, where the former 233 | uses a string containing `nft` syntax, and the latter uses an array: 234 | 235 | ```golang 236 | tx.Add(&knftables.Set{ 237 | Name: "firewall", 238 | Type: "ipv4_addr . inet_proto . inet_service", 239 | }) 240 | tx.Add(&knftables.Element{ 241 | Set: "firewall", 242 | Key: []string{"10.1.2.3", "tcp", "80"}, 243 | }) 244 | ``` 245 | 246 | This will probably be fixed at some point, which may result in a 247 | change to how the `type` vs `typeof` distinction is handled as well. 248 | 249 | ### Optimization and rule representation 250 | 251 | We will need to optimize the performance of large transactions. One 252 | change that is likely is to avoid pre-concatenating rule elements in 253 | cases like: 254 | 255 | ```golang 256 | tx.Add(&knftables.Rule{ 257 | Chain: "mychain", 258 | Rule: knftables.Concat( 259 | "ip daddr", destIP, 260 | "ip protocol", "tcp", 261 | "th port", destPort, 262 | "jump", destChain, 263 | ) 264 | }) 265 | ``` 266 | 267 | This will presumably require a change to `knftables.Rule` and/or 268 | `knftables.Concat()` but I'm not sure exactly what it will be. 269 | 270 | ## Community, discussion, contribution, and support 271 | 272 | knftables is maintained by [Kubernetes SIG Network](https://github.com/kubernetes/community/tree/master/sig-network). 273 | 274 | - [sig-network slack channel](https://kubernetes.slack.com/messages/sig-network) 275 | - [kubernetes-sig-network mailing list](https://groups.google.com/forum/#!forum/kubernetes-sig-network) 276 | 277 | See [`CONTRIBUTING.md`](CONTRIBUTING.md) for more information about 278 | contributing. Participation in the Kubernetes community is governed by 279 | the [Kubernetes Code of Conduct](code-of-conduct.md). 280 | -------------------------------------------------------------------------------- /SECURITY_CONTACTS: -------------------------------------------------------------------------------- 1 | # Defined below are the security contacts for this repo. 2 | # 3 | # They are the contact point for the Security Response Committee to reach out 4 | # to for triaging and handling of incoming issues. 5 | # 6 | # The below names agree to abide by the 7 | # [Embargo Policy](https://git.k8s.io/security/private-distributors-list.md#embargo-policy) 8 | # and will be removed and replaced if they violate that agreement. 9 | # 10 | # DO NOT REPORT SECURITY VULNERABILITIES DIRECTLY TO THESE NAMES, FOLLOW THE 11 | # INSTRUCTIONS AT https://kubernetes.io/security/ 12 | 13 | danwinship 14 | -------------------------------------------------------------------------------- /code-of-conduct.md: -------------------------------------------------------------------------------- 1 | # Kubernetes Community Code of Conduct 2 | 3 | Please refer to our [Kubernetes Community Code of Conduct](https://git.k8s.io/community/code-of-conduct.md) 4 | -------------------------------------------------------------------------------- /docs/iptables-porting.md: -------------------------------------------------------------------------------- 1 | # Porting iptables-based components to nftables 2 | 3 | Although it's possible to port an iptables-based component to nftables 4 | in a mostly "dumb" way that just translates the iptables chains to 5 | nftables, this isn't really encouraged, and knftables isn't designed 6 | to necessarily make that easy. 7 | 8 | (There is no particular organization to this document; it's basically 9 | a braindump.) 10 | 11 | ## NFTables tables are private 12 | 13 | In iptables, everyone has to share the same `filter`, `nat`, `raw`, 14 | and `mangle` tables, and has to figure out how to not interfere with 15 | each other. 16 | 17 | In nftables, everyone is expected to create their own table, and _not 18 | touch_ anyone else's tables. You use the `priority` levels of base 19 | chains to control what happens before and after what. 20 | 21 | ## Sets and Maps 22 | 23 | NFTables sets and maps are awesome, and you should try to use them. 24 | 25 | IPTables style: 26 | 27 | ``` 28 | -A KUBE-SERVICES -m comment --comment "ns1/svc1:p80 cluster IP" -m tcp -p tcp -d 172.30.0.41 --dport 80 -j KUBE-SVC-XPGD46QRK7WJZT7O 29 | -A KUBE-SERVICES -m comment --comment "ns2/svc2:p80 cluster IP" -m tcp -p tcp -d 172.30.0.42 --dport 80 -j KUBE-SVC-GNZBNJ2PO5MGZ6GT 30 | -A KUBE-SERVICES -m comment --comment "ns2/svc2:p80 external IP" -m tcp -p tcp -d 192.168.99.22 --dport 80 -j KUBE-EXT-GNZBNJ2PO5MGZ6GT 31 | -A KUBE-SERVICES -m comment --comment "ns2/svc2:p80 loadbalancer IP" -m tcp -p tcp -d 1.2.3.4 --dport 80 -j KUBE-EXT-GNZBNJ2PO5MGZ6GT 32 | -A KUBE-SERVICES -m comment --comment "ns3/svc3:p80 cluster IP" -m tcp -p tcp -d 172.30.0.43 --dport 80 -j KUBE-SVC-X27LE4BHSL4DOUIK 33 | -A KUBE-SERVICES -m comment --comment "ns4/svc4:p80 cluster IP" -m tcp -p tcp -d 172.30.0.44 --dport 80 -j KUBE-SVC-4SW47YFZTEDKD3PK 34 | -A KUBE-SERVICES -m comment --comment "ns4/svc4:p80 external IP" -m tcp -p tcp -d 192.168.99.33 --dport 80 -j KUBE-EXT-4SW47YFZTEDKD3PK 35 | -A KUBE-SERVICES -m comment --comment "ns5/svc5:p80 cluster IP" -m tcp -p tcp -d 172.30.0.45 --dport 80 -j KUBE-SVC-NUKIZ6OKUXPJNT4C 36 | -A KUBE-SERVICES -m comment --comment "ns5/svc5:p80 loadbalancer IP" -m tcp -p tcp -d 5.6.7.8 --dport 80 -j KUBE-FW-NUKIZ6OKUXPJNT4C 37 | ``` 38 | 39 | NFTables style: 40 | 41 | ``` 42 | add chain ip kube-proxy services 43 | add rule ip kube-proxy services ip daddr . meta l4proto . th dport vmap @service-ips 44 | 45 | add element ip kube-proxy service-ips { 172.30.0.41 . tcp . 80 : goto service-ULMVA6XW-ns1/svc1/tcp/p80 } 46 | add element ip kube-proxy service-ips { 172.30.0.42 . tcp . 80 : goto service-42NFTM6N-ns2/svc2/tcp/p80 } 47 | add element ip kube-proxy service-ips { 192.168.99.22 . tcp . 80 : goto external-42NFTM6N-ns2/svc2/tcp/p80 } 48 | add element ip kube-proxy service-ips { 1.2.3.4 . tcp . 80 : goto external-42NFTM6N-ns2/svc2/tcp/p80 } 49 | add element ip kube-proxy service-ips { 172.30.0.43 . tcp . 80 : goto service-4AT6LBPK-ns3/svc3/tcp/p80 } 50 | add element ip kube-proxy service-ips { 172.30.0.44 . tcp . 80 : goto service-LAUZTJTB-ns4/svc4/tcp/p80 } 51 | add element ip kube-proxy service-ips { 192.168.99.33 . tcp . 80 : goto external-LAUZTJTB-ns4/svc4/tcp/p80 } 52 | add element ip kube-proxy service-ips { 172.30.0.45 . tcp . 80 : goto service-HVFWP5L3-ns5/svc5/tcp/p80 } 53 | add element ip kube-proxy service-ips { 5.6.7.8 . tcp . 80 : goto external-HVFWP5L3-ns5/svc5/tcp/p80 } 54 | ``` 55 | 56 | The nftables ruleset is O(1)-ish; there is a single rule, regardless 57 | of how many services you have. The iptables ruleset is O(n); the more 58 | services you have, the more rules netfilter has to run against each 59 | packet. 60 | 61 | ## Static vs dynamic rule sets 62 | 63 | Related to the above, iptables-based components tend to need to 64 | frequently add and delete individual rules. In nftables, it is more 65 | common to have a static set of rules (e.g., the single map lookup rule 66 | above), and have dynamically modified sets and maps. 67 | 68 | In iptables, you can delete a rule by using the same syntax you used 69 | to create it. In nftables, this isn't possible; if you want to delete 70 | a rule, you need to know the current index or the static handle of the 71 | rule. This makes dynamically altering rules slightly more complicated 72 | (and, at the time of writing, knftables doesn't provide any way to 73 | learn the handle of a rule at the time the rule is created). 74 | 75 | As a result of the above (and the fact that you can safely assume that 76 | other components aren't adding rules to your tables), in idiomatic 77 | knftables usage, when you aren't sure that the current state of the 78 | chain is correct, it tends to be more common to flush-and-recreate the 79 | chain rather than checking whether individual rules exist in it and are 80 | correct. 81 | 82 | ## Use of comments 83 | 84 | In iptables, comments only exist on rules. In nftables, you can add 85 | comments to: 86 | 87 | - rules 88 | - set/map elements 89 | - chains 90 | - sets 91 | - maps 92 | - tables 93 | 94 | When porting kube-proxy, we found that in many places where kube-proxy 95 | had per-rule comments, we really only needed per-chain comments, which 96 | explained all of the rules in the chain. 97 | 98 | (Additionally, we ended up getting rid of a lot of comments just 99 | because we use much longer and more-self-documenting chain names in 100 | the nftables kube-proxy backend, as seen above. (The maximum length of 101 | an nftables chain name is 256, as compared to iptables's 32.)) 102 | 103 | ## The `inet`, `ip`, and `ip6` tables 104 | 105 | For dual-stack rulesets, nftables provides two options: 106 | 107 | - Put IPv4 rules in an `ip`-family table and IPv6 rules in an `ip6`-family table. 108 | - Put all rules in an `inet`-family table. 109 | 110 | For kube-proxy, the architecture of kube-proxy meant that the split 111 | `ip`/`ip6` option made much more sense. But the split option often 112 | ends up making sense for other components too, because you can't have 113 | sets/maps that hold/match both IPv4 and IPv6 addresses. If you need to 114 | have rules that match particular IPs against a set or map, then you 115 | need to have per-IP-family sets/map regardless of whether the rules 116 | are in `ip`/`ip6` or `inet`. 117 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package knftables 18 | 19 | import ( 20 | "errors" 21 | "fmt" 22 | "os/exec" 23 | "strings" 24 | "syscall" 25 | ) 26 | 27 | type nftablesError struct { 28 | wrapped error 29 | msg string 30 | errno syscall.Errno 31 | } 32 | 33 | // wrapError wraps an error resulting from running nft 34 | func wrapError(err error) error { 35 | nerr := &nftablesError{wrapped: err, msg: err.Error()} 36 | ee := &exec.ExitError{} 37 | if errors.As(err, &ee) { 38 | if len(ee.Stderr) > 0 { 39 | nerr.msg = string(ee.Stderr) 40 | eol := strings.Index(nerr.msg, "\n") 41 | // The nft binary does not call setlocale() and so will return 42 | // English error strings regardless of the locale. 43 | enoent := strings.Index(nerr.msg, "No such file or directory") 44 | eexist := strings.Index(nerr.msg, "File exists") 45 | if enoent != -1 && (enoent < eol || eol == -1) { 46 | nerr.errno = syscall.ENOENT 47 | } else if eexist != -1 && (eexist < eol || eol == -1) { 48 | nerr.errno = syscall.EEXIST 49 | } 50 | } 51 | } 52 | return nerr 53 | } 54 | 55 | // notFoundError returns an nftablesError with the given message for which IsNotFound will 56 | // return true. 57 | func notFoundError(format string, args ...interface{}) error { 58 | return &nftablesError{msg: fmt.Sprintf(format, args...), errno: syscall.ENOENT} 59 | } 60 | 61 | // existsError returns an nftablesError with the given message for which IsAlreadyExists 62 | // will return true. 63 | func existsError(format string, args ...interface{}) error { 64 | return &nftablesError{msg: fmt.Sprintf(format, args...), errno: syscall.EEXIST} 65 | } 66 | 67 | func (nerr *nftablesError) Error() string { 68 | return nerr.msg 69 | } 70 | 71 | func (nerr *nftablesError) Unwrap() error { 72 | return nerr.wrapped 73 | } 74 | 75 | // IsNotFound tests if err corresponds to an nftables "not found" error of any sort. 76 | // (e.g., in response to a "delete rule" command, this might indicate that the rule 77 | // doesn't exist, or the chain doesn't exist, or the table doesn't exist.) 78 | func IsNotFound(err error) bool { 79 | var nerr *nftablesError 80 | if errors.As(err, &nerr) { 81 | return nerr.errno == syscall.ENOENT 82 | } 83 | return false 84 | } 85 | 86 | // IsAlreadyExists tests if err corresponds to an nftables "already exists" error (e.g. 87 | // when doing a "create" rather than an "add"). 88 | func IsAlreadyExists(err error) bool { 89 | var nerr *nftablesError 90 | if errors.As(err, &nerr) { 91 | return nerr.errno == syscall.EEXIST 92 | } 93 | return false 94 | } 95 | -------------------------------------------------------------------------------- /error_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package knftables 18 | 19 | import ( 20 | "fmt" 21 | "os/exec" 22 | "testing" 23 | ) 24 | 25 | func mkExecError(stderr string) error { 26 | return wrapError(&exec.ExitError{Stderr: []byte(stderr)}) 27 | } 28 | 29 | func TestError(t *testing.T) { 30 | for _, tc := range []struct { 31 | name string 32 | err error 33 | isNotFound bool 34 | isExists bool 35 | }{ 36 | { 37 | name: "generic doesn't exist", 38 | err: mkExecError("Error: No such file or directory\ndelete element ip nosuchtable set {10.0.0.1}\n ^^^^^^^^^^^\n"), 39 | isNotFound: true, 40 | isExists: false, 41 | }, 42 | { 43 | name: "doesn't exist with suggestion", 44 | err: mkExecError("Error: No such file or directory; did you mean table ‘foo’ in family ip?\nblah blah blah\n"), 45 | isNotFound: true, 46 | isExists: false, 47 | }, 48 | { 49 | name: "already exists", 50 | err: mkExecError("Error: Could not process rule: File exists\ncreate table foo\n ^^^"), 51 | isNotFound: false, 52 | isExists: true, 53 | }, 54 | { 55 | name: "wrapped doesn't exist", 56 | err: fmt.Errorf("oh my! %w", mkExecError("Error: No such file or directory")), 57 | isNotFound: true, 58 | isExists: false, 59 | }, 60 | { 61 | name: "misc error", 62 | err: mkExecError("Error: syntax error, unexpected string, expecting '{' or '$'"), 63 | isNotFound: false, 64 | isExists: false, 65 | }, 66 | { 67 | name: "misleading misc error", 68 | err: mkExecError("Error: syntax error, unexpected comment\nadd rule foo chain1 comment \"No such file or directory\" drop\n ^^^^^^^"), 69 | isNotFound: false, 70 | isExists: false, 71 | }, 72 | { 73 | name: "not an ExecError, so not interpreted", 74 | err: wrapError(fmt.Errorf("Error: No such file or directory\ndelete element ip nosuchtable set {10.0.0.1}\n ^^^^^^^^^^^\n")), //nolint:revive 75 | isNotFound: false, 76 | isExists: false, 77 | }, 78 | { 79 | name: "fake not found", 80 | err: notFoundError("not found"), 81 | isNotFound: true, 82 | isExists: false, 83 | }, 84 | { 85 | name: "fake exists", 86 | err: existsError("already exists"), 87 | isNotFound: false, 88 | isExists: true, 89 | }, 90 | } { 91 | t.Run(tc.name, func(t *testing.T) { 92 | if IsNotFound(tc.err) != tc.isNotFound { 93 | t.Errorf("expected IsNotFound %v, got %v", tc.isNotFound, IsNotFound(tc.err)) 94 | } 95 | if IsAlreadyExists(tc.err) != tc.isExists { 96 | t.Errorf("expected IsAlreadyExists %v, got %v", tc.isExists, IsAlreadyExists(tc.err)) 97 | } 98 | }) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /exec.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package knftables 18 | 19 | import ( 20 | "os/exec" 21 | ) 22 | 23 | // execer is a mockable wrapper around os/exec. 24 | type execer interface { 25 | // LookPath wraps exec.LookPath 26 | LookPath(file string) (string, error) 27 | 28 | // Run runs cmd as with cmd.Output(). If an error occurs, and the process outputs 29 | // stderr, then that output will be returned in the error. 30 | Run(cmd *exec.Cmd) (string, error) 31 | } 32 | 33 | // realExec implements execer by actually using os/exec 34 | type realExec struct{} 35 | 36 | // LookPath is part of execer 37 | func (realExec) LookPath(file string) (string, error) { 38 | return exec.LookPath(file) 39 | } 40 | 41 | // Run is part of execer 42 | func (realExec) Run(cmd *exec.Cmd) (string, error) { 43 | out, err := cmd.Output() 44 | if err != nil { 45 | err = wrapError(err) 46 | } 47 | return string(out), err 48 | } 49 | -------------------------------------------------------------------------------- /exec_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package knftables 18 | 19 | import ( 20 | "bytes" 21 | "fmt" 22 | "io" 23 | "os/exec" 24 | "reflect" 25 | "strings" 26 | "testing" 27 | ) 28 | 29 | // fakeExec is a mockable implementation of execer for unit tests 30 | type fakeExec struct { 31 | t *testing.T 32 | 33 | // missingBinaries is the set of binaries for which LookPath should fail 34 | missingBinaries map[string]bool 35 | 36 | // expected is the list of expected Run calls 37 | expected []expectedCmd 38 | 39 | // matched is used internally, to keep track of where we are in expected 40 | matched int 41 | } 42 | 43 | func newFakeExec(t *testing.T) *fakeExec { 44 | return &fakeExec{t: t, missingBinaries: make(map[string]bool)} 45 | } 46 | 47 | func (fe *fakeExec) LookPath(file string) (string, error) { 48 | if fe.missingBinaries[file] { 49 | return "", &exec.Error{Name: file, Err: exec.ErrNotFound} 50 | } 51 | return "/" + file, nil 52 | } 53 | 54 | // expectedCmd details one expected fakeExec Cmd 55 | type expectedCmd struct { 56 | args []string 57 | stdin string 58 | stdout string 59 | err error 60 | } 61 | 62 | func (fe *fakeExec) Run(cmd *exec.Cmd) (string, error) { 63 | if fe.t.Failed() { 64 | return "", fmt.Errorf("unit test failed") 65 | } 66 | 67 | if len(fe.expected) == fe.matched { 68 | fe.t.Errorf("ran out of commands before executing %v", cmd.Args) 69 | return "", fmt.Errorf("unit test failed") 70 | } 71 | expected := &fe.expected[fe.matched] 72 | fe.matched++ 73 | 74 | if !reflect.DeepEqual(expected.args, cmd.Args) { 75 | fe.t.Errorf("incorrect arguments: expected %v, got %v", expected.args, cmd.Args) 76 | return "", fmt.Errorf("unit test failed") 77 | } 78 | 79 | var stdin string 80 | if cmd.Stdin != nil { 81 | inBytes, _ := io.ReadAll(cmd.Stdin) 82 | stdin = string(inBytes) 83 | } 84 | if expected.stdin != stdin { 85 | fe.t.Errorf("incorrect stdin: expected %q, got %q", expected.stdin, stdin) 86 | return "", fmt.Errorf("unit test failed") 87 | } 88 | 89 | return expected.stdout, expected.err 90 | } 91 | 92 | type execTestCase struct { 93 | name string 94 | command []string 95 | stdin string 96 | expectedOut string 97 | expectedErr string 98 | } 99 | 100 | var execTestCases = []execTestCase{ 101 | { 102 | name: "/bin/true", 103 | command: []string{"/bin/true"}, 104 | expectedOut: "", 105 | }, 106 | { 107 | name: "/bin/false", 108 | command: []string{"/bin/false"}, 109 | expectedOut: "", 110 | expectedErr: "exit status 1", 111 | }, 112 | { 113 | name: "echo", 114 | command: []string{"echo", "one", "two", "three"}, 115 | expectedOut: "one two three\n", 116 | }, 117 | { 118 | name: "with stdin", 119 | command: []string{"cat"}, 120 | stdin: "one\ntwo\nthree\n", 121 | expectedOut: "one\ntwo\nthree\n", 122 | }, 123 | { 124 | name: "missing command", 125 | command: []string{"/does/not/exist/command"}, 126 | expectedOut: "", 127 | expectedErr: "no such file or directory", 128 | }, 129 | { 130 | name: "fail", 131 | command: []string{"cat", "."}, 132 | expectedOut: "", 133 | expectedErr: "Is a directory", 134 | }, 135 | } 136 | 137 | func TestRealExec(t *testing.T) { 138 | for _, tc := range execTestCases { 139 | t.Run(tc.name, func(t *testing.T) { 140 | execer := &realExec{} 141 | cmd := exec.Command(tc.command[0], tc.command[1:]...) 142 | if tc.stdin != "" { 143 | cmd.Stdin = bytes.NewBufferString(tc.stdin) 144 | } 145 | out, err := execer.Run(cmd) 146 | if out != tc.expectedOut { 147 | t.Errorf("expected output %q, got %q", tc.expectedOut, out) 148 | } 149 | if err != nil { 150 | if tc.expectedErr == "" { 151 | t.Errorf("expected no error, got %v", err) 152 | } else if !strings.Contains(err.Error(), tc.expectedErr) { 153 | t.Errorf("expected error containing %q, got %v", tc.expectedErr, err) 154 | } 155 | } else if tc.expectedErr != "" { 156 | t.Errorf("expected error containing %q, got no error", tc.expectedErr) 157 | } 158 | }) 159 | } 160 | } 161 | 162 | func TestFakeExec(t *testing.T) { 163 | for _, tc := range execTestCases { 164 | t.Run(tc.name, func(t *testing.T) { 165 | execer := newFakeExec(t) 166 | execer.missingBinaries["/does/not/exist/command"] = true 167 | execer.expected = []expectedCmd{{ 168 | args: tc.command, 169 | stdin: tc.stdin, 170 | stdout: tc.expectedOut, 171 | }} 172 | if tc.expectedErr != "" { 173 | execer.expected[0].err = fmt.Errorf(tc.expectedErr) 174 | } 175 | 176 | cmd := exec.Command(tc.command[0], tc.command[1:]...) 177 | if tc.stdin != "" { 178 | cmd.Stdin = bytes.NewBufferString(tc.stdin) 179 | } 180 | out, err := execer.Run(cmd) 181 | if out != tc.expectedOut { 182 | t.Errorf("expected output %q, got %q", tc.expectedOut, out) 183 | } 184 | if err != nil { 185 | if tc.expectedErr == "" { 186 | t.Errorf("expected no error, got %v", err) 187 | } else if !strings.Contains(err.Error(), tc.expectedErr) { 188 | t.Errorf("expected error containing %q, got %v", tc.expectedErr, err) 189 | } 190 | } else if tc.expectedErr != "" { 191 | t.Errorf("expected error containing %q, got no error", tc.expectedErr) 192 | } 193 | }) 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module sigs.k8s.io/knftables 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/google/go-cmp v0.5.9 7 | github.com/lithammer/dedent v1.1.0 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 2 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 3 | github.com/lithammer/dedent v1.1.0 h1:VNzHMVCBNG1j0fh3OrsFRkVUwStdDArbgBWoPAffktY= 4 | github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= 5 | -------------------------------------------------------------------------------- /hack/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright 2024 The Kubernetes Authors. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -o errexit -o nounset -o pipefail 18 | 19 | go test -race -count=1 . 20 | -------------------------------------------------------------------------------- /hack/tools/.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | timeout: 2m 3 | 4 | linters: 5 | disable-all: true 6 | enable: 7 | # default golangci-lint lints 8 | - errcheck 9 | - gosimple 10 | - govet 11 | - ineffassign 12 | - staticcheck 13 | - typecheck 14 | - unused 15 | 16 | # additional lints 17 | - errorlint 18 | - exportloopref 19 | - gochecknoinits 20 | - gofmt 21 | - misspell 22 | - revive 23 | - unparam 24 | - whitespace 25 | 26 | linters-settings: 27 | errorlint: 28 | errorf: true 29 | -------------------------------------------------------------------------------- /hack/tools/README.md: -------------------------------------------------------------------------------- 1 | This directory contains a stub go module used to track version of development 2 | tools like the Kubernetes code generators. -------------------------------------------------------------------------------- /hack/tools/go.mod: -------------------------------------------------------------------------------- 1 | module sigs.k8s.io/knftables/hack/tools 2 | 3 | go 1.21 4 | 5 | toolchain go1.22.0 6 | 7 | require github.com/golangci/golangci-lint v1.59.0 8 | 9 | require ( 10 | 4d63.com/gocheckcompilerdirectives v1.2.1 // indirect 11 | 4d63.com/gochecknoglobals v0.2.1 // indirect 12 | github.com/4meepo/tagalign v1.3.4 // indirect 13 | github.com/Abirdcfly/dupword v0.0.14 // indirect 14 | github.com/Antonboom/errname v0.1.13 // indirect 15 | github.com/Antonboom/nilnil v0.1.9 // indirect 16 | github.com/Antonboom/testifylint v1.3.0 // indirect 17 | github.com/BurntSushi/toml v1.4.0 // indirect 18 | github.com/Crocmagnon/fatcontext v0.2.2 // indirect 19 | github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 // indirect 20 | github.com/GaijinEntertainment/go-exhaustruct/v3 v3.2.0 // indirect 21 | github.com/Masterminds/semver/v3 v3.2.1 // indirect 22 | github.com/OpenPeeDeeP/depguard/v2 v2.2.0 // indirect 23 | github.com/alecthomas/go-check-sumtype v0.1.4 // indirect 24 | github.com/alexkohler/nakedret/v2 v2.0.4 // indirect 25 | github.com/alexkohler/prealloc v1.0.0 // indirect 26 | github.com/alingse/asasalint v0.0.11 // indirect 27 | github.com/ashanbrown/forbidigo v1.6.0 // indirect 28 | github.com/ashanbrown/makezero v1.1.1 // indirect 29 | github.com/beorn7/perks v1.0.1 // indirect 30 | github.com/bkielbasa/cyclop v1.2.1 // indirect 31 | github.com/blizzy78/varnamelen v0.8.0 // indirect 32 | github.com/bombsimon/wsl/v4 v4.2.1 // indirect 33 | github.com/breml/bidichk v0.2.7 // indirect 34 | github.com/breml/errchkjson v0.3.6 // indirect 35 | github.com/butuzov/ireturn v0.3.0 // indirect 36 | github.com/butuzov/mirror v1.2.0 // indirect 37 | github.com/catenacyber/perfsprint v0.7.1 // indirect 38 | github.com/ccojocar/zxcvbn-go v1.0.2 // indirect 39 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 40 | github.com/charithe/durationcheck v0.0.10 // indirect 41 | github.com/chavacava/garif v0.1.0 // indirect 42 | github.com/ckaznocha/intrange v0.1.2 // indirect 43 | github.com/curioswitch/go-reassign v0.2.0 // indirect 44 | github.com/daixiang0/gci v0.13.4 // indirect 45 | github.com/davecgh/go-spew v1.1.1 // indirect 46 | github.com/denis-tingaikin/go-header v0.5.0 // indirect 47 | github.com/ettle/strcase v0.2.0 // indirect 48 | github.com/fatih/color v1.17.0 // indirect 49 | github.com/fatih/structtag v1.2.0 // indirect 50 | github.com/firefart/nonamedreturns v1.0.5 // indirect 51 | github.com/fsnotify/fsnotify v1.5.4 // indirect 52 | github.com/fzipp/gocyclo v0.6.0 // indirect 53 | github.com/ghostiam/protogetter v0.3.6 // indirect 54 | github.com/go-critic/go-critic v0.11.4 // indirect 55 | github.com/go-toolsmith/astcast v1.1.0 // indirect 56 | github.com/go-toolsmith/astcopy v1.1.0 // indirect 57 | github.com/go-toolsmith/astequal v1.2.0 // indirect 58 | github.com/go-toolsmith/astfmt v1.1.0 // indirect 59 | github.com/go-toolsmith/astp v1.1.0 // indirect 60 | github.com/go-toolsmith/strparse v1.1.0 // indirect 61 | github.com/go-toolsmith/typep v1.1.0 // indirect 62 | github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect 63 | github.com/go-xmlfmt/xmlfmt v1.1.2 // indirect 64 | github.com/gobwas/glob v0.2.3 // indirect 65 | github.com/gofrs/flock v0.8.1 // indirect 66 | github.com/golang/protobuf v1.5.3 // indirect 67 | github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a // indirect 68 | github.com/golangci/gofmt v0.0.0-20231018234816-f50ced29576e // indirect 69 | github.com/golangci/misspell v0.5.1 // indirect 70 | github.com/golangci/modinfo v0.3.4 // indirect 71 | github.com/golangci/plugin-module-register v0.1.1 // indirect 72 | github.com/golangci/revgrep v0.5.3 // indirect 73 | github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed // indirect 74 | github.com/google/go-cmp v0.6.0 // indirect 75 | github.com/gordonklaus/ineffassign v0.1.0 // indirect 76 | github.com/gostaticanalysis/analysisutil v0.7.1 // indirect 77 | github.com/gostaticanalysis/comment v1.4.2 // indirect 78 | github.com/gostaticanalysis/forcetypeassert v0.1.0 // indirect 79 | github.com/gostaticanalysis/nilerr v0.1.1 // indirect 80 | github.com/hashicorp/go-version v1.7.0 // indirect 81 | github.com/hashicorp/hcl v1.0.0 // indirect 82 | github.com/hexops/gotextdiff v1.0.3 // indirect 83 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 84 | github.com/jgautheron/goconst v1.7.1 // indirect 85 | github.com/jingyugao/rowserrcheck v1.1.1 // indirect 86 | github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af // indirect 87 | github.com/jjti/go-spancheck v0.6.1 // indirect 88 | github.com/julz/importas v0.1.0 // indirect 89 | github.com/karamaru-alpha/copyloopvar v1.1.0 // indirect 90 | github.com/kisielk/errcheck v1.7.0 // indirect 91 | github.com/kkHAIKE/contextcheck v1.1.5 // indirect 92 | github.com/kulti/thelper v0.6.3 // indirect 93 | github.com/kunwardeep/paralleltest v1.0.10 // indirect 94 | github.com/kyoh86/exportloopref v0.1.11 // indirect 95 | github.com/lasiar/canonicalheader v1.1.1 // indirect 96 | github.com/ldez/gomoddirectives v0.2.4 // indirect 97 | github.com/ldez/tagliatelle v0.5.0 // indirect 98 | github.com/leonklingele/grouper v1.1.2 // indirect 99 | github.com/lufeee/execinquery v1.2.1 // indirect 100 | github.com/macabu/inamedparam v0.1.3 // indirect 101 | github.com/magiconair/properties v1.8.6 // indirect 102 | github.com/maratori/testableexamples v1.0.0 // indirect 103 | github.com/maratori/testpackage v1.1.1 // indirect 104 | github.com/matoous/godox v0.0.0-20230222163458-006bad1f9d26 // indirect 105 | github.com/mattn/go-colorable v0.1.13 // indirect 106 | github.com/mattn/go-isatty v0.0.20 // indirect 107 | github.com/mattn/go-runewidth v0.0.9 // indirect 108 | github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect 109 | github.com/mgechev/revive v1.3.7 // indirect 110 | github.com/mitchellh/go-homedir v1.1.0 // indirect 111 | github.com/mitchellh/mapstructure v1.5.0 // indirect 112 | github.com/moricho/tparallel v0.3.1 // indirect 113 | github.com/nakabonne/nestif v0.3.1 // indirect 114 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect 115 | github.com/nishanths/exhaustive v0.12.0 // indirect 116 | github.com/nishanths/predeclared v0.2.2 // indirect 117 | github.com/nunnatsa/ginkgolinter v0.16.2 // indirect 118 | github.com/olekukonko/tablewriter v0.0.5 // indirect 119 | github.com/pelletier/go-toml v1.9.5 // indirect 120 | github.com/pelletier/go-toml/v2 v2.2.2 // indirect 121 | github.com/pmezard/go-difflib v1.0.0 // indirect 122 | github.com/polyfloyd/go-errorlint v1.5.1 // indirect 123 | github.com/prometheus/client_golang v1.12.1 // indirect 124 | github.com/prometheus/client_model v0.2.0 // indirect 125 | github.com/prometheus/common v0.32.1 // indirect 126 | github.com/prometheus/procfs v0.7.3 // indirect 127 | github.com/quasilyte/go-ruleguard v0.4.2 // indirect 128 | github.com/quasilyte/go-ruleguard/dsl v0.3.22 // indirect 129 | github.com/quasilyte/gogrep v0.5.0 // indirect 130 | github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect 131 | github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect 132 | github.com/ryancurrah/gomodguard v1.3.2 // indirect 133 | github.com/ryanrolds/sqlclosecheck v0.5.1 // indirect 134 | github.com/sanposhiho/wastedassign/v2 v2.0.7 // indirect 135 | github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 // indirect 136 | github.com/sashamelentyev/interfacebloat v1.1.0 // indirect 137 | github.com/sashamelentyev/usestdlibvars v1.25.0 // indirect 138 | github.com/securego/gosec/v2 v2.20.1-0.20240525090044-5f0084eb01a9 // indirect 139 | github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c // indirect 140 | github.com/sirupsen/logrus v1.9.3 // indirect 141 | github.com/sivchari/containedctx v1.0.3 // indirect 142 | github.com/sivchari/tenv v1.7.1 // indirect 143 | github.com/sonatard/noctx v0.0.2 // indirect 144 | github.com/sourcegraph/go-diff v0.7.0 // indirect 145 | github.com/spf13/afero v1.11.0 // indirect 146 | github.com/spf13/cast v1.5.0 // indirect 147 | github.com/spf13/cobra v1.7.0 // indirect 148 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 149 | github.com/spf13/pflag v1.0.5 // indirect 150 | github.com/spf13/viper v1.12.0 // indirect 151 | github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect 152 | github.com/stbenjam/no-sprintf-host-port v0.1.1 // indirect 153 | github.com/stretchr/objx v0.5.2 // indirect 154 | github.com/stretchr/testify v1.9.0 // indirect 155 | github.com/subosito/gotenv v1.4.1 // indirect 156 | github.com/t-yuki/gocover-cobertura v0.0.0-20180217150009-aaee18c8195c // indirect 157 | github.com/tdakkota/asciicheck v0.2.0 // indirect 158 | github.com/tetafro/godot v1.4.16 // indirect 159 | github.com/timakin/bodyclose v0.0.0-20230421092635-574207250966 // indirect 160 | github.com/timonwong/loggercheck v0.9.4 // indirect 161 | github.com/tomarrell/wrapcheck/v2 v2.8.3 // indirect 162 | github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect 163 | github.com/ultraware/funlen v0.1.0 // indirect 164 | github.com/ultraware/whitespace v0.1.1 // indirect 165 | github.com/uudashr/gocognit v1.1.2 // indirect 166 | github.com/xen0n/gosmopolitan v1.2.2 // indirect 167 | github.com/yagipy/maintidx v1.0.0 // indirect 168 | github.com/yeya24/promlinter v0.3.0 // indirect 169 | github.com/ykadowak/zerologlint v0.1.5 // indirect 170 | gitlab.com/bosi/decorder v0.4.2 // indirect 171 | go-simpler.org/musttag v0.12.2 // indirect 172 | go-simpler.org/sloglint v0.7.0 // indirect 173 | go.uber.org/atomic v1.7.0 // indirect 174 | go.uber.org/automaxprocs v1.5.3 // indirect 175 | go.uber.org/multierr v1.6.0 // indirect 176 | go.uber.org/zap v1.24.0 // indirect 177 | golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc // indirect 178 | golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f // indirect 179 | golang.org/x/mod v0.17.0 // indirect 180 | golang.org/x/sync v0.7.0 // indirect 181 | golang.org/x/sys v0.20.0 // indirect 182 | golang.org/x/text v0.15.0 // indirect 183 | golang.org/x/tools v0.21.0 // indirect 184 | google.golang.org/protobuf v1.33.0 // indirect 185 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect 186 | gopkg.in/ini.v1 v1.67.0 // indirect 187 | gopkg.in/yaml.v2 v2.4.0 // indirect 188 | gopkg.in/yaml.v3 v3.0.1 // indirect 189 | honnef.co/go/tools v0.4.7 // indirect 190 | mvdan.cc/gofumpt v0.6.0 // indirect 191 | mvdan.cc/unparam v0.0.0-20240427195214-063aff900ca1 // indirect 192 | ) 193 | -------------------------------------------------------------------------------- /hack/tools/tools.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | // +build tools 3 | 4 | /* 5 | Package tools is used to track binary dependencies with go modules 6 | https://github.com/golang/go/wiki/Modules#how-can-i-track-tool-dependencies-for-a-module 7 | */ 8 | package tools 9 | 10 | import ( 11 | _ "github.com/golangci/golangci-lint/cmd/golangci-lint" 12 | ) 13 | -------------------------------------------------------------------------------- /hack/update.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright 2024 The Kubernetes Authors. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -o errexit -o nounset -o pipefail 18 | 19 | GOFMT_FILES=*.go 20 | 21 | gofmt -s -w ${GOFMT_FILES} 22 | -------------------------------------------------------------------------------- /hack/verify.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright 2024 The Kubernetes Authors. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -o errexit -o nounset -o pipefail 18 | 19 | REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." &> /dev/null && pwd -P)" 20 | 21 | cd "${REPO_ROOT}/hack/tools" 22 | go build -o "${REPO_ROOT}"/hack/bin/golangci-lint github.com/golangci/golangci-lint/cmd/golangci-lint 23 | cd "${REPO_ROOT}" 24 | 25 | "${REPO_ROOT}"/hack/bin/golangci-lint --config "${REPO_ROOT}/hack/tools/.golangci.yml" run ./... 26 | -------------------------------------------------------------------------------- /transaction.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package knftables 18 | 19 | import ( 20 | "bytes" 21 | "fmt" 22 | ) 23 | 24 | // Transaction represents an nftables transaction 25 | type Transaction struct { 26 | *nftContext 27 | 28 | operations []operation 29 | err error 30 | } 31 | 32 | // operation contains a single nftables operation (eg "add table", "flush chain") 33 | type operation struct { 34 | verb verb 35 | obj Object 36 | } 37 | 38 | // verb is used internally to represent the different "nft" verbs 39 | type verb string 40 | 41 | const ( 42 | addVerb verb = "add" 43 | createVerb verb = "create" 44 | insertVerb verb = "insert" 45 | replaceVerb verb = "replace" 46 | deleteVerb verb = "delete" 47 | flushVerb verb = "flush" 48 | resetVerb verb = "reset" 49 | ) 50 | 51 | // populateCommandBuf populates the transaction as series of nft commands to the given bytes.Buffer. 52 | func (tx *Transaction) populateCommandBuf(buf *bytes.Buffer) error { 53 | if tx.err != nil { 54 | return tx.err 55 | } 56 | 57 | for _, op := range tx.operations { 58 | op.obj.writeOperation(op.verb, tx.nftContext, buf) 59 | } 60 | return nil 61 | } 62 | 63 | // String returns the transaction as a string containing the nft commands; if there is 64 | // a pending error, it will be output as a comment at the end of the transaction. 65 | func (tx *Transaction) String() string { 66 | buf := &bytes.Buffer{} 67 | for _, op := range tx.operations { 68 | op.obj.writeOperation(op.verb, tx.nftContext, buf) 69 | } 70 | 71 | if tx.err != nil { 72 | fmt.Fprintf(buf, "# ERROR: %v", tx.err) 73 | } 74 | 75 | return buf.String() 76 | } 77 | 78 | // NumOperations returns the number of operations queued in the transaction. 79 | func (tx *Transaction) NumOperations() int { 80 | return len(tx.operations) 81 | } 82 | 83 | func (tx *Transaction) operation(verb verb, obj Object) { 84 | if tx.err != nil { 85 | return 86 | } 87 | if tx.err = obj.validate(verb); tx.err != nil { 88 | return 89 | } 90 | 91 | tx.operations = append(tx.operations, operation{verb: verb, obj: obj}) 92 | } 93 | 94 | // Add adds an "nft add" operation to tx, ensuring that obj exists by creating it if it 95 | // did not already exist. (If obj is a Rule, it will be appended to the end of its chain, 96 | // or else added after the Rule indicated by this rule's Index or Handle.) The Add() call 97 | // always succeeds, but if obj is invalid, or inconsistent with the existing nftables 98 | // state, then an error will be returned when the transaction is Run. 99 | func (tx *Transaction) Add(obj Object) { 100 | tx.operation(addVerb, obj) 101 | } 102 | 103 | // Create adds an "nft create" operation to tx, creating obj, which must not already 104 | // exist. (If obj is a Rule, it will be appended to the end of its chain, or else added 105 | // after the Rule indicated by this rule's Index or Handle.) The Create() call always 106 | // succeeds, but if obj is invalid, already exists, or is inconsistent with the existing 107 | // nftables state, then an error will be returned when the transaction is Run. 108 | func (tx *Transaction) Create(obj Object) { 109 | tx.operation(createVerb, obj) 110 | } 111 | 112 | // Insert adds an "nft insert" operation to tx, inserting obj (which must be a Rule) at 113 | // the start of its chain, or before the other Rule indicated by this rule's Index or 114 | // Handle. The Insert() call always succeeds, but if obj is invalid or is inconsistent 115 | // with the existing nftables state, then an error will be returned when the transaction 116 | // is Run. 117 | func (tx *Transaction) Insert(obj Object) { 118 | tx.operation(insertVerb, obj) 119 | } 120 | 121 | // Replace adds an "nft replace" operation to tx, replacing an existing rule with obj 122 | // (which must be a Rule). The Replace() call always succeeds, but if obj is invalid, does 123 | // not contain the Handle of an existing rule, or is inconsistent with the existing 124 | // nftables state, then an error will be returned when the transaction is Run. 125 | func (tx *Transaction) Replace(obj Object) { 126 | tx.operation(replaceVerb, obj) 127 | } 128 | 129 | // Flush adds an "nft flush" operation to tx, clearing the contents of obj. The Flush() 130 | // call always succeeds, but if obj does not exist (or does not support flushing) then an 131 | // error will be returned when the transaction is Run. 132 | func (tx *Transaction) Flush(obj Object) { 133 | tx.operation(flushVerb, obj) 134 | } 135 | 136 | // Delete adds an "nft delete" operation to tx, deleting obj. The Delete() call always 137 | // succeeds, but if obj does not exist or cannot be deleted based on the information 138 | // provided (eg, Handle is required but not set) then an error will be returned when the 139 | // transaction is Run. 140 | func (tx *Transaction) Delete(obj Object) { 141 | tx.operation(deleteVerb, obj) 142 | } 143 | 144 | // Reset adds a "nft reset" operation to tx, resetting obj (which must be a Counter). 145 | // The Reset() call always succeeds, but if obj does not exist then an error will be 146 | // returned when the transaction is Run. 147 | func (tx *Transaction) Reset(obj Object) { 148 | tx.operation(resetVerb, obj) 149 | } 150 | -------------------------------------------------------------------------------- /types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package knftables 18 | 19 | import ( 20 | "io" 21 | "time" 22 | ) 23 | 24 | const ( 25 | // Maximum length of a table, chain, set, etc, name 26 | NameLengthMax = 256 27 | 28 | // Maximum length of a comment 29 | CommentLengthMax = 128 30 | ) 31 | 32 | // Object is the interface for an nftables object. All of the concrete object types 33 | // implement this interface. 34 | type Object interface { 35 | // validate validates an object for an operation 36 | validate(verb verb) error 37 | 38 | // writeOperation writes out an "nft" operation involving the object. It assumes 39 | // that the object has been validated. 40 | writeOperation(verb verb, ctx *nftContext, writer io.Writer) 41 | 42 | // parse is the opposite of writeOperation; it fills Object fields based on an "nft add" 43 | // command. line is the part of the line after "nft add " 44 | // (so for most types it starts with the object name). 45 | // If error is returned, Object's fields may be partially filled, therefore Object should not be used. 46 | parse(line string) error 47 | } 48 | 49 | // Family is an nftables family 50 | type Family string 51 | 52 | const ( 53 | // IPv4Family represents the "ip" nftables family, for IPv4 rules. 54 | IPv4Family Family = "ip" 55 | 56 | // IPv6Family represents the "ip6" nftables family, for IPv6 rules. 57 | IPv6Family Family = "ip6" 58 | 59 | // InetFamily represents the "inet" nftables family, for mixed IPv4 and IPv6 rules. 60 | InetFamily Family = "inet" 61 | 62 | // ARPFamily represents the "arp" nftables family, for ARP rules. 63 | ARPFamily Family = "arp" 64 | 65 | // BridgeFamily represents the "bridge" nftables family, for rules operating 66 | // on packets traversing a bridge. 67 | BridgeFamily Family = "bridge" 68 | 69 | // NetDevFamily represents the "netdev" nftables family, for rules operating on 70 | // the device ingress/egress path. 71 | NetDevFamily Family = "netdev" 72 | ) 73 | 74 | // TableFlag represents a table flag 75 | type TableFlag string 76 | 77 | const ( 78 | // DormantFlag indicates that a table is not currently evaluated. (Its base chains 79 | // are unregistered.) 80 | DormantFlag TableFlag = "dormant" 81 | ) 82 | 83 | // Table represents an nftables table. 84 | type Table struct { 85 | // Comment is an optional comment for the table. (Requires kernel >= 5.10 and 86 | // nft >= 0.9.7; otherwise this field will be silently ignored. Requires 87 | // nft >= 1.0.8 to include comments in List() results.) 88 | Comment *string 89 | 90 | // Flags are the table flags 91 | Flags []TableFlag 92 | 93 | // Handle is an identifier that can be used to uniquely identify an object when 94 | // deleting it. When adding a new object, this must be nil. 95 | Handle *int 96 | } 97 | 98 | // BaseChainType represents the "type" of a "base chain" (ie, a chain that is attached to a hook). 99 | // See https://wiki.nftables.org/wiki-nftables/index.php/Configuring_chains#Base_chain_types 100 | type BaseChainType string 101 | 102 | const ( 103 | // FilterType is the chain type for basic packet filtering. 104 | FilterType BaseChainType = "filter" 105 | 106 | // NATType is the chain type for doing DNAT, SNAT, and masquerading. 107 | // NAT operations are only available from certain hooks. 108 | NATType BaseChainType = "nat" 109 | 110 | // RouteType is the chain type for rules that change the routing of packets. 111 | // Chains of this type can only be added to the "output" hook. 112 | RouteType BaseChainType = "route" 113 | ) 114 | 115 | // BaseChainHook represents the "hook" that a base chain is attached to. 116 | // See https://wiki.nftables.org/wiki-nftables/index.php/Configuring_chains#Base_chain_hooks 117 | // and https://wiki.nftables.org/wiki-nftables/index.php/Netfilter_hooks 118 | type BaseChainHook string 119 | 120 | const ( 121 | // PreroutingHook is the "prerouting" stage of packet processing, which is the 122 | // first stage (after "ingress") for inbound ("input path" and "forward path") 123 | // packets. 124 | PreroutingHook BaseChainHook = "prerouting" 125 | 126 | // InputHook is the "input" stage of packet processing, which happens after 127 | // "prerouting" for inbound packets being delivered to an interface on this host, 128 | // in this network namespace. 129 | InputHook BaseChainHook = "input" 130 | 131 | // ForwardHook is the "forward" stage of packet processing, which happens after 132 | // "prerouting" for inbound packets destined for a non-local IP (i.e. on another 133 | // host or in another network namespace) 134 | ForwardHook BaseChainHook = "forward" 135 | 136 | // OutputHook is the "output" stage of packet processing, which is the first stage 137 | // for outbound packets, regardless of their final destination. 138 | OutputHook BaseChainHook = "output" 139 | 140 | // PostroutingHook is the "postrouting" stage of packet processing, which is the 141 | // final stage (before "egress") for outbound ("forward path" and "output path") 142 | // packets. 143 | PostroutingHook BaseChainHook = "postrouting" 144 | 145 | // IngressHook is the "ingress" stage of packet processing, in the "netdev" family 146 | // or (with kernel >= 5.10 and nft >= 0.9.7) the "inet" family. 147 | IngressHook BaseChainHook = "ingress" 148 | 149 | // EgressHook is the "egress" stage of packet processing, in the "netdev" family 150 | // (with kernel >= 5.16 and nft >= 1.0.1). 151 | EgressHook BaseChainHook = "egress" 152 | ) 153 | 154 | // BaseChainPriority represents the "priority" of a base chain. Lower values run earlier. 155 | // See https://wiki.nftables.org/wiki-nftables/index.php/Configuring_chains#Base_chain_priority 156 | // and https://wiki.nftables.org/wiki-nftables/index.php/Netfilter_hooks#Priority_within_hook 157 | // 158 | // In addition to the const values, you can also use a signed integer value, or an 159 | // arithmetic expression consisting of a const value followed by "+" or "-" and an 160 | // integer. 161 | type BaseChainPriority string 162 | 163 | const ( 164 | // RawPriority is the earliest named priority. In particular, it can be used for 165 | // rules that need to run before conntrack. It is equivalent to the value -300 and 166 | // can be used in the ip, ip6, and inet families. 167 | RawPriority BaseChainPriority = "raw" 168 | 169 | // ManglePriority is the standard priority for packet-rewriting operations. It is 170 | // equivalent to the value -150 and can be used in the ip, ip6, and inet families. 171 | ManglePriority BaseChainPriority = "mangle" 172 | 173 | // DNATPriority is the standard priority for DNAT operations. In the ip, ip6, and 174 | // inet families, it is equivalent to the value -100. In the bridge family it is 175 | // equivalent to the value -300. In both cases it can only be used from the 176 | // prerouting hook. 177 | DNATPriority BaseChainPriority = "dstnat" 178 | 179 | // FilterPriority is the standard priority for filtering operations. In the ip, 180 | // ip6, inet, arp, and netdev families, it is equivalent to the value 0. In the 181 | // bridge family it is equivalent to the value -200. 182 | FilterPriority BaseChainPriority = "filter" 183 | 184 | // OutPriority is FIXME. It is equivalent to the value 300 and can only be used in 185 | // the bridge family. 186 | OutPriority BaseChainPriority = "out" 187 | 188 | // SecurityPriority is the standard priority for security operations ("where 189 | // secmark can be set for example"). It is equivalent to the value 50 and can be 190 | // used in the ip, ip6, and inet families. 191 | SecurityPriority BaseChainPriority = "security" 192 | 193 | // SNATPriority is the standard priority for SNAT operations. In the ip, ip6, and 194 | // inet families, it is equivalent to the value 100. In the bridge family it is 195 | // equivalent to the value 300. In both cases it can only be used from the 196 | // postrouting hook. 197 | SNATPriority BaseChainPriority = "srcnat" 198 | ) 199 | 200 | // BaseChainPolicy sets what happens to packets not explicitly accepted or refused by a 201 | // base chain. 202 | type BaseChainPolicy string 203 | 204 | const ( 205 | // AcceptPolicy, which is the default, accepts any unmatched packets (though, 206 | // as with any other nftables chain, a later chain can drop or reject it). 207 | AcceptPolicy BaseChainPolicy = "accept" 208 | 209 | // DropPolicy drops any unmatched packets. 210 | DropPolicy BaseChainPolicy = "drop" 211 | ) 212 | 213 | // Chain represents an nftables chain; either a "base chain" (if Type, Hook, and Priority 214 | // are specified), or a "regular chain" (if they are not). 215 | type Chain struct { 216 | // Name is the name of the chain. 217 | Name string 218 | 219 | // Type is the chain type; this must be set for a base chain and unset for a 220 | // regular chain. 221 | Type *BaseChainType 222 | // Hook is the hook that the chain is connected to; this must be set for a base 223 | // chain and unset for a regular chain. 224 | Hook *BaseChainHook 225 | // Priority is the chain priority; this must be set for a base chain and unset for 226 | // a regular chain. You can call ParsePriority() to convert this to a number. 227 | Priority *BaseChainPriority 228 | 229 | // Policy is the policy for packets not explicitly accepted or refused by a base 230 | // chain. 231 | Policy *BaseChainPolicy 232 | 233 | // Device is the network interface that the chain is attached to; this must be set 234 | // for a base chain connected to the "ingress" or "egress" hooks, and unset for 235 | // all other chains. 236 | Device *string 237 | 238 | // Comment is an optional comment for the object. (Requires kernel >= 5.10 and 239 | // nft >= 0.9.7; otherwise this field will be silently ignored. Requires 240 | // nft >= 1.0.8 to include comments in List() results.) 241 | Comment *string 242 | 243 | // Handle is an identifier that can be used to uniquely identify an object when 244 | // deleting it. When adding a new object, this must be nil 245 | Handle *int 246 | } 247 | 248 | // Rule represents a rule in a chain 249 | type Rule struct { 250 | // Chain is the name of the chain that contains this rule 251 | Chain string 252 | 253 | // Rule is the rule in standard nftables syntax. (Should be empty on Delete, but 254 | // is ignored if not.) Note that this does not include any rule comment, which is 255 | // separate from the rule itself. 256 | Rule string 257 | 258 | // Comment is an optional comment for the rule. 259 | Comment *string 260 | 261 | // Index is the number of a rule (counting from 0) to Add this Rule after or 262 | // Insert it before. Cannot be specified along with Handle. If neither Index 263 | // nor Handle is specified then Add appends the rule the end of the chain and 264 | // Insert prepends it to the beginning. 265 | Index *int 266 | 267 | // Handle is a rule handle. In Add or Insert, if set, this is the handle of 268 | // existing rule to put the new rule after/before. In Delete or Replace, this 269 | // indicates the existing rule to delete/replace, and is mandatory. In the result 270 | // of a List, this will indicate the rule's handle that can then be used in a 271 | // later operation. 272 | Handle *int 273 | } 274 | 275 | // SetFlag represents a set or map flag 276 | type SetFlag string 277 | 278 | const ( 279 | // ConstantFlag is a flag indicating that the set/map is constant. FIXME UNDOCUMENTED 280 | ConstantFlag SetFlag = "constant" 281 | 282 | // DynamicFlag is a flag indicating that the set contains stateful objects 283 | // (counters, quotas, or limits) that will be dynamically updated. 284 | DynamicFlag SetFlag = "dynamic" 285 | 286 | // IntervalFlag is a flag indicating that the set contains either CIDR elements or 287 | // IP ranges. 288 | IntervalFlag SetFlag = "interval" 289 | 290 | // TimeoutFlag is a flag indicating that the set/map has a timeout after which 291 | // dynamically added elements will be removed. (It is set automatically if the 292 | // set/map has a Timeout.) 293 | TimeoutFlag SetFlag = "timeout" 294 | ) 295 | 296 | // SetPolicy represents a set or map storage policy 297 | type SetPolicy string 298 | 299 | const ( 300 | // PolicyPerformance FIXME 301 | PerformancePolicy SetPolicy = "performance" 302 | 303 | // PolicyMemory FIXME 304 | MemoryPolicy SetPolicy = "memory" 305 | ) 306 | 307 | // Set represents the definition of an nftables set (but not its elements) 308 | type Set struct { 309 | // Name is the name of the set. 310 | Name string 311 | 312 | // Type is the type of the set key (eg "ipv4_addr"). Either Type or TypeOf, but 313 | // not both, must be non-empty. 314 | Type string 315 | 316 | // TypeOf is the type of the set key as an nftables expression (eg "ip saddr"). 317 | // Either Type or TypeOf, but not both, must be non-empty. (Requires at least nft 318 | // 0.9.4, and newer than that for some types.) 319 | TypeOf string 320 | 321 | // Flags are the set flags 322 | Flags []SetFlag 323 | 324 | // Timeout is the time that an element will stay in the set before being removed. 325 | // (Optional; mandatory for sets that will be added to from the packet path) 326 | Timeout *time.Duration 327 | 328 | // GCInterval is the interval at which timed-out elements will be removed from the 329 | // set. (Optional; FIXME DEFAULT) 330 | GCInterval *time.Duration 331 | 332 | // Size if the maximum numer of elements in the set. 333 | // (Optional; mandatory for sets that will be added to from the packet path) 334 | Size *uint64 335 | 336 | // Policy is the FIXME 337 | Policy *SetPolicy 338 | 339 | // AutoMerge indicates that adjacent/overlapping set elements should be merged 340 | // together (only for interval sets) 341 | AutoMerge *bool 342 | 343 | // Comment is an optional comment for the object. (Requires kernel >= 5.10 and 344 | // nft >= 0.9.7; otherwise this field will be silently ignored.) 345 | Comment *string 346 | 347 | // Handle is an identifier that can be used to uniquely identify an object when 348 | // deleting it. When adding a new object, this must be nil 349 | Handle *int 350 | } 351 | 352 | // Map represents the definition of an nftables map (but not its elements) 353 | type Map struct { 354 | // Name is the name of the map. 355 | Name string 356 | 357 | // Type is the type of the map key and value (eg "ipv4_addr : verdict"). Either 358 | // Type or TypeOf, but not both, must be non-empty. 359 | Type string 360 | 361 | // TypeOf is the type of the set key as an nftables expression (eg "ip saddr : verdict"). 362 | // Either Type or TypeOf, but not both, must be non-empty. (Requires at least nft 0.9.4, 363 | // and newer than that for some types.) 364 | TypeOf string 365 | 366 | // Flags are the map flags 367 | Flags []SetFlag 368 | 369 | // Timeout is the time that an element will stay in the set before being removed. 370 | // (Optional; mandatory for sets that will be added to from the packet path) 371 | Timeout *time.Duration 372 | 373 | // GCInterval is the interval at which timed-out elements will be removed from the 374 | // set. (Optional; FIXME DEFAULT) 375 | GCInterval *time.Duration 376 | 377 | // Size if the maximum numer of elements in the set. 378 | // (Optional; mandatory for sets that will be added to from the packet path) 379 | Size *uint64 380 | 381 | // Policy is the FIXME 382 | Policy *SetPolicy 383 | 384 | // Comment is an optional comment for the object. (Requires kernel >= 5.10 and 385 | // nft >= 0.9.7; otherwise this field will be silently ignored.) 386 | Comment *string 387 | 388 | // Handle is an identifier that can be used to uniquely identify an object when 389 | // deleting it. When adding a new object, this must be nil 390 | Handle *int 391 | } 392 | 393 | // Element represents a set or map element 394 | type Element struct { 395 | // Set is the name of the set that contains this element (or the empty string if 396 | // this is a map element.) 397 | Set string 398 | 399 | // Map is the name of the map that contains this element (or the empty string if 400 | // this is a set element.) 401 | Map string 402 | 403 | // Key is the element key. (The list contains a single element for "simple" keys, 404 | // or multiple elements for concatenations.) 405 | Key []string 406 | 407 | // Value is the map element value. As with Key, this may be a single value or 408 | // multiple. For set elements, this must be nil. 409 | Value []string 410 | 411 | // Comment is an optional comment for the element 412 | Comment *string 413 | } 414 | 415 | type FlowtableIngressPriority string 416 | 417 | const ( 418 | // FilterIngressPriority is the priority for the filter value in the Ingress hook 419 | // that stands for 0. 420 | FilterIngressPriority FlowtableIngressPriority = "filter" 421 | ) 422 | 423 | // Flowtable represents an nftables flowtable. 424 | // https://wiki.nftables.org/wiki-nftables/index.php/Flowtables 425 | type Flowtable struct { 426 | // Name is the name of the flowtable. 427 | Name string 428 | 429 | // The Priority can be a signed integer or FlowtableIngressPriority which stands for 0. 430 | // Addition and subtraction can be used to set relative priority, e.g. filter + 5 equals to 5. 431 | Priority *FlowtableIngressPriority 432 | 433 | // The Devices are specified as iifname(s) of the input interface(s) of the traffic 434 | // that should be offloaded. 435 | Devices []string 436 | 437 | // Handle is an identifier that can be used to uniquely identify an object when 438 | // deleting it. When adding a new object, this must be nil 439 | Handle *int 440 | } 441 | 442 | // Counter represents named counter 443 | type Counter struct { 444 | // Name is the name of the named counter 445 | Name string 446 | 447 | // Comment is an optional comment for the counter 448 | Comment *string 449 | 450 | // Packets represents numbers of packets tracked by the counter. 451 | // This will be filled in by ListCounters() but can be nil when creating new counter. 452 | Packets *uint64 453 | 454 | // Bytes represents numbers of bytes tracked by the counter. 455 | // This will be filled in by ListCounters() but can be nil when creating new counter. 456 | Bytes *uint64 457 | 458 | // Handle is an identifier that can be used to uniquely identify an object when 459 | // deleting it. When adding a new object, this must be nil 460 | Handle *int 461 | } 462 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package knftables 18 | 19 | import ( 20 | "fmt" 21 | "strconv" 22 | "strings" 23 | ) 24 | 25 | // PtrTo can be used to fill in optional field values in objects 26 | func PtrTo[T any](val T) *T { 27 | return &val 28 | } 29 | 30 | var numericPriorities = map[string]int{ 31 | "raw": -300, 32 | "mangle": -150, 33 | "dstnat": -100, 34 | "filter": 0, 35 | "security": 50, 36 | "srcnat": 100, 37 | } 38 | 39 | var bridgeNumericPriorities = map[string]int{ 40 | "dstnat": -300, 41 | "filter": -200, 42 | "out": 100, 43 | "srcnat": 300, 44 | } 45 | 46 | // ParsePriority tries to convert the string form of a chain priority into a number 47 | func ParsePriority(family Family, priority string) (int, error) { 48 | val, err := strconv.Atoi(priority) 49 | if err == nil { 50 | return val, nil 51 | } 52 | 53 | modVal := 0 54 | if i := strings.IndexAny(priority, "+-"); i != -1 { 55 | mod := priority[i:] 56 | modVal, err = strconv.Atoi(mod) 57 | if err != nil { 58 | return 0, fmt.Errorf("could not parse modifier %q: %w", mod, err) 59 | } 60 | priority = priority[:i] 61 | } 62 | 63 | var found bool 64 | if family == BridgeFamily { 65 | val, found = bridgeNumericPriorities[priority] 66 | } else { 67 | val, found = numericPriorities[priority] 68 | } 69 | if !found { 70 | return 0, fmt.Errorf("unknown priority %q", priority) 71 | } 72 | 73 | return val + modVal, nil 74 | } 75 | 76 | // Concat is a helper (primarily) for constructing Rule objects. It takes a series of 77 | // arguments and concatenates them together into a single string with spaces between the 78 | // arguments. Strings are output as-is, string arrays are output element by element, 79 | // numbers are output as with `fmt.Sprintf("%d")`, and all other types are output as with 80 | // `fmt.Sprintf("%s")`. To help with set/map lookup syntax, an argument of "@" will not 81 | // be followed by a space, so you can do, eg, `Concat("ip saddr", "@", setName)`. 82 | func Concat(args ...interface{}) string { 83 | b := &strings.Builder{} 84 | var needSpace, wroteAt bool 85 | for _, arg := range args { 86 | switch x := arg.(type) { 87 | case string: 88 | if needSpace { 89 | b.WriteByte(' ') 90 | } 91 | b.WriteString(x) 92 | wroteAt = (x == "@") 93 | case []string: 94 | for _, s := range x { 95 | if needSpace { 96 | b.WriteByte(' ') 97 | } 98 | b.WriteString(s) 99 | wroteAt = (s == "@") 100 | needSpace = b.Len() > 0 && !wroteAt 101 | } 102 | case int, uint, int16, uint16, int32, uint32, int64, uint64: 103 | if needSpace { 104 | b.WriteByte(' ') 105 | } 106 | fmt.Fprintf(b, "%d", x) 107 | default: 108 | if needSpace { 109 | b.WriteByte(' ') 110 | } 111 | fmt.Fprintf(b, "%s", x) 112 | } 113 | 114 | needSpace = b.Len() > 0 && !wroteAt 115 | } 116 | return b.String() 117 | } 118 | -------------------------------------------------------------------------------- /util_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package knftables 18 | 19 | import ( 20 | "net" 21 | "testing" 22 | ) 23 | 24 | func TestConcat(t *testing.T) { 25 | _, cidr, _ := net.ParseCIDR("10.2.0.0/24") 26 | 27 | for _, tc := range []struct { 28 | name string 29 | values []interface{} 30 | out string 31 | }{ 32 | { 33 | name: "empty", 34 | values: nil, 35 | out: "", 36 | }, 37 | { 38 | name: "one string", 39 | values: []interface{}{ 40 | "foo", 41 | }, 42 | out: "foo", 43 | }, 44 | { 45 | name: "one array", 46 | values: []interface{}{ 47 | []string{"foo", "bar", "baz"}, 48 | }, 49 | out: "foo bar baz", 50 | }, 51 | { 52 | name: "mixed", 53 | values: []interface{}{ 54 | "foo", []string{"bar", "baz"}, "quux", 55 | }, 56 | out: "foo bar baz quux", 57 | }, 58 | { 59 | name: "empty array", 60 | values: []interface{}{ 61 | "foo", []string{}, "bar", "baz", 62 | }, 63 | // no extra spaces 64 | out: "foo bar baz", 65 | }, 66 | { 67 | name: "numbers", 68 | values: []interface{}{ 69 | 1, uint16(65535), int64(-123456789), 70 | }, 71 | out: "1 65535 -123456789", 72 | }, 73 | { 74 | name: "everything", 75 | values: []interface{}{ 76 | IPv4Family, "saddr", cidr, 77 | []string{}, 78 | "th port", 8080, 79 | "@", "mySetName", 80 | []string{"@", "myMapName"}, 81 | []string{"ct", "state", "established"}, 82 | "drop", 83 | }, 84 | out: "ip saddr 10.2.0.0/24 th port 8080 @mySetName @myMapName ct state established drop", 85 | }, 86 | } { 87 | t.Run(tc.name, func(t *testing.T) { 88 | out := Concat(tc.values...) 89 | if out != tc.out { 90 | t.Errorf("expected %q got %q", tc.out, out) 91 | } 92 | }) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /vendor/github.com/google/go-cmp/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 The Go Authors. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google Inc. nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /vendor/github.com/google/go-cmp/cmp/export_panic.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017, The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | //go:build purego 6 | // +build purego 7 | 8 | package cmp 9 | 10 | import "reflect" 11 | 12 | const supportExporters = false 13 | 14 | func retrieveUnexportedField(reflect.Value, reflect.StructField, bool) reflect.Value { 15 | panic("no support for forcibly accessing unexported fields") 16 | } 17 | -------------------------------------------------------------------------------- /vendor/github.com/google/go-cmp/cmp/export_unsafe.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017, The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | //go:build !purego 6 | // +build !purego 7 | 8 | package cmp 9 | 10 | import ( 11 | "reflect" 12 | "unsafe" 13 | ) 14 | 15 | const supportExporters = true 16 | 17 | // retrieveUnexportedField uses unsafe to forcibly retrieve any field from 18 | // a struct such that the value has read-write permissions. 19 | // 20 | // The parent struct, v, must be addressable, while f must be a StructField 21 | // describing the field to retrieve. If addr is false, 22 | // then the returned value will be shallowed copied to be non-addressable. 23 | func retrieveUnexportedField(v reflect.Value, f reflect.StructField, addr bool) reflect.Value { 24 | ve := reflect.NewAt(f.Type, unsafe.Pointer(uintptr(unsafe.Pointer(v.UnsafeAddr()))+f.Offset)).Elem() 25 | if !addr { 26 | // A field is addressable if and only if the struct is addressable. 27 | // If the original parent value was not addressable, shallow copy the 28 | // value to make it non-addressable to avoid leaking an implementation 29 | // detail of how forcibly exporting a field works. 30 | if ve.Kind() == reflect.Interface && ve.IsNil() { 31 | return reflect.Zero(f.Type) 32 | } 33 | return reflect.ValueOf(ve.Interface()).Convert(f.Type) 34 | } 35 | return ve 36 | } 37 | -------------------------------------------------------------------------------- /vendor/github.com/google/go-cmp/cmp/internal/diff/debug_disable.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017, The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | //go:build !cmp_debug 6 | // +build !cmp_debug 7 | 8 | package diff 9 | 10 | var debug debugger 11 | 12 | type debugger struct{} 13 | 14 | func (debugger) Begin(_, _ int, f EqualFunc, _, _ *EditScript) EqualFunc { 15 | return f 16 | } 17 | func (debugger) Update() {} 18 | func (debugger) Finish() {} 19 | -------------------------------------------------------------------------------- /vendor/github.com/google/go-cmp/cmp/internal/diff/debug_enable.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017, The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | //go:build cmp_debug 6 | // +build cmp_debug 7 | 8 | package diff 9 | 10 | import ( 11 | "fmt" 12 | "strings" 13 | "sync" 14 | "time" 15 | ) 16 | 17 | // The algorithm can be seen running in real-time by enabling debugging: 18 | // go test -tags=cmp_debug -v 19 | // 20 | // Example output: 21 | // === RUN TestDifference/#34 22 | // ┌───────────────────────────────┐ 23 | // │ \ · · · · · · · · · · · · · · │ 24 | // │ · # · · · · · · · · · · · · · │ 25 | // │ · \ · · · · · · · · · · · · · │ 26 | // │ · · \ · · · · · · · · · · · · │ 27 | // │ · · · X # · · · · · · · · · · │ 28 | // │ · · · # \ · · · · · · · · · · │ 29 | // │ · · · · · # # · · · · · · · · │ 30 | // │ · · · · · # \ · · · · · · · · │ 31 | // │ · · · · · · · \ · · · · · · · │ 32 | // │ · · · · · · · · \ · · · · · · │ 33 | // │ · · · · · · · · · \ · · · · · │ 34 | // │ · · · · · · · · · · \ · · # · │ 35 | // │ · · · · · · · · · · · \ # # · │ 36 | // │ · · · · · · · · · · · # # # · │ 37 | // │ · · · · · · · · · · # # # # · │ 38 | // │ · · · · · · · · · # # # # # · │ 39 | // │ · · · · · · · · · · · · · · \ │ 40 | // └───────────────────────────────┘ 41 | // [.Y..M.XY......YXYXY.|] 42 | // 43 | // The grid represents the edit-graph where the horizontal axis represents 44 | // list X and the vertical axis represents list Y. The start of the two lists 45 | // is the top-left, while the ends are the bottom-right. The '·' represents 46 | // an unexplored node in the graph. The '\' indicates that the two symbols 47 | // from list X and Y are equal. The 'X' indicates that two symbols are similar 48 | // (but not exactly equal) to each other. The '#' indicates that the two symbols 49 | // are different (and not similar). The algorithm traverses this graph trying to 50 | // make the paths starting in the top-left and the bottom-right connect. 51 | // 52 | // The series of '.', 'X', 'Y', and 'M' characters at the bottom represents 53 | // the currently established path from the forward and reverse searches, 54 | // separated by a '|' character. 55 | 56 | const ( 57 | updateDelay = 100 * time.Millisecond 58 | finishDelay = 500 * time.Millisecond 59 | ansiTerminal = true // ANSI escape codes used to move terminal cursor 60 | ) 61 | 62 | var debug debugger 63 | 64 | type debugger struct { 65 | sync.Mutex 66 | p1, p2 EditScript 67 | fwdPath, revPath *EditScript 68 | grid []byte 69 | lines int 70 | } 71 | 72 | func (dbg *debugger) Begin(nx, ny int, f EqualFunc, p1, p2 *EditScript) EqualFunc { 73 | dbg.Lock() 74 | dbg.fwdPath, dbg.revPath = p1, p2 75 | top := "┌─" + strings.Repeat("──", nx) + "┐\n" 76 | row := "│ " + strings.Repeat("· ", nx) + "│\n" 77 | btm := "└─" + strings.Repeat("──", nx) + "┘\n" 78 | dbg.grid = []byte(top + strings.Repeat(row, ny) + btm) 79 | dbg.lines = strings.Count(dbg.String(), "\n") 80 | fmt.Print(dbg) 81 | 82 | // Wrap the EqualFunc so that we can intercept each result. 83 | return func(ix, iy int) (r Result) { 84 | cell := dbg.grid[len(top)+iy*len(row):][len("│ ")+len("· ")*ix:][:len("·")] 85 | for i := range cell { 86 | cell[i] = 0 // Zero out the multiple bytes of UTF-8 middle-dot 87 | } 88 | switch r = f(ix, iy); { 89 | case r.Equal(): 90 | cell[0] = '\\' 91 | case r.Similar(): 92 | cell[0] = 'X' 93 | default: 94 | cell[0] = '#' 95 | } 96 | return 97 | } 98 | } 99 | 100 | func (dbg *debugger) Update() { 101 | dbg.print(updateDelay) 102 | } 103 | 104 | func (dbg *debugger) Finish() { 105 | dbg.print(finishDelay) 106 | dbg.Unlock() 107 | } 108 | 109 | func (dbg *debugger) String() string { 110 | dbg.p1, dbg.p2 = *dbg.fwdPath, dbg.p2[:0] 111 | for i := len(*dbg.revPath) - 1; i >= 0; i-- { 112 | dbg.p2 = append(dbg.p2, (*dbg.revPath)[i]) 113 | } 114 | return fmt.Sprintf("%s[%v|%v]\n\n", dbg.grid, dbg.p1, dbg.p2) 115 | } 116 | 117 | func (dbg *debugger) print(d time.Duration) { 118 | if ansiTerminal { 119 | fmt.Printf("\x1b[%dA", dbg.lines) // Reset terminal cursor 120 | } 121 | fmt.Print(dbg) 122 | time.Sleep(d) 123 | } 124 | -------------------------------------------------------------------------------- /vendor/github.com/google/go-cmp/cmp/internal/diff/diff.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017, The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package diff implements an algorithm for producing edit-scripts. 6 | // The edit-script is a sequence of operations needed to transform one list 7 | // of symbols into another (or vice-versa). The edits allowed are insertions, 8 | // deletions, and modifications. The summation of all edits is called the 9 | // Levenshtein distance as this problem is well-known in computer science. 10 | // 11 | // This package prioritizes performance over accuracy. That is, the run time 12 | // is more important than obtaining a minimal Levenshtein distance. 13 | package diff 14 | 15 | import ( 16 | "math/rand" 17 | "time" 18 | 19 | "github.com/google/go-cmp/cmp/internal/flags" 20 | ) 21 | 22 | // EditType represents a single operation within an edit-script. 23 | type EditType uint8 24 | 25 | const ( 26 | // Identity indicates that a symbol pair is identical in both list X and Y. 27 | Identity EditType = iota 28 | // UniqueX indicates that a symbol only exists in X and not Y. 29 | UniqueX 30 | // UniqueY indicates that a symbol only exists in Y and not X. 31 | UniqueY 32 | // Modified indicates that a symbol pair is a modification of each other. 33 | Modified 34 | ) 35 | 36 | // EditScript represents the series of differences between two lists. 37 | type EditScript []EditType 38 | 39 | // String returns a human-readable string representing the edit-script where 40 | // Identity, UniqueX, UniqueY, and Modified are represented by the 41 | // '.', 'X', 'Y', and 'M' characters, respectively. 42 | func (es EditScript) String() string { 43 | b := make([]byte, len(es)) 44 | for i, e := range es { 45 | switch e { 46 | case Identity: 47 | b[i] = '.' 48 | case UniqueX: 49 | b[i] = 'X' 50 | case UniqueY: 51 | b[i] = 'Y' 52 | case Modified: 53 | b[i] = 'M' 54 | default: 55 | panic("invalid edit-type") 56 | } 57 | } 58 | return string(b) 59 | } 60 | 61 | // stats returns a histogram of the number of each type of edit operation. 62 | func (es EditScript) stats() (s struct{ NI, NX, NY, NM int }) { 63 | for _, e := range es { 64 | switch e { 65 | case Identity: 66 | s.NI++ 67 | case UniqueX: 68 | s.NX++ 69 | case UniqueY: 70 | s.NY++ 71 | case Modified: 72 | s.NM++ 73 | default: 74 | panic("invalid edit-type") 75 | } 76 | } 77 | return 78 | } 79 | 80 | // Dist is the Levenshtein distance and is guaranteed to be 0 if and only if 81 | // lists X and Y are equal. 82 | func (es EditScript) Dist() int { return len(es) - es.stats().NI } 83 | 84 | // LenX is the length of the X list. 85 | func (es EditScript) LenX() int { return len(es) - es.stats().NY } 86 | 87 | // LenY is the length of the Y list. 88 | func (es EditScript) LenY() int { return len(es) - es.stats().NX } 89 | 90 | // EqualFunc reports whether the symbols at indexes ix and iy are equal. 91 | // When called by Difference, the index is guaranteed to be within nx and ny. 92 | type EqualFunc func(ix int, iy int) Result 93 | 94 | // Result is the result of comparison. 95 | // NumSame is the number of sub-elements that are equal. 96 | // NumDiff is the number of sub-elements that are not equal. 97 | type Result struct{ NumSame, NumDiff int } 98 | 99 | // BoolResult returns a Result that is either Equal or not Equal. 100 | func BoolResult(b bool) Result { 101 | if b { 102 | return Result{NumSame: 1} // Equal, Similar 103 | } else { 104 | return Result{NumDiff: 2} // Not Equal, not Similar 105 | } 106 | } 107 | 108 | // Equal indicates whether the symbols are equal. Two symbols are equal 109 | // if and only if NumDiff == 0. If Equal, then they are also Similar. 110 | func (r Result) Equal() bool { return r.NumDiff == 0 } 111 | 112 | // Similar indicates whether two symbols are similar and may be represented 113 | // by using the Modified type. As a special case, we consider binary comparisons 114 | // (i.e., those that return Result{1, 0} or Result{0, 1}) to be similar. 115 | // 116 | // The exact ratio of NumSame to NumDiff to determine similarity may change. 117 | func (r Result) Similar() bool { 118 | // Use NumSame+1 to offset NumSame so that binary comparisons are similar. 119 | return r.NumSame+1 >= r.NumDiff 120 | } 121 | 122 | var randBool = rand.New(rand.NewSource(time.Now().Unix())).Intn(2) == 0 123 | 124 | // Difference reports whether two lists of lengths nx and ny are equal 125 | // given the definition of equality provided as f. 126 | // 127 | // This function returns an edit-script, which is a sequence of operations 128 | // needed to convert one list into the other. The following invariants for 129 | // the edit-script are maintained: 130 | // - eq == (es.Dist()==0) 131 | // - nx == es.LenX() 132 | // - ny == es.LenY() 133 | // 134 | // This algorithm is not guaranteed to be an optimal solution (i.e., one that 135 | // produces an edit-script with a minimal Levenshtein distance). This algorithm 136 | // favors performance over optimality. The exact output is not guaranteed to 137 | // be stable and may change over time. 138 | func Difference(nx, ny int, f EqualFunc) (es EditScript) { 139 | // This algorithm is based on traversing what is known as an "edit-graph". 140 | // See Figure 1 from "An O(ND) Difference Algorithm and Its Variations" 141 | // by Eugene W. Myers. Since D can be as large as N itself, this is 142 | // effectively O(N^2). Unlike the algorithm from that paper, we are not 143 | // interested in the optimal path, but at least some "decent" path. 144 | // 145 | // For example, let X and Y be lists of symbols: 146 | // X = [A B C A B B A] 147 | // Y = [C B A B A C] 148 | // 149 | // The edit-graph can be drawn as the following: 150 | // A B C A B B A 151 | // ┌─────────────┐ 152 | // C │_|_|\|_|_|_|_│ 0 153 | // B │_|\|_|_|\|\|_│ 1 154 | // A │\|_|_|\|_|_|\│ 2 155 | // B │_|\|_|_|\|\|_│ 3 156 | // A │\|_|_|\|_|_|\│ 4 157 | // C │ | |\| | | | │ 5 158 | // └─────────────┘ 6 159 | // 0 1 2 3 4 5 6 7 160 | // 161 | // List X is written along the horizontal axis, while list Y is written 162 | // along the vertical axis. At any point on this grid, if the symbol in 163 | // list X matches the corresponding symbol in list Y, then a '\' is drawn. 164 | // The goal of any minimal edit-script algorithm is to find a path from the 165 | // top-left corner to the bottom-right corner, while traveling through the 166 | // fewest horizontal or vertical edges. 167 | // A horizontal edge is equivalent to inserting a symbol from list X. 168 | // A vertical edge is equivalent to inserting a symbol from list Y. 169 | // A diagonal edge is equivalent to a matching symbol between both X and Y. 170 | 171 | // Invariants: 172 | // - 0 ≤ fwdPath.X ≤ (fwdFrontier.X, revFrontier.X) ≤ revPath.X ≤ nx 173 | // - 0 ≤ fwdPath.Y ≤ (fwdFrontier.Y, revFrontier.Y) ≤ revPath.Y ≤ ny 174 | // 175 | // In general: 176 | // - fwdFrontier.X < revFrontier.X 177 | // - fwdFrontier.Y < revFrontier.Y 178 | // 179 | // Unless, it is time for the algorithm to terminate. 180 | fwdPath := path{+1, point{0, 0}, make(EditScript, 0, (nx+ny)/2)} 181 | revPath := path{-1, point{nx, ny}, make(EditScript, 0)} 182 | fwdFrontier := fwdPath.point // Forward search frontier 183 | revFrontier := revPath.point // Reverse search frontier 184 | 185 | // Search budget bounds the cost of searching for better paths. 186 | // The longest sequence of non-matching symbols that can be tolerated is 187 | // approximately the square-root of the search budget. 188 | searchBudget := 4 * (nx + ny) // O(n) 189 | 190 | // Running the tests with the "cmp_debug" build tag prints a visualization 191 | // of the algorithm running in real-time. This is educational for 192 | // understanding how the algorithm works. See debug_enable.go. 193 | f = debug.Begin(nx, ny, f, &fwdPath.es, &revPath.es) 194 | 195 | // The algorithm below is a greedy, meet-in-the-middle algorithm for 196 | // computing sub-optimal edit-scripts between two lists. 197 | // 198 | // The algorithm is approximately as follows: 199 | // - Searching for differences switches back-and-forth between 200 | // a search that starts at the beginning (the top-left corner), and 201 | // a search that starts at the end (the bottom-right corner). 202 | // The goal of the search is connect with the search 203 | // from the opposite corner. 204 | // - As we search, we build a path in a greedy manner, 205 | // where the first match seen is added to the path (this is sub-optimal, 206 | // but provides a decent result in practice). When matches are found, 207 | // we try the next pair of symbols in the lists and follow all matches 208 | // as far as possible. 209 | // - When searching for matches, we search along a diagonal going through 210 | // through the "frontier" point. If no matches are found, 211 | // we advance the frontier towards the opposite corner. 212 | // - This algorithm terminates when either the X coordinates or the 213 | // Y coordinates of the forward and reverse frontier points ever intersect. 214 | 215 | // This algorithm is correct even if searching only in the forward direction 216 | // or in the reverse direction. We do both because it is commonly observed 217 | // that two lists commonly differ because elements were added to the front 218 | // or end of the other list. 219 | // 220 | // Non-deterministically start with either the forward or reverse direction 221 | // to introduce some deliberate instability so that we have the flexibility 222 | // to change this algorithm in the future. 223 | if flags.Deterministic || randBool { 224 | goto forwardSearch 225 | } else { 226 | goto reverseSearch 227 | } 228 | 229 | forwardSearch: 230 | { 231 | // Forward search from the beginning. 232 | if fwdFrontier.X >= revFrontier.X || fwdFrontier.Y >= revFrontier.Y || searchBudget == 0 { 233 | goto finishSearch 234 | } 235 | for stop1, stop2, i := false, false, 0; !(stop1 && stop2) && searchBudget > 0; i++ { 236 | // Search in a diagonal pattern for a match. 237 | z := zigzag(i) 238 | p := point{fwdFrontier.X + z, fwdFrontier.Y - z} 239 | switch { 240 | case p.X >= revPath.X || p.Y < fwdPath.Y: 241 | stop1 = true // Hit top-right corner 242 | case p.Y >= revPath.Y || p.X < fwdPath.X: 243 | stop2 = true // Hit bottom-left corner 244 | case f(p.X, p.Y).Equal(): 245 | // Match found, so connect the path to this point. 246 | fwdPath.connect(p, f) 247 | fwdPath.append(Identity) 248 | // Follow sequence of matches as far as possible. 249 | for fwdPath.X < revPath.X && fwdPath.Y < revPath.Y { 250 | if !f(fwdPath.X, fwdPath.Y).Equal() { 251 | break 252 | } 253 | fwdPath.append(Identity) 254 | } 255 | fwdFrontier = fwdPath.point 256 | stop1, stop2 = true, true 257 | default: 258 | searchBudget-- // Match not found 259 | } 260 | debug.Update() 261 | } 262 | // Advance the frontier towards reverse point. 263 | if revPath.X-fwdFrontier.X >= revPath.Y-fwdFrontier.Y { 264 | fwdFrontier.X++ 265 | } else { 266 | fwdFrontier.Y++ 267 | } 268 | goto reverseSearch 269 | } 270 | 271 | reverseSearch: 272 | { 273 | // Reverse search from the end. 274 | if fwdFrontier.X >= revFrontier.X || fwdFrontier.Y >= revFrontier.Y || searchBudget == 0 { 275 | goto finishSearch 276 | } 277 | for stop1, stop2, i := false, false, 0; !(stop1 && stop2) && searchBudget > 0; i++ { 278 | // Search in a diagonal pattern for a match. 279 | z := zigzag(i) 280 | p := point{revFrontier.X - z, revFrontier.Y + z} 281 | switch { 282 | case fwdPath.X >= p.X || revPath.Y < p.Y: 283 | stop1 = true // Hit bottom-left corner 284 | case fwdPath.Y >= p.Y || revPath.X < p.X: 285 | stop2 = true // Hit top-right corner 286 | case f(p.X-1, p.Y-1).Equal(): 287 | // Match found, so connect the path to this point. 288 | revPath.connect(p, f) 289 | revPath.append(Identity) 290 | // Follow sequence of matches as far as possible. 291 | for fwdPath.X < revPath.X && fwdPath.Y < revPath.Y { 292 | if !f(revPath.X-1, revPath.Y-1).Equal() { 293 | break 294 | } 295 | revPath.append(Identity) 296 | } 297 | revFrontier = revPath.point 298 | stop1, stop2 = true, true 299 | default: 300 | searchBudget-- // Match not found 301 | } 302 | debug.Update() 303 | } 304 | // Advance the frontier towards forward point. 305 | if revFrontier.X-fwdPath.X >= revFrontier.Y-fwdPath.Y { 306 | revFrontier.X-- 307 | } else { 308 | revFrontier.Y-- 309 | } 310 | goto forwardSearch 311 | } 312 | 313 | finishSearch: 314 | // Join the forward and reverse paths and then append the reverse path. 315 | fwdPath.connect(revPath.point, f) 316 | for i := len(revPath.es) - 1; i >= 0; i-- { 317 | t := revPath.es[i] 318 | revPath.es = revPath.es[:i] 319 | fwdPath.append(t) 320 | } 321 | debug.Finish() 322 | return fwdPath.es 323 | } 324 | 325 | type path struct { 326 | dir int // +1 if forward, -1 if reverse 327 | point // Leading point of the EditScript path 328 | es EditScript 329 | } 330 | 331 | // connect appends any necessary Identity, Modified, UniqueX, or UniqueY types 332 | // to the edit-script to connect p.point to dst. 333 | func (p *path) connect(dst point, f EqualFunc) { 334 | if p.dir > 0 { 335 | // Connect in forward direction. 336 | for dst.X > p.X && dst.Y > p.Y { 337 | switch r := f(p.X, p.Y); { 338 | case r.Equal(): 339 | p.append(Identity) 340 | case r.Similar(): 341 | p.append(Modified) 342 | case dst.X-p.X >= dst.Y-p.Y: 343 | p.append(UniqueX) 344 | default: 345 | p.append(UniqueY) 346 | } 347 | } 348 | for dst.X > p.X { 349 | p.append(UniqueX) 350 | } 351 | for dst.Y > p.Y { 352 | p.append(UniqueY) 353 | } 354 | } else { 355 | // Connect in reverse direction. 356 | for p.X > dst.X && p.Y > dst.Y { 357 | switch r := f(p.X-1, p.Y-1); { 358 | case r.Equal(): 359 | p.append(Identity) 360 | case r.Similar(): 361 | p.append(Modified) 362 | case p.Y-dst.Y >= p.X-dst.X: 363 | p.append(UniqueY) 364 | default: 365 | p.append(UniqueX) 366 | } 367 | } 368 | for p.X > dst.X { 369 | p.append(UniqueX) 370 | } 371 | for p.Y > dst.Y { 372 | p.append(UniqueY) 373 | } 374 | } 375 | } 376 | 377 | func (p *path) append(t EditType) { 378 | p.es = append(p.es, t) 379 | switch t { 380 | case Identity, Modified: 381 | p.add(p.dir, p.dir) 382 | case UniqueX: 383 | p.add(p.dir, 0) 384 | case UniqueY: 385 | p.add(0, p.dir) 386 | } 387 | debug.Update() 388 | } 389 | 390 | type point struct{ X, Y int } 391 | 392 | func (p *point) add(dx, dy int) { p.X += dx; p.Y += dy } 393 | 394 | // zigzag maps a consecutive sequence of integers to a zig-zag sequence. 395 | // 396 | // [0 1 2 3 4 5 ...] => [0 -1 +1 -2 +2 ...] 397 | func zigzag(x int) int { 398 | if x&1 != 0 { 399 | x = ^x 400 | } 401 | return x >> 1 402 | } 403 | -------------------------------------------------------------------------------- /vendor/github.com/google/go-cmp/cmp/internal/flags/flags.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019, The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package flags 6 | 7 | // Deterministic controls whether the output of Diff should be deterministic. 8 | // This is only used for testing. 9 | var Deterministic bool 10 | -------------------------------------------------------------------------------- /vendor/github.com/google/go-cmp/cmp/internal/function/func.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017, The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package function provides functionality for identifying function types. 6 | package function 7 | 8 | import ( 9 | "reflect" 10 | "regexp" 11 | "runtime" 12 | "strings" 13 | ) 14 | 15 | type funcType int 16 | 17 | const ( 18 | _ funcType = iota 19 | 20 | tbFunc // func(T) bool 21 | ttbFunc // func(T, T) bool 22 | trbFunc // func(T, R) bool 23 | tibFunc // func(T, I) bool 24 | trFunc // func(T) R 25 | 26 | Equal = ttbFunc // func(T, T) bool 27 | EqualAssignable = tibFunc // func(T, I) bool; encapsulates func(T, T) bool 28 | Transformer = trFunc // func(T) R 29 | ValueFilter = ttbFunc // func(T, T) bool 30 | Less = ttbFunc // func(T, T) bool 31 | ValuePredicate = tbFunc // func(T) bool 32 | KeyValuePredicate = trbFunc // func(T, R) bool 33 | ) 34 | 35 | var boolType = reflect.TypeOf(true) 36 | 37 | // IsType reports whether the reflect.Type is of the specified function type. 38 | func IsType(t reflect.Type, ft funcType) bool { 39 | if t == nil || t.Kind() != reflect.Func || t.IsVariadic() { 40 | return false 41 | } 42 | ni, no := t.NumIn(), t.NumOut() 43 | switch ft { 44 | case tbFunc: // func(T) bool 45 | if ni == 1 && no == 1 && t.Out(0) == boolType { 46 | return true 47 | } 48 | case ttbFunc: // func(T, T) bool 49 | if ni == 2 && no == 1 && t.In(0) == t.In(1) && t.Out(0) == boolType { 50 | return true 51 | } 52 | case trbFunc: // func(T, R) bool 53 | if ni == 2 && no == 1 && t.Out(0) == boolType { 54 | return true 55 | } 56 | case tibFunc: // func(T, I) bool 57 | if ni == 2 && no == 1 && t.In(0).AssignableTo(t.In(1)) && t.Out(0) == boolType { 58 | return true 59 | } 60 | case trFunc: // func(T) R 61 | if ni == 1 && no == 1 { 62 | return true 63 | } 64 | } 65 | return false 66 | } 67 | 68 | var lastIdentRx = regexp.MustCompile(`[_\p{L}][_\p{L}\p{N}]*$`) 69 | 70 | // NameOf returns the name of the function value. 71 | func NameOf(v reflect.Value) string { 72 | fnc := runtime.FuncForPC(v.Pointer()) 73 | if fnc == nil { 74 | return "" 75 | } 76 | fullName := fnc.Name() // e.g., "long/path/name/mypkg.(*MyType).(long/path/name/mypkg.myMethod)-fm" 77 | 78 | // Method closures have a "-fm" suffix. 79 | fullName = strings.TrimSuffix(fullName, "-fm") 80 | 81 | var name string 82 | for len(fullName) > 0 { 83 | inParen := strings.HasSuffix(fullName, ")") 84 | fullName = strings.TrimSuffix(fullName, ")") 85 | 86 | s := lastIdentRx.FindString(fullName) 87 | if s == "" { 88 | break 89 | } 90 | name = s + "." + name 91 | fullName = strings.TrimSuffix(fullName, s) 92 | 93 | if i := strings.LastIndexByte(fullName, '('); inParen && i >= 0 { 94 | fullName = fullName[:i] 95 | } 96 | fullName = strings.TrimSuffix(fullName, ".") 97 | } 98 | return strings.TrimSuffix(name, ".") 99 | } 100 | -------------------------------------------------------------------------------- /vendor/github.com/google/go-cmp/cmp/internal/value/name.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020, The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package value 6 | 7 | import ( 8 | "reflect" 9 | "strconv" 10 | ) 11 | 12 | var anyType = reflect.TypeOf((*interface{})(nil)).Elem() 13 | 14 | // TypeString is nearly identical to reflect.Type.String, 15 | // but has an additional option to specify that full type names be used. 16 | func TypeString(t reflect.Type, qualified bool) string { 17 | return string(appendTypeName(nil, t, qualified, false)) 18 | } 19 | 20 | func appendTypeName(b []byte, t reflect.Type, qualified, elideFunc bool) []byte { 21 | // BUG: Go reflection provides no way to disambiguate two named types 22 | // of the same name and within the same package, 23 | // but declared within the namespace of different functions. 24 | 25 | // Use the "any" alias instead of "interface{}" for better readability. 26 | if t == anyType { 27 | return append(b, "any"...) 28 | } 29 | 30 | // Named type. 31 | if t.Name() != "" { 32 | if qualified && t.PkgPath() != "" { 33 | b = append(b, '"') 34 | b = append(b, t.PkgPath()...) 35 | b = append(b, '"') 36 | b = append(b, '.') 37 | b = append(b, t.Name()...) 38 | } else { 39 | b = append(b, t.String()...) 40 | } 41 | return b 42 | } 43 | 44 | // Unnamed type. 45 | switch k := t.Kind(); k { 46 | case reflect.Bool, reflect.String, reflect.UnsafePointer, 47 | reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, 48 | reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr, 49 | reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128: 50 | b = append(b, k.String()...) 51 | case reflect.Chan: 52 | if t.ChanDir() == reflect.RecvDir { 53 | b = append(b, "<-"...) 54 | } 55 | b = append(b, "chan"...) 56 | if t.ChanDir() == reflect.SendDir { 57 | b = append(b, "<-"...) 58 | } 59 | b = append(b, ' ') 60 | b = appendTypeName(b, t.Elem(), qualified, false) 61 | case reflect.Func: 62 | if !elideFunc { 63 | b = append(b, "func"...) 64 | } 65 | b = append(b, '(') 66 | for i := 0; i < t.NumIn(); i++ { 67 | if i > 0 { 68 | b = append(b, ", "...) 69 | } 70 | if i == t.NumIn()-1 && t.IsVariadic() { 71 | b = append(b, "..."...) 72 | b = appendTypeName(b, t.In(i).Elem(), qualified, false) 73 | } else { 74 | b = appendTypeName(b, t.In(i), qualified, false) 75 | } 76 | } 77 | b = append(b, ')') 78 | switch t.NumOut() { 79 | case 0: 80 | // Do nothing 81 | case 1: 82 | b = append(b, ' ') 83 | b = appendTypeName(b, t.Out(0), qualified, false) 84 | default: 85 | b = append(b, " ("...) 86 | for i := 0; i < t.NumOut(); i++ { 87 | if i > 0 { 88 | b = append(b, ", "...) 89 | } 90 | b = appendTypeName(b, t.Out(i), qualified, false) 91 | } 92 | b = append(b, ')') 93 | } 94 | case reflect.Struct: 95 | b = append(b, "struct{ "...) 96 | for i := 0; i < t.NumField(); i++ { 97 | if i > 0 { 98 | b = append(b, "; "...) 99 | } 100 | sf := t.Field(i) 101 | if !sf.Anonymous { 102 | if qualified && sf.PkgPath != "" { 103 | b = append(b, '"') 104 | b = append(b, sf.PkgPath...) 105 | b = append(b, '"') 106 | b = append(b, '.') 107 | } 108 | b = append(b, sf.Name...) 109 | b = append(b, ' ') 110 | } 111 | b = appendTypeName(b, sf.Type, qualified, false) 112 | if sf.Tag != "" { 113 | b = append(b, ' ') 114 | b = strconv.AppendQuote(b, string(sf.Tag)) 115 | } 116 | } 117 | if b[len(b)-1] == ' ' { 118 | b = b[:len(b)-1] 119 | } else { 120 | b = append(b, ' ') 121 | } 122 | b = append(b, '}') 123 | case reflect.Slice, reflect.Array: 124 | b = append(b, '[') 125 | if k == reflect.Array { 126 | b = strconv.AppendUint(b, uint64(t.Len()), 10) 127 | } 128 | b = append(b, ']') 129 | b = appendTypeName(b, t.Elem(), qualified, false) 130 | case reflect.Map: 131 | b = append(b, "map["...) 132 | b = appendTypeName(b, t.Key(), qualified, false) 133 | b = append(b, ']') 134 | b = appendTypeName(b, t.Elem(), qualified, false) 135 | case reflect.Ptr: 136 | b = append(b, '*') 137 | b = appendTypeName(b, t.Elem(), qualified, false) 138 | case reflect.Interface: 139 | b = append(b, "interface{ "...) 140 | for i := 0; i < t.NumMethod(); i++ { 141 | if i > 0 { 142 | b = append(b, "; "...) 143 | } 144 | m := t.Method(i) 145 | if qualified && m.PkgPath != "" { 146 | b = append(b, '"') 147 | b = append(b, m.PkgPath...) 148 | b = append(b, '"') 149 | b = append(b, '.') 150 | } 151 | b = append(b, m.Name...) 152 | b = appendTypeName(b, m.Type, qualified, true) 153 | } 154 | if b[len(b)-1] == ' ' { 155 | b = b[:len(b)-1] 156 | } else { 157 | b = append(b, ' ') 158 | } 159 | b = append(b, '}') 160 | default: 161 | panic("invalid kind: " + k.String()) 162 | } 163 | return b 164 | } 165 | -------------------------------------------------------------------------------- /vendor/github.com/google/go-cmp/cmp/internal/value/pointer_purego.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018, The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | //go:build purego 6 | // +build purego 7 | 8 | package value 9 | 10 | import "reflect" 11 | 12 | // Pointer is an opaque typed pointer and is guaranteed to be comparable. 13 | type Pointer struct { 14 | p uintptr 15 | t reflect.Type 16 | } 17 | 18 | // PointerOf returns a Pointer from v, which must be a 19 | // reflect.Ptr, reflect.Slice, or reflect.Map. 20 | func PointerOf(v reflect.Value) Pointer { 21 | // NOTE: Storing a pointer as an uintptr is technically incorrect as it 22 | // assumes that the GC implementation does not use a moving collector. 23 | return Pointer{v.Pointer(), v.Type()} 24 | } 25 | 26 | // IsNil reports whether the pointer is nil. 27 | func (p Pointer) IsNil() bool { 28 | return p.p == 0 29 | } 30 | 31 | // Uintptr returns the pointer as a uintptr. 32 | func (p Pointer) Uintptr() uintptr { 33 | return p.p 34 | } 35 | -------------------------------------------------------------------------------- /vendor/github.com/google/go-cmp/cmp/internal/value/pointer_unsafe.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018, The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | //go:build !purego 6 | // +build !purego 7 | 8 | package value 9 | 10 | import ( 11 | "reflect" 12 | "unsafe" 13 | ) 14 | 15 | // Pointer is an opaque typed pointer and is guaranteed to be comparable. 16 | type Pointer struct { 17 | p unsafe.Pointer 18 | t reflect.Type 19 | } 20 | 21 | // PointerOf returns a Pointer from v, which must be a 22 | // reflect.Ptr, reflect.Slice, or reflect.Map. 23 | func PointerOf(v reflect.Value) Pointer { 24 | // The proper representation of a pointer is unsafe.Pointer, 25 | // which is necessary if the GC ever uses a moving collector. 26 | return Pointer{unsafe.Pointer(v.Pointer()), v.Type()} 27 | } 28 | 29 | // IsNil reports whether the pointer is nil. 30 | func (p Pointer) IsNil() bool { 31 | return p.p == nil 32 | } 33 | 34 | // Uintptr returns the pointer as a uintptr. 35 | func (p Pointer) Uintptr() uintptr { 36 | return uintptr(p.p) 37 | } 38 | -------------------------------------------------------------------------------- /vendor/github.com/google/go-cmp/cmp/internal/value/sort.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017, The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package value 6 | 7 | import ( 8 | "fmt" 9 | "math" 10 | "reflect" 11 | "sort" 12 | ) 13 | 14 | // SortKeys sorts a list of map keys, deduplicating keys if necessary. 15 | // The type of each value must be comparable. 16 | func SortKeys(vs []reflect.Value) []reflect.Value { 17 | if len(vs) == 0 { 18 | return vs 19 | } 20 | 21 | // Sort the map keys. 22 | sort.SliceStable(vs, func(i, j int) bool { return isLess(vs[i], vs[j]) }) 23 | 24 | // Deduplicate keys (fails for NaNs). 25 | vs2 := vs[:1] 26 | for _, v := range vs[1:] { 27 | if isLess(vs2[len(vs2)-1], v) { 28 | vs2 = append(vs2, v) 29 | } 30 | } 31 | return vs2 32 | } 33 | 34 | // isLess is a generic function for sorting arbitrary map keys. 35 | // The inputs must be of the same type and must be comparable. 36 | func isLess(x, y reflect.Value) bool { 37 | switch x.Type().Kind() { 38 | case reflect.Bool: 39 | return !x.Bool() && y.Bool() 40 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 41 | return x.Int() < y.Int() 42 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: 43 | return x.Uint() < y.Uint() 44 | case reflect.Float32, reflect.Float64: 45 | // NOTE: This does not sort -0 as less than +0 46 | // since Go maps treat -0 and +0 as equal keys. 47 | fx, fy := x.Float(), y.Float() 48 | return fx < fy || math.IsNaN(fx) && !math.IsNaN(fy) 49 | case reflect.Complex64, reflect.Complex128: 50 | cx, cy := x.Complex(), y.Complex() 51 | rx, ix, ry, iy := real(cx), imag(cx), real(cy), imag(cy) 52 | if rx == ry || (math.IsNaN(rx) && math.IsNaN(ry)) { 53 | return ix < iy || math.IsNaN(ix) && !math.IsNaN(iy) 54 | } 55 | return rx < ry || math.IsNaN(rx) && !math.IsNaN(ry) 56 | case reflect.Ptr, reflect.UnsafePointer, reflect.Chan: 57 | return x.Pointer() < y.Pointer() 58 | case reflect.String: 59 | return x.String() < y.String() 60 | case reflect.Array: 61 | for i := 0; i < x.Len(); i++ { 62 | if isLess(x.Index(i), y.Index(i)) { 63 | return true 64 | } 65 | if isLess(y.Index(i), x.Index(i)) { 66 | return false 67 | } 68 | } 69 | return false 70 | case reflect.Struct: 71 | for i := 0; i < x.NumField(); i++ { 72 | if isLess(x.Field(i), y.Field(i)) { 73 | return true 74 | } 75 | if isLess(y.Field(i), x.Field(i)) { 76 | return false 77 | } 78 | } 79 | return false 80 | case reflect.Interface: 81 | vx, vy := x.Elem(), y.Elem() 82 | if !vx.IsValid() || !vy.IsValid() { 83 | return !vx.IsValid() && vy.IsValid() 84 | } 85 | tx, ty := vx.Type(), vy.Type() 86 | if tx == ty { 87 | return isLess(x.Elem(), y.Elem()) 88 | } 89 | if tx.Kind() != ty.Kind() { 90 | return vx.Kind() < vy.Kind() 91 | } 92 | if tx.String() != ty.String() { 93 | return tx.String() < ty.String() 94 | } 95 | if tx.PkgPath() != ty.PkgPath() { 96 | return tx.PkgPath() < ty.PkgPath() 97 | } 98 | // This can happen in rare situations, so we fallback to just comparing 99 | // the unique pointer for a reflect.Type. This guarantees deterministic 100 | // ordering within a program, but it is obviously not stable. 101 | return reflect.ValueOf(vx.Type()).Pointer() < reflect.ValueOf(vy.Type()).Pointer() 102 | default: 103 | // Must be Func, Map, or Slice; which are not comparable. 104 | panic(fmt.Sprintf("%T is not comparable", x.Type())) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /vendor/github.com/google/go-cmp/cmp/path.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017, The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package cmp 6 | 7 | import ( 8 | "fmt" 9 | "reflect" 10 | "strings" 11 | "unicode" 12 | "unicode/utf8" 13 | 14 | "github.com/google/go-cmp/cmp/internal/value" 15 | ) 16 | 17 | // Path is a list of PathSteps describing the sequence of operations to get 18 | // from some root type to the current position in the value tree. 19 | // The first Path element is always an operation-less PathStep that exists 20 | // simply to identify the initial type. 21 | // 22 | // When traversing structs with embedded structs, the embedded struct will 23 | // always be accessed as a field before traversing the fields of the 24 | // embedded struct themselves. That is, an exported field from the 25 | // embedded struct will never be accessed directly from the parent struct. 26 | type Path []PathStep 27 | 28 | // PathStep is a union-type for specific operations to traverse 29 | // a value's tree structure. Users of this package never need to implement 30 | // these types as values of this type will be returned by this package. 31 | // 32 | // Implementations of this interface are 33 | // StructField, SliceIndex, MapIndex, Indirect, TypeAssertion, and Transform. 34 | type PathStep interface { 35 | String() string 36 | 37 | // Type is the resulting type after performing the path step. 38 | Type() reflect.Type 39 | 40 | // Values is the resulting values after performing the path step. 41 | // The type of each valid value is guaranteed to be identical to Type. 42 | // 43 | // In some cases, one or both may be invalid or have restrictions: 44 | // - For StructField, both are not interface-able if the current field 45 | // is unexported and the struct type is not explicitly permitted by 46 | // an Exporter to traverse unexported fields. 47 | // - For SliceIndex, one may be invalid if an element is missing from 48 | // either the x or y slice. 49 | // - For MapIndex, one may be invalid if an entry is missing from 50 | // either the x or y map. 51 | // 52 | // The provided values must not be mutated. 53 | Values() (vx, vy reflect.Value) 54 | } 55 | 56 | var ( 57 | _ PathStep = StructField{} 58 | _ PathStep = SliceIndex{} 59 | _ PathStep = MapIndex{} 60 | _ PathStep = Indirect{} 61 | _ PathStep = TypeAssertion{} 62 | _ PathStep = Transform{} 63 | ) 64 | 65 | func (pa *Path) push(s PathStep) { 66 | *pa = append(*pa, s) 67 | } 68 | 69 | func (pa *Path) pop() { 70 | *pa = (*pa)[:len(*pa)-1] 71 | } 72 | 73 | // Last returns the last PathStep in the Path. 74 | // If the path is empty, this returns a non-nil PathStep that reports a nil Type. 75 | func (pa Path) Last() PathStep { 76 | return pa.Index(-1) 77 | } 78 | 79 | // Index returns the ith step in the Path and supports negative indexing. 80 | // A negative index starts counting from the tail of the Path such that -1 81 | // refers to the last step, -2 refers to the second-to-last step, and so on. 82 | // If index is invalid, this returns a non-nil PathStep that reports a nil Type. 83 | func (pa Path) Index(i int) PathStep { 84 | if i < 0 { 85 | i = len(pa) + i 86 | } 87 | if i < 0 || i >= len(pa) { 88 | return pathStep{} 89 | } 90 | return pa[i] 91 | } 92 | 93 | // String returns the simplified path to a node. 94 | // The simplified path only contains struct field accesses. 95 | // 96 | // For example: 97 | // 98 | // MyMap.MySlices.MyField 99 | func (pa Path) String() string { 100 | var ss []string 101 | for _, s := range pa { 102 | if _, ok := s.(StructField); ok { 103 | ss = append(ss, s.String()) 104 | } 105 | } 106 | return strings.TrimPrefix(strings.Join(ss, ""), ".") 107 | } 108 | 109 | // GoString returns the path to a specific node using Go syntax. 110 | // 111 | // For example: 112 | // 113 | // (*root.MyMap["key"].(*mypkg.MyStruct).MySlices)[2][3].MyField 114 | func (pa Path) GoString() string { 115 | var ssPre, ssPost []string 116 | var numIndirect int 117 | for i, s := range pa { 118 | var nextStep PathStep 119 | if i+1 < len(pa) { 120 | nextStep = pa[i+1] 121 | } 122 | switch s := s.(type) { 123 | case Indirect: 124 | numIndirect++ 125 | pPre, pPost := "(", ")" 126 | switch nextStep.(type) { 127 | case Indirect: 128 | continue // Next step is indirection, so let them batch up 129 | case StructField: 130 | numIndirect-- // Automatic indirection on struct fields 131 | case nil: 132 | pPre, pPost = "", "" // Last step; no need for parenthesis 133 | } 134 | if numIndirect > 0 { 135 | ssPre = append(ssPre, pPre+strings.Repeat("*", numIndirect)) 136 | ssPost = append(ssPost, pPost) 137 | } 138 | numIndirect = 0 139 | continue 140 | case Transform: 141 | ssPre = append(ssPre, s.trans.name+"(") 142 | ssPost = append(ssPost, ")") 143 | continue 144 | } 145 | ssPost = append(ssPost, s.String()) 146 | } 147 | for i, j := 0, len(ssPre)-1; i < j; i, j = i+1, j-1 { 148 | ssPre[i], ssPre[j] = ssPre[j], ssPre[i] 149 | } 150 | return strings.Join(ssPre, "") + strings.Join(ssPost, "") 151 | } 152 | 153 | type pathStep struct { 154 | typ reflect.Type 155 | vx, vy reflect.Value 156 | } 157 | 158 | func (ps pathStep) Type() reflect.Type { return ps.typ } 159 | func (ps pathStep) Values() (vx, vy reflect.Value) { return ps.vx, ps.vy } 160 | func (ps pathStep) String() string { 161 | if ps.typ == nil { 162 | return "" 163 | } 164 | s := value.TypeString(ps.typ, false) 165 | if s == "" || strings.ContainsAny(s, "{}\n") { 166 | return "root" // Type too simple or complex to print 167 | } 168 | return fmt.Sprintf("{%s}", s) 169 | } 170 | 171 | // StructField represents a struct field access on a field called Name. 172 | type StructField struct{ *structField } 173 | type structField struct { 174 | pathStep 175 | name string 176 | idx int 177 | 178 | // These fields are used for forcibly accessing an unexported field. 179 | // pvx, pvy, and field are only valid if unexported is true. 180 | unexported bool 181 | mayForce bool // Forcibly allow visibility 182 | paddr bool // Was parent addressable? 183 | pvx, pvy reflect.Value // Parent values (always addressable) 184 | field reflect.StructField // Field information 185 | } 186 | 187 | func (sf StructField) Type() reflect.Type { return sf.typ } 188 | func (sf StructField) Values() (vx, vy reflect.Value) { 189 | if !sf.unexported { 190 | return sf.vx, sf.vy // CanInterface reports true 191 | } 192 | 193 | // Forcibly obtain read-write access to an unexported struct field. 194 | if sf.mayForce { 195 | vx = retrieveUnexportedField(sf.pvx, sf.field, sf.paddr) 196 | vy = retrieveUnexportedField(sf.pvy, sf.field, sf.paddr) 197 | return vx, vy // CanInterface reports true 198 | } 199 | return sf.vx, sf.vy // CanInterface reports false 200 | } 201 | func (sf StructField) String() string { return fmt.Sprintf(".%s", sf.name) } 202 | 203 | // Name is the field name. 204 | func (sf StructField) Name() string { return sf.name } 205 | 206 | // Index is the index of the field in the parent struct type. 207 | // See reflect.Type.Field. 208 | func (sf StructField) Index() int { return sf.idx } 209 | 210 | // SliceIndex is an index operation on a slice or array at some index Key. 211 | type SliceIndex struct{ *sliceIndex } 212 | type sliceIndex struct { 213 | pathStep 214 | xkey, ykey int 215 | isSlice bool // False for reflect.Array 216 | } 217 | 218 | func (si SliceIndex) Type() reflect.Type { return si.typ } 219 | func (si SliceIndex) Values() (vx, vy reflect.Value) { return si.vx, si.vy } 220 | func (si SliceIndex) String() string { 221 | switch { 222 | case si.xkey == si.ykey: 223 | return fmt.Sprintf("[%d]", si.xkey) 224 | case si.ykey == -1: 225 | // [5->?] means "I don't know where X[5] went" 226 | return fmt.Sprintf("[%d->?]", si.xkey) 227 | case si.xkey == -1: 228 | // [?->3] means "I don't know where Y[3] came from" 229 | return fmt.Sprintf("[?->%d]", si.ykey) 230 | default: 231 | // [5->3] means "X[5] moved to Y[3]" 232 | return fmt.Sprintf("[%d->%d]", si.xkey, si.ykey) 233 | } 234 | } 235 | 236 | // Key is the index key; it may return -1 if in a split state 237 | func (si SliceIndex) Key() int { 238 | if si.xkey != si.ykey { 239 | return -1 240 | } 241 | return si.xkey 242 | } 243 | 244 | // SplitKeys are the indexes for indexing into slices in the 245 | // x and y values, respectively. These indexes may differ due to the 246 | // insertion or removal of an element in one of the slices, causing 247 | // all of the indexes to be shifted. If an index is -1, then that 248 | // indicates that the element does not exist in the associated slice. 249 | // 250 | // Key is guaranteed to return -1 if and only if the indexes returned 251 | // by SplitKeys are not the same. SplitKeys will never return -1 for 252 | // both indexes. 253 | func (si SliceIndex) SplitKeys() (ix, iy int) { return si.xkey, si.ykey } 254 | 255 | // MapIndex is an index operation on a map at some index Key. 256 | type MapIndex struct{ *mapIndex } 257 | type mapIndex struct { 258 | pathStep 259 | key reflect.Value 260 | } 261 | 262 | func (mi MapIndex) Type() reflect.Type { return mi.typ } 263 | func (mi MapIndex) Values() (vx, vy reflect.Value) { return mi.vx, mi.vy } 264 | func (mi MapIndex) String() string { return fmt.Sprintf("[%#v]", mi.key) } 265 | 266 | // Key is the value of the map key. 267 | func (mi MapIndex) Key() reflect.Value { return mi.key } 268 | 269 | // Indirect represents pointer indirection on the parent type. 270 | type Indirect struct{ *indirect } 271 | type indirect struct { 272 | pathStep 273 | } 274 | 275 | func (in Indirect) Type() reflect.Type { return in.typ } 276 | func (in Indirect) Values() (vx, vy reflect.Value) { return in.vx, in.vy } 277 | func (in Indirect) String() string { return "*" } 278 | 279 | // TypeAssertion represents a type assertion on an interface. 280 | type TypeAssertion struct{ *typeAssertion } 281 | type typeAssertion struct { 282 | pathStep 283 | } 284 | 285 | func (ta TypeAssertion) Type() reflect.Type { return ta.typ } 286 | func (ta TypeAssertion) Values() (vx, vy reflect.Value) { return ta.vx, ta.vy } 287 | func (ta TypeAssertion) String() string { return fmt.Sprintf(".(%v)", value.TypeString(ta.typ, false)) } 288 | 289 | // Transform is a transformation from the parent type to the current type. 290 | type Transform struct{ *transform } 291 | type transform struct { 292 | pathStep 293 | trans *transformer 294 | } 295 | 296 | func (tf Transform) Type() reflect.Type { return tf.typ } 297 | func (tf Transform) Values() (vx, vy reflect.Value) { return tf.vx, tf.vy } 298 | func (tf Transform) String() string { return fmt.Sprintf("%s()", tf.trans.name) } 299 | 300 | // Name is the name of the Transformer. 301 | func (tf Transform) Name() string { return tf.trans.name } 302 | 303 | // Func is the function pointer to the transformer function. 304 | func (tf Transform) Func() reflect.Value { return tf.trans.fnc } 305 | 306 | // Option returns the originally constructed Transformer option. 307 | // The == operator can be used to detect the exact option used. 308 | func (tf Transform) Option() Option { return tf.trans } 309 | 310 | // pointerPath represents a dual-stack of pointers encountered when 311 | // recursively traversing the x and y values. This data structure supports 312 | // detection of cycles and determining whether the cycles are equal. 313 | // In Go, cycles can occur via pointers, slices, and maps. 314 | // 315 | // The pointerPath uses a map to represent a stack; where descension into a 316 | // pointer pushes the address onto the stack, and ascension from a pointer 317 | // pops the address from the stack. Thus, when traversing into a pointer from 318 | // reflect.Ptr, reflect.Slice element, or reflect.Map, we can detect cycles 319 | // by checking whether the pointer has already been visited. The cycle detection 320 | // uses a separate stack for the x and y values. 321 | // 322 | // If a cycle is detected we need to determine whether the two pointers 323 | // should be considered equal. The definition of equality chosen by Equal 324 | // requires two graphs to have the same structure. To determine this, both the 325 | // x and y values must have a cycle where the previous pointers were also 326 | // encountered together as a pair. 327 | // 328 | // Semantically, this is equivalent to augmenting Indirect, SliceIndex, and 329 | // MapIndex with pointer information for the x and y values. 330 | // Suppose px and py are two pointers to compare, we then search the 331 | // Path for whether px was ever encountered in the Path history of x, and 332 | // similarly so with py. If either side has a cycle, the comparison is only 333 | // equal if both px and py have a cycle resulting from the same PathStep. 334 | // 335 | // Using a map as a stack is more performant as we can perform cycle detection 336 | // in O(1) instead of O(N) where N is len(Path). 337 | type pointerPath struct { 338 | // mx is keyed by x pointers, where the value is the associated y pointer. 339 | mx map[value.Pointer]value.Pointer 340 | // my is keyed by y pointers, where the value is the associated x pointer. 341 | my map[value.Pointer]value.Pointer 342 | } 343 | 344 | func (p *pointerPath) Init() { 345 | p.mx = make(map[value.Pointer]value.Pointer) 346 | p.my = make(map[value.Pointer]value.Pointer) 347 | } 348 | 349 | // Push indicates intent to descend into pointers vx and vy where 350 | // visited reports whether either has been seen before. If visited before, 351 | // equal reports whether both pointers were encountered together. 352 | // Pop must be called if and only if the pointers were never visited. 353 | // 354 | // The pointers vx and vy must be a reflect.Ptr, reflect.Slice, or reflect.Map 355 | // and be non-nil. 356 | func (p pointerPath) Push(vx, vy reflect.Value) (equal, visited bool) { 357 | px := value.PointerOf(vx) 358 | py := value.PointerOf(vy) 359 | _, ok1 := p.mx[px] 360 | _, ok2 := p.my[py] 361 | if ok1 || ok2 { 362 | equal = p.mx[px] == py && p.my[py] == px // Pointers paired together 363 | return equal, true 364 | } 365 | p.mx[px] = py 366 | p.my[py] = px 367 | return false, false 368 | } 369 | 370 | // Pop ascends from pointers vx and vy. 371 | func (p pointerPath) Pop(vx, vy reflect.Value) { 372 | delete(p.mx, value.PointerOf(vx)) 373 | delete(p.my, value.PointerOf(vy)) 374 | } 375 | 376 | // isExported reports whether the identifier is exported. 377 | func isExported(id string) bool { 378 | r, _ := utf8.DecodeRuneInString(id) 379 | return unicode.IsUpper(r) 380 | } 381 | -------------------------------------------------------------------------------- /vendor/github.com/google/go-cmp/cmp/report.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017, The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package cmp 6 | 7 | // defaultReporter implements the reporter interface. 8 | // 9 | // As Equal serially calls the PushStep, Report, and PopStep methods, the 10 | // defaultReporter constructs a tree-based representation of the compared value 11 | // and the result of each comparison (see valueNode). 12 | // 13 | // When the String method is called, the FormatDiff method transforms the 14 | // valueNode tree into a textNode tree, which is a tree-based representation 15 | // of the textual output (see textNode). 16 | // 17 | // Lastly, the textNode.String method produces the final report as a string. 18 | type defaultReporter struct { 19 | root *valueNode 20 | curr *valueNode 21 | } 22 | 23 | func (r *defaultReporter) PushStep(ps PathStep) { 24 | r.curr = r.curr.PushStep(ps) 25 | if r.root == nil { 26 | r.root = r.curr 27 | } 28 | } 29 | func (r *defaultReporter) Report(rs Result) { 30 | r.curr.Report(rs) 31 | } 32 | func (r *defaultReporter) PopStep() { 33 | r.curr = r.curr.PopStep() 34 | } 35 | 36 | // String provides a full report of the differences detected as a structured 37 | // literal in pseudo-Go syntax. String may only be called after the entire tree 38 | // has been traversed. 39 | func (r *defaultReporter) String() string { 40 | assert(r.root != nil && r.curr == nil) 41 | if r.root.NumDiff == 0 { 42 | return "" 43 | } 44 | ptrs := new(pointerReferences) 45 | text := formatOptions{}.FormatDiff(r.root, ptrs) 46 | resolveReferences(text) 47 | return text.String() 48 | } 49 | 50 | func assert(ok bool) { 51 | if !ok { 52 | panic("assertion failure") 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /vendor/github.com/google/go-cmp/cmp/report_compare.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019, The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package cmp 6 | 7 | import ( 8 | "fmt" 9 | "reflect" 10 | ) 11 | 12 | // numContextRecords is the number of surrounding equal records to print. 13 | const numContextRecords = 2 14 | 15 | type diffMode byte 16 | 17 | const ( 18 | diffUnknown diffMode = 0 19 | diffIdentical diffMode = ' ' 20 | diffRemoved diffMode = '-' 21 | diffInserted diffMode = '+' 22 | ) 23 | 24 | type typeMode int 25 | 26 | const ( 27 | // emitType always prints the type. 28 | emitType typeMode = iota 29 | // elideType never prints the type. 30 | elideType 31 | // autoType prints the type only for composite kinds 32 | // (i.e., structs, slices, arrays, and maps). 33 | autoType 34 | ) 35 | 36 | type formatOptions struct { 37 | // DiffMode controls the output mode of FormatDiff. 38 | // 39 | // If diffUnknown, then produce a diff of the x and y values. 40 | // If diffIdentical, then emit values as if they were equal. 41 | // If diffRemoved, then only emit x values (ignoring y values). 42 | // If diffInserted, then only emit y values (ignoring x values). 43 | DiffMode diffMode 44 | 45 | // TypeMode controls whether to print the type for the current node. 46 | // 47 | // As a general rule of thumb, we always print the type of the next node 48 | // after an interface, and always elide the type of the next node after 49 | // a slice or map node. 50 | TypeMode typeMode 51 | 52 | // formatValueOptions are options specific to printing reflect.Values. 53 | formatValueOptions 54 | } 55 | 56 | func (opts formatOptions) WithDiffMode(d diffMode) formatOptions { 57 | opts.DiffMode = d 58 | return opts 59 | } 60 | func (opts formatOptions) WithTypeMode(t typeMode) formatOptions { 61 | opts.TypeMode = t 62 | return opts 63 | } 64 | func (opts formatOptions) WithVerbosity(level int) formatOptions { 65 | opts.VerbosityLevel = level 66 | opts.LimitVerbosity = true 67 | return opts 68 | } 69 | func (opts formatOptions) verbosity() uint { 70 | switch { 71 | case opts.VerbosityLevel < 0: 72 | return 0 73 | case opts.VerbosityLevel > 16: 74 | return 16 // some reasonable maximum to avoid shift overflow 75 | default: 76 | return uint(opts.VerbosityLevel) 77 | } 78 | } 79 | 80 | const maxVerbosityPreset = 6 81 | 82 | // verbosityPreset modifies the verbosity settings given an index 83 | // between 0 and maxVerbosityPreset, inclusive. 84 | func verbosityPreset(opts formatOptions, i int) formatOptions { 85 | opts.VerbosityLevel = int(opts.verbosity()) + 2*i 86 | if i > 0 { 87 | opts.AvoidStringer = true 88 | } 89 | if i >= maxVerbosityPreset { 90 | opts.PrintAddresses = true 91 | opts.QualifiedNames = true 92 | } 93 | return opts 94 | } 95 | 96 | // FormatDiff converts a valueNode tree into a textNode tree, where the later 97 | // is a textual representation of the differences detected in the former. 98 | func (opts formatOptions) FormatDiff(v *valueNode, ptrs *pointerReferences) (out textNode) { 99 | if opts.DiffMode == diffIdentical { 100 | opts = opts.WithVerbosity(1) 101 | } else if opts.verbosity() < 3 { 102 | opts = opts.WithVerbosity(3) 103 | } 104 | 105 | // Check whether we have specialized formatting for this node. 106 | // This is not necessary, but helpful for producing more readable outputs. 107 | if opts.CanFormatDiffSlice(v) { 108 | return opts.FormatDiffSlice(v) 109 | } 110 | 111 | var parentKind reflect.Kind 112 | if v.parent != nil && v.parent.TransformerName == "" { 113 | parentKind = v.parent.Type.Kind() 114 | } 115 | 116 | // For leaf nodes, format the value based on the reflect.Values alone. 117 | // As a special case, treat equal []byte as a leaf nodes. 118 | isBytes := v.Type.Kind() == reflect.Slice && v.Type.Elem() == byteType 119 | isEqualBytes := isBytes && v.NumDiff+v.NumIgnored+v.NumTransformed == 0 120 | if v.MaxDepth == 0 || isEqualBytes { 121 | switch opts.DiffMode { 122 | case diffUnknown, diffIdentical: 123 | // Format Equal. 124 | if v.NumDiff == 0 { 125 | outx := opts.FormatValue(v.ValueX, parentKind, ptrs) 126 | outy := opts.FormatValue(v.ValueY, parentKind, ptrs) 127 | if v.NumIgnored > 0 && v.NumSame == 0 { 128 | return textEllipsis 129 | } else if outx.Len() < outy.Len() { 130 | return outx 131 | } else { 132 | return outy 133 | } 134 | } 135 | 136 | // Format unequal. 137 | assert(opts.DiffMode == diffUnknown) 138 | var list textList 139 | outx := opts.WithTypeMode(elideType).FormatValue(v.ValueX, parentKind, ptrs) 140 | outy := opts.WithTypeMode(elideType).FormatValue(v.ValueY, parentKind, ptrs) 141 | for i := 0; i <= maxVerbosityPreset && outx != nil && outy != nil && outx.Equal(outy); i++ { 142 | opts2 := verbosityPreset(opts, i).WithTypeMode(elideType) 143 | outx = opts2.FormatValue(v.ValueX, parentKind, ptrs) 144 | outy = opts2.FormatValue(v.ValueY, parentKind, ptrs) 145 | } 146 | if outx != nil { 147 | list = append(list, textRecord{Diff: '-', Value: outx}) 148 | } 149 | if outy != nil { 150 | list = append(list, textRecord{Diff: '+', Value: outy}) 151 | } 152 | return opts.WithTypeMode(emitType).FormatType(v.Type, list) 153 | case diffRemoved: 154 | return opts.FormatValue(v.ValueX, parentKind, ptrs) 155 | case diffInserted: 156 | return opts.FormatValue(v.ValueY, parentKind, ptrs) 157 | default: 158 | panic("invalid diff mode") 159 | } 160 | } 161 | 162 | // Register slice element to support cycle detection. 163 | if parentKind == reflect.Slice { 164 | ptrRefs := ptrs.PushPair(v.ValueX, v.ValueY, opts.DiffMode, true) 165 | defer ptrs.Pop() 166 | defer func() { out = wrapTrunkReferences(ptrRefs, out) }() 167 | } 168 | 169 | // Descend into the child value node. 170 | if v.TransformerName != "" { 171 | out := opts.WithTypeMode(emitType).FormatDiff(v.Value, ptrs) 172 | out = &textWrap{Prefix: "Inverse(" + v.TransformerName + ", ", Value: out, Suffix: ")"} 173 | return opts.FormatType(v.Type, out) 174 | } else { 175 | switch k := v.Type.Kind(); k { 176 | case reflect.Struct, reflect.Array, reflect.Slice: 177 | out = opts.formatDiffList(v.Records, k, ptrs) 178 | out = opts.FormatType(v.Type, out) 179 | case reflect.Map: 180 | // Register map to support cycle detection. 181 | ptrRefs := ptrs.PushPair(v.ValueX, v.ValueY, opts.DiffMode, false) 182 | defer ptrs.Pop() 183 | 184 | out = opts.formatDiffList(v.Records, k, ptrs) 185 | out = wrapTrunkReferences(ptrRefs, out) 186 | out = opts.FormatType(v.Type, out) 187 | case reflect.Ptr: 188 | // Register pointer to support cycle detection. 189 | ptrRefs := ptrs.PushPair(v.ValueX, v.ValueY, opts.DiffMode, false) 190 | defer ptrs.Pop() 191 | 192 | out = opts.FormatDiff(v.Value, ptrs) 193 | out = wrapTrunkReferences(ptrRefs, out) 194 | out = &textWrap{Prefix: "&", Value: out} 195 | case reflect.Interface: 196 | out = opts.WithTypeMode(emitType).FormatDiff(v.Value, ptrs) 197 | default: 198 | panic(fmt.Sprintf("%v cannot have children", k)) 199 | } 200 | return out 201 | } 202 | } 203 | 204 | func (opts formatOptions) formatDiffList(recs []reportRecord, k reflect.Kind, ptrs *pointerReferences) textNode { 205 | // Derive record name based on the data structure kind. 206 | var name string 207 | var formatKey func(reflect.Value) string 208 | switch k { 209 | case reflect.Struct: 210 | name = "field" 211 | opts = opts.WithTypeMode(autoType) 212 | formatKey = func(v reflect.Value) string { return v.String() } 213 | case reflect.Slice, reflect.Array: 214 | name = "element" 215 | opts = opts.WithTypeMode(elideType) 216 | formatKey = func(reflect.Value) string { return "" } 217 | case reflect.Map: 218 | name = "entry" 219 | opts = opts.WithTypeMode(elideType) 220 | formatKey = func(v reflect.Value) string { return formatMapKey(v, false, ptrs) } 221 | } 222 | 223 | maxLen := -1 224 | if opts.LimitVerbosity { 225 | if opts.DiffMode == diffIdentical { 226 | maxLen = ((1 << opts.verbosity()) >> 1) << 2 // 0, 4, 8, 16, 32, etc... 227 | } else { 228 | maxLen = (1 << opts.verbosity()) << 1 // 2, 4, 8, 16, 32, 64, etc... 229 | } 230 | opts.VerbosityLevel-- 231 | } 232 | 233 | // Handle unification. 234 | switch opts.DiffMode { 235 | case diffIdentical, diffRemoved, diffInserted: 236 | var list textList 237 | var deferredEllipsis bool // Add final "..." to indicate records were dropped 238 | for _, r := range recs { 239 | if len(list) == maxLen { 240 | deferredEllipsis = true 241 | break 242 | } 243 | 244 | // Elide struct fields that are zero value. 245 | if k == reflect.Struct { 246 | var isZero bool 247 | switch opts.DiffMode { 248 | case diffIdentical: 249 | isZero = r.Value.ValueX.IsZero() || r.Value.ValueY.IsZero() 250 | case diffRemoved: 251 | isZero = r.Value.ValueX.IsZero() 252 | case diffInserted: 253 | isZero = r.Value.ValueY.IsZero() 254 | } 255 | if isZero { 256 | continue 257 | } 258 | } 259 | // Elide ignored nodes. 260 | if r.Value.NumIgnored > 0 && r.Value.NumSame+r.Value.NumDiff == 0 { 261 | deferredEllipsis = !(k == reflect.Slice || k == reflect.Array) 262 | if !deferredEllipsis { 263 | list.AppendEllipsis(diffStats{}) 264 | } 265 | continue 266 | } 267 | if out := opts.FormatDiff(r.Value, ptrs); out != nil { 268 | list = append(list, textRecord{Key: formatKey(r.Key), Value: out}) 269 | } 270 | } 271 | if deferredEllipsis { 272 | list.AppendEllipsis(diffStats{}) 273 | } 274 | return &textWrap{Prefix: "{", Value: list, Suffix: "}"} 275 | case diffUnknown: 276 | default: 277 | panic("invalid diff mode") 278 | } 279 | 280 | // Handle differencing. 281 | var numDiffs int 282 | var list textList 283 | var keys []reflect.Value // invariant: len(list) == len(keys) 284 | groups := coalesceAdjacentRecords(name, recs) 285 | maxGroup := diffStats{Name: name} 286 | for i, ds := range groups { 287 | if maxLen >= 0 && numDiffs >= maxLen { 288 | maxGroup = maxGroup.Append(ds) 289 | continue 290 | } 291 | 292 | // Handle equal records. 293 | if ds.NumDiff() == 0 { 294 | // Compute the number of leading and trailing records to print. 295 | var numLo, numHi int 296 | numEqual := ds.NumIgnored + ds.NumIdentical 297 | for numLo < numContextRecords && numLo+numHi < numEqual && i != 0 { 298 | if r := recs[numLo].Value; r.NumIgnored > 0 && r.NumSame+r.NumDiff == 0 { 299 | break 300 | } 301 | numLo++ 302 | } 303 | for numHi < numContextRecords && numLo+numHi < numEqual && i != len(groups)-1 { 304 | if r := recs[numEqual-numHi-1].Value; r.NumIgnored > 0 && r.NumSame+r.NumDiff == 0 { 305 | break 306 | } 307 | numHi++ 308 | } 309 | if numEqual-(numLo+numHi) == 1 && ds.NumIgnored == 0 { 310 | numHi++ // Avoid pointless coalescing of a single equal record 311 | } 312 | 313 | // Format the equal values. 314 | for _, r := range recs[:numLo] { 315 | out := opts.WithDiffMode(diffIdentical).FormatDiff(r.Value, ptrs) 316 | list = append(list, textRecord{Key: formatKey(r.Key), Value: out}) 317 | keys = append(keys, r.Key) 318 | } 319 | if numEqual > numLo+numHi { 320 | ds.NumIdentical -= numLo + numHi 321 | list.AppendEllipsis(ds) 322 | for len(keys) < len(list) { 323 | keys = append(keys, reflect.Value{}) 324 | } 325 | } 326 | for _, r := range recs[numEqual-numHi : numEqual] { 327 | out := opts.WithDiffMode(diffIdentical).FormatDiff(r.Value, ptrs) 328 | list = append(list, textRecord{Key: formatKey(r.Key), Value: out}) 329 | keys = append(keys, r.Key) 330 | } 331 | recs = recs[numEqual:] 332 | continue 333 | } 334 | 335 | // Handle unequal records. 336 | for _, r := range recs[:ds.NumDiff()] { 337 | switch { 338 | case opts.CanFormatDiffSlice(r.Value): 339 | out := opts.FormatDiffSlice(r.Value) 340 | list = append(list, textRecord{Key: formatKey(r.Key), Value: out}) 341 | keys = append(keys, r.Key) 342 | case r.Value.NumChildren == r.Value.MaxDepth: 343 | outx := opts.WithDiffMode(diffRemoved).FormatDiff(r.Value, ptrs) 344 | outy := opts.WithDiffMode(diffInserted).FormatDiff(r.Value, ptrs) 345 | for i := 0; i <= maxVerbosityPreset && outx != nil && outy != nil && outx.Equal(outy); i++ { 346 | opts2 := verbosityPreset(opts, i) 347 | outx = opts2.WithDiffMode(diffRemoved).FormatDiff(r.Value, ptrs) 348 | outy = opts2.WithDiffMode(diffInserted).FormatDiff(r.Value, ptrs) 349 | } 350 | if outx != nil { 351 | list = append(list, textRecord{Diff: diffRemoved, Key: formatKey(r.Key), Value: outx}) 352 | keys = append(keys, r.Key) 353 | } 354 | if outy != nil { 355 | list = append(list, textRecord{Diff: diffInserted, Key: formatKey(r.Key), Value: outy}) 356 | keys = append(keys, r.Key) 357 | } 358 | default: 359 | out := opts.FormatDiff(r.Value, ptrs) 360 | list = append(list, textRecord{Key: formatKey(r.Key), Value: out}) 361 | keys = append(keys, r.Key) 362 | } 363 | } 364 | recs = recs[ds.NumDiff():] 365 | numDiffs += ds.NumDiff() 366 | } 367 | if maxGroup.IsZero() { 368 | assert(len(recs) == 0) 369 | } else { 370 | list.AppendEllipsis(maxGroup) 371 | for len(keys) < len(list) { 372 | keys = append(keys, reflect.Value{}) 373 | } 374 | } 375 | assert(len(list) == len(keys)) 376 | 377 | // For maps, the default formatting logic uses fmt.Stringer which may 378 | // produce ambiguous output. Avoid calling String to disambiguate. 379 | if k == reflect.Map { 380 | var ambiguous bool 381 | seenKeys := map[string]reflect.Value{} 382 | for i, currKey := range keys { 383 | if currKey.IsValid() { 384 | strKey := list[i].Key 385 | prevKey, seen := seenKeys[strKey] 386 | if seen && prevKey.CanInterface() && currKey.CanInterface() { 387 | ambiguous = prevKey.Interface() != currKey.Interface() 388 | if ambiguous { 389 | break 390 | } 391 | } 392 | seenKeys[strKey] = currKey 393 | } 394 | } 395 | if ambiguous { 396 | for i, k := range keys { 397 | if k.IsValid() { 398 | list[i].Key = formatMapKey(k, true, ptrs) 399 | } 400 | } 401 | } 402 | } 403 | 404 | return &textWrap{Prefix: "{", Value: list, Suffix: "}"} 405 | } 406 | 407 | // coalesceAdjacentRecords coalesces the list of records into groups of 408 | // adjacent equal, or unequal counts. 409 | func coalesceAdjacentRecords(name string, recs []reportRecord) (groups []diffStats) { 410 | var prevCase int // Arbitrary index into which case last occurred 411 | lastStats := func(i int) *diffStats { 412 | if prevCase != i { 413 | groups = append(groups, diffStats{Name: name}) 414 | prevCase = i 415 | } 416 | return &groups[len(groups)-1] 417 | } 418 | for _, r := range recs { 419 | switch rv := r.Value; { 420 | case rv.NumIgnored > 0 && rv.NumSame+rv.NumDiff == 0: 421 | lastStats(1).NumIgnored++ 422 | case rv.NumDiff == 0: 423 | lastStats(1).NumIdentical++ 424 | case rv.NumDiff > 0 && !rv.ValueY.IsValid(): 425 | lastStats(2).NumRemoved++ 426 | case rv.NumDiff > 0 && !rv.ValueX.IsValid(): 427 | lastStats(2).NumInserted++ 428 | default: 429 | lastStats(2).NumModified++ 430 | } 431 | } 432 | return groups 433 | } 434 | -------------------------------------------------------------------------------- /vendor/github.com/google/go-cmp/cmp/report_references.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020, The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package cmp 6 | 7 | import ( 8 | "fmt" 9 | "reflect" 10 | "strings" 11 | 12 | "github.com/google/go-cmp/cmp/internal/flags" 13 | "github.com/google/go-cmp/cmp/internal/value" 14 | ) 15 | 16 | const ( 17 | pointerDelimPrefix = "⟪" 18 | pointerDelimSuffix = "⟫" 19 | ) 20 | 21 | // formatPointer prints the address of the pointer. 22 | func formatPointer(p value.Pointer, withDelims bool) string { 23 | v := p.Uintptr() 24 | if flags.Deterministic { 25 | v = 0xdeadf00f // Only used for stable testing purposes 26 | } 27 | if withDelims { 28 | return pointerDelimPrefix + formatHex(uint64(v)) + pointerDelimSuffix 29 | } 30 | return formatHex(uint64(v)) 31 | } 32 | 33 | // pointerReferences is a stack of pointers visited so far. 34 | type pointerReferences [][2]value.Pointer 35 | 36 | func (ps *pointerReferences) PushPair(vx, vy reflect.Value, d diffMode, deref bool) (pp [2]value.Pointer) { 37 | if deref && vx.IsValid() { 38 | vx = vx.Addr() 39 | } 40 | if deref && vy.IsValid() { 41 | vy = vy.Addr() 42 | } 43 | switch d { 44 | case diffUnknown, diffIdentical: 45 | pp = [2]value.Pointer{value.PointerOf(vx), value.PointerOf(vy)} 46 | case diffRemoved: 47 | pp = [2]value.Pointer{value.PointerOf(vx), value.Pointer{}} 48 | case diffInserted: 49 | pp = [2]value.Pointer{value.Pointer{}, value.PointerOf(vy)} 50 | } 51 | *ps = append(*ps, pp) 52 | return pp 53 | } 54 | 55 | func (ps *pointerReferences) Push(v reflect.Value) (p value.Pointer, seen bool) { 56 | p = value.PointerOf(v) 57 | for _, pp := range *ps { 58 | if p == pp[0] || p == pp[1] { 59 | return p, true 60 | } 61 | } 62 | *ps = append(*ps, [2]value.Pointer{p, p}) 63 | return p, false 64 | } 65 | 66 | func (ps *pointerReferences) Pop() { 67 | *ps = (*ps)[:len(*ps)-1] 68 | } 69 | 70 | // trunkReferences is metadata for a textNode indicating that the sub-tree 71 | // represents the value for either pointer in a pair of references. 72 | type trunkReferences struct{ pp [2]value.Pointer } 73 | 74 | // trunkReference is metadata for a textNode indicating that the sub-tree 75 | // represents the value for the given pointer reference. 76 | type trunkReference struct{ p value.Pointer } 77 | 78 | // leafReference is metadata for a textNode indicating that the value is 79 | // truncated as it refers to another part of the tree (i.e., a trunk). 80 | type leafReference struct{ p value.Pointer } 81 | 82 | func wrapTrunkReferences(pp [2]value.Pointer, s textNode) textNode { 83 | switch { 84 | case pp[0].IsNil(): 85 | return &textWrap{Value: s, Metadata: trunkReference{pp[1]}} 86 | case pp[1].IsNil(): 87 | return &textWrap{Value: s, Metadata: trunkReference{pp[0]}} 88 | case pp[0] == pp[1]: 89 | return &textWrap{Value: s, Metadata: trunkReference{pp[0]}} 90 | default: 91 | return &textWrap{Value: s, Metadata: trunkReferences{pp}} 92 | } 93 | } 94 | func wrapTrunkReference(p value.Pointer, printAddress bool, s textNode) textNode { 95 | var prefix string 96 | if printAddress { 97 | prefix = formatPointer(p, true) 98 | } 99 | return &textWrap{Prefix: prefix, Value: s, Metadata: trunkReference{p}} 100 | } 101 | func makeLeafReference(p value.Pointer, printAddress bool) textNode { 102 | out := &textWrap{Prefix: "(", Value: textEllipsis, Suffix: ")"} 103 | var prefix string 104 | if printAddress { 105 | prefix = formatPointer(p, true) 106 | } 107 | return &textWrap{Prefix: prefix, Value: out, Metadata: leafReference{p}} 108 | } 109 | 110 | // resolveReferences walks the textNode tree searching for any leaf reference 111 | // metadata and resolves each against the corresponding trunk references. 112 | // Since pointer addresses in memory are not particularly readable to the user, 113 | // it replaces each pointer value with an arbitrary and unique reference ID. 114 | func resolveReferences(s textNode) { 115 | var walkNodes func(textNode, func(textNode)) 116 | walkNodes = func(s textNode, f func(textNode)) { 117 | f(s) 118 | switch s := s.(type) { 119 | case *textWrap: 120 | walkNodes(s.Value, f) 121 | case textList: 122 | for _, r := range s { 123 | walkNodes(r.Value, f) 124 | } 125 | } 126 | } 127 | 128 | // Collect all trunks and leaves with reference metadata. 129 | var trunks, leaves []*textWrap 130 | walkNodes(s, func(s textNode) { 131 | if s, ok := s.(*textWrap); ok { 132 | switch s.Metadata.(type) { 133 | case leafReference: 134 | leaves = append(leaves, s) 135 | case trunkReference, trunkReferences: 136 | trunks = append(trunks, s) 137 | } 138 | } 139 | }) 140 | 141 | // No leaf references to resolve. 142 | if len(leaves) == 0 { 143 | return 144 | } 145 | 146 | // Collect the set of all leaf references to resolve. 147 | leafPtrs := make(map[value.Pointer]bool) 148 | for _, leaf := range leaves { 149 | leafPtrs[leaf.Metadata.(leafReference).p] = true 150 | } 151 | 152 | // Collect the set of trunk pointers that are always paired together. 153 | // This allows us to assign a single ID to both pointers for brevity. 154 | // If a pointer in a pair ever occurs by itself or as a different pair, 155 | // then the pair is broken. 156 | pairedTrunkPtrs := make(map[value.Pointer]value.Pointer) 157 | unpair := func(p value.Pointer) { 158 | if !pairedTrunkPtrs[p].IsNil() { 159 | pairedTrunkPtrs[pairedTrunkPtrs[p]] = value.Pointer{} // invalidate other half 160 | } 161 | pairedTrunkPtrs[p] = value.Pointer{} // invalidate this half 162 | } 163 | for _, trunk := range trunks { 164 | switch p := trunk.Metadata.(type) { 165 | case trunkReference: 166 | unpair(p.p) // standalone pointer cannot be part of a pair 167 | case trunkReferences: 168 | p0, ok0 := pairedTrunkPtrs[p.pp[0]] 169 | p1, ok1 := pairedTrunkPtrs[p.pp[1]] 170 | switch { 171 | case !ok0 && !ok1: 172 | // Register the newly seen pair. 173 | pairedTrunkPtrs[p.pp[0]] = p.pp[1] 174 | pairedTrunkPtrs[p.pp[1]] = p.pp[0] 175 | case ok0 && ok1 && p0 == p.pp[1] && p1 == p.pp[0]: 176 | // Exact pair already seen; do nothing. 177 | default: 178 | // Pair conflicts with some other pair; break all pairs. 179 | unpair(p.pp[0]) 180 | unpair(p.pp[1]) 181 | } 182 | } 183 | } 184 | 185 | // Correlate each pointer referenced by leaves to a unique identifier, 186 | // and print the IDs for each trunk that matches those pointers. 187 | var nextID uint 188 | ptrIDs := make(map[value.Pointer]uint) 189 | newID := func() uint { 190 | id := nextID 191 | nextID++ 192 | return id 193 | } 194 | for _, trunk := range trunks { 195 | switch p := trunk.Metadata.(type) { 196 | case trunkReference: 197 | if print := leafPtrs[p.p]; print { 198 | id, ok := ptrIDs[p.p] 199 | if !ok { 200 | id = newID() 201 | ptrIDs[p.p] = id 202 | } 203 | trunk.Prefix = updateReferencePrefix(trunk.Prefix, formatReference(id)) 204 | } 205 | case trunkReferences: 206 | print0 := leafPtrs[p.pp[0]] 207 | print1 := leafPtrs[p.pp[1]] 208 | if print0 || print1 { 209 | id0, ok0 := ptrIDs[p.pp[0]] 210 | id1, ok1 := ptrIDs[p.pp[1]] 211 | isPair := pairedTrunkPtrs[p.pp[0]] == p.pp[1] && pairedTrunkPtrs[p.pp[1]] == p.pp[0] 212 | if isPair { 213 | var id uint 214 | assert(ok0 == ok1) // must be seen together or not at all 215 | if ok0 { 216 | assert(id0 == id1) // must have the same ID 217 | id = id0 218 | } else { 219 | id = newID() 220 | ptrIDs[p.pp[0]] = id 221 | ptrIDs[p.pp[1]] = id 222 | } 223 | trunk.Prefix = updateReferencePrefix(trunk.Prefix, formatReference(id)) 224 | } else { 225 | if print0 && !ok0 { 226 | id0 = newID() 227 | ptrIDs[p.pp[0]] = id0 228 | } 229 | if print1 && !ok1 { 230 | id1 = newID() 231 | ptrIDs[p.pp[1]] = id1 232 | } 233 | switch { 234 | case print0 && print1: 235 | trunk.Prefix = updateReferencePrefix(trunk.Prefix, formatReference(id0)+","+formatReference(id1)) 236 | case print0: 237 | trunk.Prefix = updateReferencePrefix(trunk.Prefix, formatReference(id0)) 238 | case print1: 239 | trunk.Prefix = updateReferencePrefix(trunk.Prefix, formatReference(id1)) 240 | } 241 | } 242 | } 243 | } 244 | } 245 | 246 | // Update all leaf references with the unique identifier. 247 | for _, leaf := range leaves { 248 | if id, ok := ptrIDs[leaf.Metadata.(leafReference).p]; ok { 249 | leaf.Prefix = updateReferencePrefix(leaf.Prefix, formatReference(id)) 250 | } 251 | } 252 | } 253 | 254 | func formatReference(id uint) string { 255 | return fmt.Sprintf("ref#%d", id) 256 | } 257 | 258 | func updateReferencePrefix(prefix, ref string) string { 259 | if prefix == "" { 260 | return pointerDelimPrefix + ref + pointerDelimSuffix 261 | } 262 | suffix := strings.TrimPrefix(prefix, pointerDelimPrefix) 263 | return pointerDelimPrefix + ref + ": " + suffix 264 | } 265 | -------------------------------------------------------------------------------- /vendor/github.com/google/go-cmp/cmp/report_reflect.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019, The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package cmp 6 | 7 | import ( 8 | "bytes" 9 | "fmt" 10 | "reflect" 11 | "strconv" 12 | "strings" 13 | "unicode" 14 | "unicode/utf8" 15 | 16 | "github.com/google/go-cmp/cmp/internal/value" 17 | ) 18 | 19 | var ( 20 | anyType = reflect.TypeOf((*interface{})(nil)).Elem() 21 | stringType = reflect.TypeOf((*string)(nil)).Elem() 22 | bytesType = reflect.TypeOf((*[]byte)(nil)).Elem() 23 | byteType = reflect.TypeOf((*byte)(nil)).Elem() 24 | ) 25 | 26 | type formatValueOptions struct { 27 | // AvoidStringer controls whether to avoid calling custom stringer 28 | // methods like error.Error or fmt.Stringer.String. 29 | AvoidStringer bool 30 | 31 | // PrintAddresses controls whether to print the address of all pointers, 32 | // slice elements, and maps. 33 | PrintAddresses bool 34 | 35 | // QualifiedNames controls whether FormatType uses the fully qualified name 36 | // (including the full package path as opposed to just the package name). 37 | QualifiedNames bool 38 | 39 | // VerbosityLevel controls the amount of output to produce. 40 | // A higher value produces more output. A value of zero or lower produces 41 | // no output (represented using an ellipsis). 42 | // If LimitVerbosity is false, then the level is treated as infinite. 43 | VerbosityLevel int 44 | 45 | // LimitVerbosity specifies that formatting should respect VerbosityLevel. 46 | LimitVerbosity bool 47 | } 48 | 49 | // FormatType prints the type as if it were wrapping s. 50 | // This may return s as-is depending on the current type and TypeMode mode. 51 | func (opts formatOptions) FormatType(t reflect.Type, s textNode) textNode { 52 | // Check whether to emit the type or not. 53 | switch opts.TypeMode { 54 | case autoType: 55 | switch t.Kind() { 56 | case reflect.Struct, reflect.Slice, reflect.Array, reflect.Map: 57 | if s.Equal(textNil) { 58 | return s 59 | } 60 | default: 61 | return s 62 | } 63 | if opts.DiffMode == diffIdentical { 64 | return s // elide type for identical nodes 65 | } 66 | case elideType: 67 | return s 68 | } 69 | 70 | // Determine the type label, applying special handling for unnamed types. 71 | typeName := value.TypeString(t, opts.QualifiedNames) 72 | if t.Name() == "" { 73 | // According to Go grammar, certain type literals contain symbols that 74 | // do not strongly bind to the next lexicographical token (e.g., *T). 75 | switch t.Kind() { 76 | case reflect.Chan, reflect.Func, reflect.Ptr: 77 | typeName = "(" + typeName + ")" 78 | } 79 | } 80 | return &textWrap{Prefix: typeName, Value: wrapParens(s)} 81 | } 82 | 83 | // wrapParens wraps s with a set of parenthesis, but avoids it if the 84 | // wrapped node itself is already surrounded by a pair of parenthesis or braces. 85 | // It handles unwrapping one level of pointer-reference nodes. 86 | func wrapParens(s textNode) textNode { 87 | var refNode *textWrap 88 | if s2, ok := s.(*textWrap); ok { 89 | // Unwrap a single pointer reference node. 90 | switch s2.Metadata.(type) { 91 | case leafReference, trunkReference, trunkReferences: 92 | refNode = s2 93 | if s3, ok := refNode.Value.(*textWrap); ok { 94 | s2 = s3 95 | } 96 | } 97 | 98 | // Already has delimiters that make parenthesis unnecessary. 99 | hasParens := strings.HasPrefix(s2.Prefix, "(") && strings.HasSuffix(s2.Suffix, ")") 100 | hasBraces := strings.HasPrefix(s2.Prefix, "{") && strings.HasSuffix(s2.Suffix, "}") 101 | if hasParens || hasBraces { 102 | return s 103 | } 104 | } 105 | if refNode != nil { 106 | refNode.Value = &textWrap{Prefix: "(", Value: refNode.Value, Suffix: ")"} 107 | return s 108 | } 109 | return &textWrap{Prefix: "(", Value: s, Suffix: ")"} 110 | } 111 | 112 | // FormatValue prints the reflect.Value, taking extra care to avoid descending 113 | // into pointers already in ptrs. As pointers are visited, ptrs is also updated. 114 | func (opts formatOptions) FormatValue(v reflect.Value, parentKind reflect.Kind, ptrs *pointerReferences) (out textNode) { 115 | if !v.IsValid() { 116 | return nil 117 | } 118 | t := v.Type() 119 | 120 | // Check slice element for cycles. 121 | if parentKind == reflect.Slice { 122 | ptrRef, visited := ptrs.Push(v.Addr()) 123 | if visited { 124 | return makeLeafReference(ptrRef, false) 125 | } 126 | defer ptrs.Pop() 127 | defer func() { out = wrapTrunkReference(ptrRef, false, out) }() 128 | } 129 | 130 | // Check whether there is an Error or String method to call. 131 | if !opts.AvoidStringer && v.CanInterface() { 132 | // Avoid calling Error or String methods on nil receivers since many 133 | // implementations crash when doing so. 134 | if (t.Kind() != reflect.Ptr && t.Kind() != reflect.Interface) || !v.IsNil() { 135 | var prefix, strVal string 136 | func() { 137 | // Swallow and ignore any panics from String or Error. 138 | defer func() { recover() }() 139 | switch v := v.Interface().(type) { 140 | case error: 141 | strVal = v.Error() 142 | prefix = "e" 143 | case fmt.Stringer: 144 | strVal = v.String() 145 | prefix = "s" 146 | } 147 | }() 148 | if prefix != "" { 149 | return opts.formatString(prefix, strVal) 150 | } 151 | } 152 | } 153 | 154 | // Check whether to explicitly wrap the result with the type. 155 | var skipType bool 156 | defer func() { 157 | if !skipType { 158 | out = opts.FormatType(t, out) 159 | } 160 | }() 161 | 162 | switch t.Kind() { 163 | case reflect.Bool: 164 | return textLine(fmt.Sprint(v.Bool())) 165 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 166 | return textLine(fmt.Sprint(v.Int())) 167 | case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64: 168 | return textLine(fmt.Sprint(v.Uint())) 169 | case reflect.Uint8: 170 | if parentKind == reflect.Slice || parentKind == reflect.Array { 171 | return textLine(formatHex(v.Uint())) 172 | } 173 | return textLine(fmt.Sprint(v.Uint())) 174 | case reflect.Uintptr: 175 | return textLine(formatHex(v.Uint())) 176 | case reflect.Float32, reflect.Float64: 177 | return textLine(fmt.Sprint(v.Float())) 178 | case reflect.Complex64, reflect.Complex128: 179 | return textLine(fmt.Sprint(v.Complex())) 180 | case reflect.String: 181 | return opts.formatString("", v.String()) 182 | case reflect.UnsafePointer, reflect.Chan, reflect.Func: 183 | return textLine(formatPointer(value.PointerOf(v), true)) 184 | case reflect.Struct: 185 | var list textList 186 | v := makeAddressable(v) // needed for retrieveUnexportedField 187 | maxLen := v.NumField() 188 | if opts.LimitVerbosity { 189 | maxLen = ((1 << opts.verbosity()) >> 1) << 2 // 0, 4, 8, 16, 32, etc... 190 | opts.VerbosityLevel-- 191 | } 192 | for i := 0; i < v.NumField(); i++ { 193 | vv := v.Field(i) 194 | if vv.IsZero() { 195 | continue // Elide fields with zero values 196 | } 197 | if len(list) == maxLen { 198 | list.AppendEllipsis(diffStats{}) 199 | break 200 | } 201 | sf := t.Field(i) 202 | if supportExporters && !isExported(sf.Name) { 203 | vv = retrieveUnexportedField(v, sf, true) 204 | } 205 | s := opts.WithTypeMode(autoType).FormatValue(vv, t.Kind(), ptrs) 206 | list = append(list, textRecord{Key: sf.Name, Value: s}) 207 | } 208 | return &textWrap{Prefix: "{", Value: list, Suffix: "}"} 209 | case reflect.Slice: 210 | if v.IsNil() { 211 | return textNil 212 | } 213 | 214 | // Check whether this is a []byte of text data. 215 | if t.Elem() == byteType { 216 | b := v.Bytes() 217 | isPrintSpace := func(r rune) bool { return unicode.IsPrint(r) || unicode.IsSpace(r) } 218 | if len(b) > 0 && utf8.Valid(b) && len(bytes.TrimFunc(b, isPrintSpace)) == 0 { 219 | out = opts.formatString("", string(b)) 220 | skipType = true 221 | return opts.FormatType(t, out) 222 | } 223 | } 224 | 225 | fallthrough 226 | case reflect.Array: 227 | maxLen := v.Len() 228 | if opts.LimitVerbosity { 229 | maxLen = ((1 << opts.verbosity()) >> 1) << 2 // 0, 4, 8, 16, 32, etc... 230 | opts.VerbosityLevel-- 231 | } 232 | var list textList 233 | for i := 0; i < v.Len(); i++ { 234 | if len(list) == maxLen { 235 | list.AppendEllipsis(diffStats{}) 236 | break 237 | } 238 | s := opts.WithTypeMode(elideType).FormatValue(v.Index(i), t.Kind(), ptrs) 239 | list = append(list, textRecord{Value: s}) 240 | } 241 | 242 | out = &textWrap{Prefix: "{", Value: list, Suffix: "}"} 243 | if t.Kind() == reflect.Slice && opts.PrintAddresses { 244 | header := fmt.Sprintf("ptr:%v, len:%d, cap:%d", formatPointer(value.PointerOf(v), false), v.Len(), v.Cap()) 245 | out = &textWrap{Prefix: pointerDelimPrefix + header + pointerDelimSuffix, Value: out} 246 | } 247 | return out 248 | case reflect.Map: 249 | if v.IsNil() { 250 | return textNil 251 | } 252 | 253 | // Check pointer for cycles. 254 | ptrRef, visited := ptrs.Push(v) 255 | if visited { 256 | return makeLeafReference(ptrRef, opts.PrintAddresses) 257 | } 258 | defer ptrs.Pop() 259 | 260 | maxLen := v.Len() 261 | if opts.LimitVerbosity { 262 | maxLen = ((1 << opts.verbosity()) >> 1) << 2 // 0, 4, 8, 16, 32, etc... 263 | opts.VerbosityLevel-- 264 | } 265 | var list textList 266 | for _, k := range value.SortKeys(v.MapKeys()) { 267 | if len(list) == maxLen { 268 | list.AppendEllipsis(diffStats{}) 269 | break 270 | } 271 | sk := formatMapKey(k, false, ptrs) 272 | sv := opts.WithTypeMode(elideType).FormatValue(v.MapIndex(k), t.Kind(), ptrs) 273 | list = append(list, textRecord{Key: sk, Value: sv}) 274 | } 275 | 276 | out = &textWrap{Prefix: "{", Value: list, Suffix: "}"} 277 | out = wrapTrunkReference(ptrRef, opts.PrintAddresses, out) 278 | return out 279 | case reflect.Ptr: 280 | if v.IsNil() { 281 | return textNil 282 | } 283 | 284 | // Check pointer for cycles. 285 | ptrRef, visited := ptrs.Push(v) 286 | if visited { 287 | out = makeLeafReference(ptrRef, opts.PrintAddresses) 288 | return &textWrap{Prefix: "&", Value: out} 289 | } 290 | defer ptrs.Pop() 291 | 292 | // Skip the name only if this is an unnamed pointer type. 293 | // Otherwise taking the address of a value does not reproduce 294 | // the named pointer type. 295 | if v.Type().Name() == "" { 296 | skipType = true // Let the underlying value print the type instead 297 | } 298 | out = opts.FormatValue(v.Elem(), t.Kind(), ptrs) 299 | out = wrapTrunkReference(ptrRef, opts.PrintAddresses, out) 300 | out = &textWrap{Prefix: "&", Value: out} 301 | return out 302 | case reflect.Interface: 303 | if v.IsNil() { 304 | return textNil 305 | } 306 | // Interfaces accept different concrete types, 307 | // so configure the underlying value to explicitly print the type. 308 | return opts.WithTypeMode(emitType).FormatValue(v.Elem(), t.Kind(), ptrs) 309 | default: 310 | panic(fmt.Sprintf("%v kind not handled", v.Kind())) 311 | } 312 | } 313 | 314 | func (opts formatOptions) formatString(prefix, s string) textNode { 315 | maxLen := len(s) 316 | maxLines := strings.Count(s, "\n") + 1 317 | if opts.LimitVerbosity { 318 | maxLen = (1 << opts.verbosity()) << 5 // 32, 64, 128, 256, etc... 319 | maxLines = (1 << opts.verbosity()) << 2 // 4, 8, 16, 32, 64, etc... 320 | } 321 | 322 | // For multiline strings, use the triple-quote syntax, 323 | // but only use it when printing removed or inserted nodes since 324 | // we only want the extra verbosity for those cases. 325 | lines := strings.Split(strings.TrimSuffix(s, "\n"), "\n") 326 | isTripleQuoted := len(lines) >= 4 && (opts.DiffMode == '-' || opts.DiffMode == '+') 327 | for i := 0; i < len(lines) && isTripleQuoted; i++ { 328 | lines[i] = strings.TrimPrefix(strings.TrimSuffix(lines[i], "\r"), "\r") // trim leading/trailing carriage returns for legacy Windows endline support 329 | isPrintable := func(r rune) bool { 330 | return unicode.IsPrint(r) || r == '\t' // specially treat tab as printable 331 | } 332 | line := lines[i] 333 | isTripleQuoted = !strings.HasPrefix(strings.TrimPrefix(line, prefix), `"""`) && !strings.HasPrefix(line, "...") && strings.TrimFunc(line, isPrintable) == "" && len(line) <= maxLen 334 | } 335 | if isTripleQuoted { 336 | var list textList 337 | list = append(list, textRecord{Diff: opts.DiffMode, Value: textLine(prefix + `"""`), ElideComma: true}) 338 | for i, line := range lines { 339 | if numElided := len(lines) - i; i == maxLines-1 && numElided > 1 { 340 | comment := commentString(fmt.Sprintf("%d elided lines", numElided)) 341 | list = append(list, textRecord{Diff: opts.DiffMode, Value: textEllipsis, ElideComma: true, Comment: comment}) 342 | break 343 | } 344 | list = append(list, textRecord{Diff: opts.DiffMode, Value: textLine(line), ElideComma: true}) 345 | } 346 | list = append(list, textRecord{Diff: opts.DiffMode, Value: textLine(prefix + `"""`), ElideComma: true}) 347 | return &textWrap{Prefix: "(", Value: list, Suffix: ")"} 348 | } 349 | 350 | // Format the string as a single-line quoted string. 351 | if len(s) > maxLen+len(textEllipsis) { 352 | return textLine(prefix + formatString(s[:maxLen]) + string(textEllipsis)) 353 | } 354 | return textLine(prefix + formatString(s)) 355 | } 356 | 357 | // formatMapKey formats v as if it were a map key. 358 | // The result is guaranteed to be a single line. 359 | func formatMapKey(v reflect.Value, disambiguate bool, ptrs *pointerReferences) string { 360 | var opts formatOptions 361 | opts.DiffMode = diffIdentical 362 | opts.TypeMode = elideType 363 | opts.PrintAddresses = disambiguate 364 | opts.AvoidStringer = disambiguate 365 | opts.QualifiedNames = disambiguate 366 | opts.VerbosityLevel = maxVerbosityPreset 367 | opts.LimitVerbosity = true 368 | s := opts.FormatValue(v, reflect.Map, ptrs).String() 369 | return strings.TrimSpace(s) 370 | } 371 | 372 | // formatString prints s as a double-quoted or backtick-quoted string. 373 | func formatString(s string) string { 374 | // Use quoted string if it the same length as a raw string literal. 375 | // Otherwise, attempt to use the raw string form. 376 | qs := strconv.Quote(s) 377 | if len(qs) == 1+len(s)+1 { 378 | return qs 379 | } 380 | 381 | // Disallow newlines to ensure output is a single line. 382 | // Only allow printable runes for readability purposes. 383 | rawInvalid := func(r rune) bool { 384 | return r == '`' || r == '\n' || !(unicode.IsPrint(r) || r == '\t') 385 | } 386 | if utf8.ValidString(s) && strings.IndexFunc(s, rawInvalid) < 0 { 387 | return "`" + s + "`" 388 | } 389 | return qs 390 | } 391 | 392 | // formatHex prints u as a hexadecimal integer in Go notation. 393 | func formatHex(u uint64) string { 394 | var f string 395 | switch { 396 | case u <= 0xff: 397 | f = "0x%02x" 398 | case u <= 0xffff: 399 | f = "0x%04x" 400 | case u <= 0xffffff: 401 | f = "0x%06x" 402 | case u <= 0xffffffff: 403 | f = "0x%08x" 404 | case u <= 0xffffffffff: 405 | f = "0x%010x" 406 | case u <= 0xffffffffffff: 407 | f = "0x%012x" 408 | case u <= 0xffffffffffffff: 409 | f = "0x%014x" 410 | case u <= 0xffffffffffffffff: 411 | f = "0x%016x" 412 | } 413 | return fmt.Sprintf(f, u) 414 | } 415 | -------------------------------------------------------------------------------- /vendor/github.com/google/go-cmp/cmp/report_text.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019, The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package cmp 6 | 7 | import ( 8 | "bytes" 9 | "fmt" 10 | "math/rand" 11 | "strings" 12 | "time" 13 | "unicode/utf8" 14 | 15 | "github.com/google/go-cmp/cmp/internal/flags" 16 | ) 17 | 18 | var randBool = rand.New(rand.NewSource(time.Now().Unix())).Intn(2) == 0 19 | 20 | const maxColumnLength = 80 21 | 22 | type indentMode int 23 | 24 | func (n indentMode) appendIndent(b []byte, d diffMode) []byte { 25 | // The output of Diff is documented as being unstable to provide future 26 | // flexibility in changing the output for more humanly readable reports. 27 | // This logic intentionally introduces instability to the exact output 28 | // so that users can detect accidental reliance on stability early on, 29 | // rather than much later when an actual change to the format occurs. 30 | if flags.Deterministic || randBool { 31 | // Use regular spaces (U+0020). 32 | switch d { 33 | case diffUnknown, diffIdentical: 34 | b = append(b, " "...) 35 | case diffRemoved: 36 | b = append(b, "- "...) 37 | case diffInserted: 38 | b = append(b, "+ "...) 39 | } 40 | } else { 41 | // Use non-breaking spaces (U+00a0). 42 | switch d { 43 | case diffUnknown, diffIdentical: 44 | b = append(b, "  "...) 45 | case diffRemoved: 46 | b = append(b, "- "...) 47 | case diffInserted: 48 | b = append(b, "+ "...) 49 | } 50 | } 51 | return repeatCount(n).appendChar(b, '\t') 52 | } 53 | 54 | type repeatCount int 55 | 56 | func (n repeatCount) appendChar(b []byte, c byte) []byte { 57 | for ; n > 0; n-- { 58 | b = append(b, c) 59 | } 60 | return b 61 | } 62 | 63 | // textNode is a simplified tree-based representation of structured text. 64 | // Possible node types are textWrap, textList, or textLine. 65 | type textNode interface { 66 | // Len reports the length in bytes of a single-line version of the tree. 67 | // Nested textRecord.Diff and textRecord.Comment fields are ignored. 68 | Len() int 69 | // Equal reports whether the two trees are structurally identical. 70 | // Nested textRecord.Diff and textRecord.Comment fields are compared. 71 | Equal(textNode) bool 72 | // String returns the string representation of the text tree. 73 | // It is not guaranteed that len(x.String()) == x.Len(), 74 | // nor that x.String() == y.String() implies that x.Equal(y). 75 | String() string 76 | 77 | // formatCompactTo formats the contents of the tree as a single-line string 78 | // to the provided buffer. Any nested textRecord.Diff and textRecord.Comment 79 | // fields are ignored. 80 | // 81 | // However, not all nodes in the tree should be collapsed as a single-line. 82 | // If a node can be collapsed as a single-line, it is replaced by a textLine 83 | // node. Since the top-level node cannot replace itself, this also returns 84 | // the current node itself. 85 | // 86 | // This does not mutate the receiver. 87 | formatCompactTo([]byte, diffMode) ([]byte, textNode) 88 | // formatExpandedTo formats the contents of the tree as a multi-line string 89 | // to the provided buffer. In order for column alignment to operate well, 90 | // formatCompactTo must be called before calling formatExpandedTo. 91 | formatExpandedTo([]byte, diffMode, indentMode) []byte 92 | } 93 | 94 | // textWrap is a wrapper that concatenates a prefix and/or a suffix 95 | // to the underlying node. 96 | type textWrap struct { 97 | Prefix string // e.g., "bytes.Buffer{" 98 | Value textNode // textWrap | textList | textLine 99 | Suffix string // e.g., "}" 100 | Metadata interface{} // arbitrary metadata; has no effect on formatting 101 | } 102 | 103 | func (s *textWrap) Len() int { 104 | return len(s.Prefix) + s.Value.Len() + len(s.Suffix) 105 | } 106 | func (s1 *textWrap) Equal(s2 textNode) bool { 107 | if s2, ok := s2.(*textWrap); ok { 108 | return s1.Prefix == s2.Prefix && s1.Value.Equal(s2.Value) && s1.Suffix == s2.Suffix 109 | } 110 | return false 111 | } 112 | func (s *textWrap) String() string { 113 | var d diffMode 114 | var n indentMode 115 | _, s2 := s.formatCompactTo(nil, d) 116 | b := n.appendIndent(nil, d) // Leading indent 117 | b = s2.formatExpandedTo(b, d, n) // Main body 118 | b = append(b, '\n') // Trailing newline 119 | return string(b) 120 | } 121 | func (s *textWrap) formatCompactTo(b []byte, d diffMode) ([]byte, textNode) { 122 | n0 := len(b) // Original buffer length 123 | b = append(b, s.Prefix...) 124 | b, s.Value = s.Value.formatCompactTo(b, d) 125 | b = append(b, s.Suffix...) 126 | if _, ok := s.Value.(textLine); ok { 127 | return b, textLine(b[n0:]) 128 | } 129 | return b, s 130 | } 131 | func (s *textWrap) formatExpandedTo(b []byte, d diffMode, n indentMode) []byte { 132 | b = append(b, s.Prefix...) 133 | b = s.Value.formatExpandedTo(b, d, n) 134 | b = append(b, s.Suffix...) 135 | return b 136 | } 137 | 138 | // textList is a comma-separated list of textWrap or textLine nodes. 139 | // The list may be formatted as multi-lines or single-line at the discretion 140 | // of the textList.formatCompactTo method. 141 | type textList []textRecord 142 | type textRecord struct { 143 | Diff diffMode // e.g., 0 or '-' or '+' 144 | Key string // e.g., "MyField" 145 | Value textNode // textWrap | textLine 146 | ElideComma bool // avoid trailing comma 147 | Comment fmt.Stringer // e.g., "6 identical fields" 148 | } 149 | 150 | // AppendEllipsis appends a new ellipsis node to the list if none already 151 | // exists at the end. If cs is non-zero it coalesces the statistics with the 152 | // previous diffStats. 153 | func (s *textList) AppendEllipsis(ds diffStats) { 154 | hasStats := !ds.IsZero() 155 | if len(*s) == 0 || !(*s)[len(*s)-1].Value.Equal(textEllipsis) { 156 | if hasStats { 157 | *s = append(*s, textRecord{Value: textEllipsis, ElideComma: true, Comment: ds}) 158 | } else { 159 | *s = append(*s, textRecord{Value: textEllipsis, ElideComma: true}) 160 | } 161 | return 162 | } 163 | if hasStats { 164 | (*s)[len(*s)-1].Comment = (*s)[len(*s)-1].Comment.(diffStats).Append(ds) 165 | } 166 | } 167 | 168 | func (s textList) Len() (n int) { 169 | for i, r := range s { 170 | n += len(r.Key) 171 | if r.Key != "" { 172 | n += len(": ") 173 | } 174 | n += r.Value.Len() 175 | if i < len(s)-1 { 176 | n += len(", ") 177 | } 178 | } 179 | return n 180 | } 181 | 182 | func (s1 textList) Equal(s2 textNode) bool { 183 | if s2, ok := s2.(textList); ok { 184 | if len(s1) != len(s2) { 185 | return false 186 | } 187 | for i := range s1 { 188 | r1, r2 := s1[i], s2[i] 189 | if !(r1.Diff == r2.Diff && r1.Key == r2.Key && r1.Value.Equal(r2.Value) && r1.Comment == r2.Comment) { 190 | return false 191 | } 192 | } 193 | return true 194 | } 195 | return false 196 | } 197 | 198 | func (s textList) String() string { 199 | return (&textWrap{Prefix: "{", Value: s, Suffix: "}"}).String() 200 | } 201 | 202 | func (s textList) formatCompactTo(b []byte, d diffMode) ([]byte, textNode) { 203 | s = append(textList(nil), s...) // Avoid mutating original 204 | 205 | // Determine whether we can collapse this list as a single line. 206 | n0 := len(b) // Original buffer length 207 | var multiLine bool 208 | for i, r := range s { 209 | if r.Diff == diffInserted || r.Diff == diffRemoved { 210 | multiLine = true 211 | } 212 | b = append(b, r.Key...) 213 | if r.Key != "" { 214 | b = append(b, ": "...) 215 | } 216 | b, s[i].Value = r.Value.formatCompactTo(b, d|r.Diff) 217 | if _, ok := s[i].Value.(textLine); !ok { 218 | multiLine = true 219 | } 220 | if r.Comment != nil { 221 | multiLine = true 222 | } 223 | if i < len(s)-1 { 224 | b = append(b, ", "...) 225 | } 226 | } 227 | // Force multi-lined output when printing a removed/inserted node that 228 | // is sufficiently long. 229 | if (d == diffInserted || d == diffRemoved) && len(b[n0:]) > maxColumnLength { 230 | multiLine = true 231 | } 232 | if !multiLine { 233 | return b, textLine(b[n0:]) 234 | } 235 | return b, s 236 | } 237 | 238 | func (s textList) formatExpandedTo(b []byte, d diffMode, n indentMode) []byte { 239 | alignKeyLens := s.alignLens( 240 | func(r textRecord) bool { 241 | _, isLine := r.Value.(textLine) 242 | return r.Key == "" || !isLine 243 | }, 244 | func(r textRecord) int { return utf8.RuneCountInString(r.Key) }, 245 | ) 246 | alignValueLens := s.alignLens( 247 | func(r textRecord) bool { 248 | _, isLine := r.Value.(textLine) 249 | return !isLine || r.Value.Equal(textEllipsis) || r.Comment == nil 250 | }, 251 | func(r textRecord) int { return utf8.RuneCount(r.Value.(textLine)) }, 252 | ) 253 | 254 | // Format lists of simple lists in a batched form. 255 | // If the list is sequence of only textLine values, 256 | // then batch multiple values on a single line. 257 | var isSimple bool 258 | for _, r := range s { 259 | _, isLine := r.Value.(textLine) 260 | isSimple = r.Diff == 0 && r.Key == "" && isLine && r.Comment == nil 261 | if !isSimple { 262 | break 263 | } 264 | } 265 | if isSimple { 266 | n++ 267 | var batch []byte 268 | emitBatch := func() { 269 | if len(batch) > 0 { 270 | b = n.appendIndent(append(b, '\n'), d) 271 | b = append(b, bytes.TrimRight(batch, " ")...) 272 | batch = batch[:0] 273 | } 274 | } 275 | for _, r := range s { 276 | line := r.Value.(textLine) 277 | if len(batch)+len(line)+len(", ") > maxColumnLength { 278 | emitBatch() 279 | } 280 | batch = append(batch, line...) 281 | batch = append(batch, ", "...) 282 | } 283 | emitBatch() 284 | n-- 285 | return n.appendIndent(append(b, '\n'), d) 286 | } 287 | 288 | // Format the list as a multi-lined output. 289 | n++ 290 | for i, r := range s { 291 | b = n.appendIndent(append(b, '\n'), d|r.Diff) 292 | if r.Key != "" { 293 | b = append(b, r.Key+": "...) 294 | } 295 | b = alignKeyLens[i].appendChar(b, ' ') 296 | 297 | b = r.Value.formatExpandedTo(b, d|r.Diff, n) 298 | if !r.ElideComma { 299 | b = append(b, ',') 300 | } 301 | b = alignValueLens[i].appendChar(b, ' ') 302 | 303 | if r.Comment != nil { 304 | b = append(b, " // "+r.Comment.String()...) 305 | } 306 | } 307 | n-- 308 | 309 | return n.appendIndent(append(b, '\n'), d) 310 | } 311 | 312 | func (s textList) alignLens( 313 | skipFunc func(textRecord) bool, 314 | lenFunc func(textRecord) int, 315 | ) []repeatCount { 316 | var startIdx, endIdx, maxLen int 317 | lens := make([]repeatCount, len(s)) 318 | for i, r := range s { 319 | if skipFunc(r) { 320 | for j := startIdx; j < endIdx && j < len(s); j++ { 321 | lens[j] = repeatCount(maxLen - lenFunc(s[j])) 322 | } 323 | startIdx, endIdx, maxLen = i+1, i+1, 0 324 | } else { 325 | if maxLen < lenFunc(r) { 326 | maxLen = lenFunc(r) 327 | } 328 | endIdx = i + 1 329 | } 330 | } 331 | for j := startIdx; j < endIdx && j < len(s); j++ { 332 | lens[j] = repeatCount(maxLen - lenFunc(s[j])) 333 | } 334 | return lens 335 | } 336 | 337 | // textLine is a single-line segment of text and is always a leaf node 338 | // in the textNode tree. 339 | type textLine []byte 340 | 341 | var ( 342 | textNil = textLine("nil") 343 | textEllipsis = textLine("...") 344 | ) 345 | 346 | func (s textLine) Len() int { 347 | return len(s) 348 | } 349 | func (s1 textLine) Equal(s2 textNode) bool { 350 | if s2, ok := s2.(textLine); ok { 351 | return bytes.Equal([]byte(s1), []byte(s2)) 352 | } 353 | return false 354 | } 355 | func (s textLine) String() string { 356 | return string(s) 357 | } 358 | func (s textLine) formatCompactTo(b []byte, d diffMode) ([]byte, textNode) { 359 | return append(b, s...), s 360 | } 361 | func (s textLine) formatExpandedTo(b []byte, _ diffMode, _ indentMode) []byte { 362 | return append(b, s...) 363 | } 364 | 365 | type diffStats struct { 366 | Name string 367 | NumIgnored int 368 | NumIdentical int 369 | NumRemoved int 370 | NumInserted int 371 | NumModified int 372 | } 373 | 374 | func (s diffStats) IsZero() bool { 375 | s.Name = "" 376 | return s == diffStats{} 377 | } 378 | 379 | func (s diffStats) NumDiff() int { 380 | return s.NumRemoved + s.NumInserted + s.NumModified 381 | } 382 | 383 | func (s diffStats) Append(ds diffStats) diffStats { 384 | assert(s.Name == ds.Name) 385 | s.NumIgnored += ds.NumIgnored 386 | s.NumIdentical += ds.NumIdentical 387 | s.NumRemoved += ds.NumRemoved 388 | s.NumInserted += ds.NumInserted 389 | s.NumModified += ds.NumModified 390 | return s 391 | } 392 | 393 | // String prints a humanly-readable summary of coalesced records. 394 | // 395 | // Example: 396 | // 397 | // diffStats{Name: "Field", NumIgnored: 5}.String() => "5 ignored fields" 398 | func (s diffStats) String() string { 399 | var ss []string 400 | var sum int 401 | labels := [...]string{"ignored", "identical", "removed", "inserted", "modified"} 402 | counts := [...]int{s.NumIgnored, s.NumIdentical, s.NumRemoved, s.NumInserted, s.NumModified} 403 | for i, n := range counts { 404 | if n > 0 { 405 | ss = append(ss, fmt.Sprintf("%d %v", n, labels[i])) 406 | } 407 | sum += n 408 | } 409 | 410 | // Pluralize the name (adjusting for some obscure English grammar rules). 411 | name := s.Name 412 | if sum > 1 { 413 | name += "s" 414 | if strings.HasSuffix(name, "ys") { 415 | name = name[:len(name)-2] + "ies" // e.g., "entrys" => "entries" 416 | } 417 | } 418 | 419 | // Format the list according to English grammar (with Oxford comma). 420 | switch n := len(ss); n { 421 | case 0: 422 | return "" 423 | case 1, 2: 424 | return strings.Join(ss, " and ") + " " + name 425 | default: 426 | return strings.Join(ss[:n-1], ", ") + ", and " + ss[n-1] + " " + name 427 | } 428 | } 429 | 430 | type commentString string 431 | 432 | func (s commentString) String() string { return string(s) } 433 | -------------------------------------------------------------------------------- /vendor/github.com/google/go-cmp/cmp/report_value.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019, The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package cmp 6 | 7 | import "reflect" 8 | 9 | // valueNode represents a single node within a report, which is a 10 | // structured representation of the value tree, containing information 11 | // regarding which nodes are equal or not. 12 | type valueNode struct { 13 | parent *valueNode 14 | 15 | Type reflect.Type 16 | ValueX reflect.Value 17 | ValueY reflect.Value 18 | 19 | // NumSame is the number of leaf nodes that are equal. 20 | // All descendants are equal only if NumDiff is 0. 21 | NumSame int 22 | // NumDiff is the number of leaf nodes that are not equal. 23 | NumDiff int 24 | // NumIgnored is the number of leaf nodes that are ignored. 25 | NumIgnored int 26 | // NumCompared is the number of leaf nodes that were compared 27 | // using an Equal method or Comparer function. 28 | NumCompared int 29 | // NumTransformed is the number of non-leaf nodes that were transformed. 30 | NumTransformed int 31 | // NumChildren is the number of transitive descendants of this node. 32 | // This counts from zero; thus, leaf nodes have no descendants. 33 | NumChildren int 34 | // MaxDepth is the maximum depth of the tree. This counts from zero; 35 | // thus, leaf nodes have a depth of zero. 36 | MaxDepth int 37 | 38 | // Records is a list of struct fields, slice elements, or map entries. 39 | Records []reportRecord // If populated, implies Value is not populated 40 | 41 | // Value is the result of a transformation, pointer indirect, of 42 | // type assertion. 43 | Value *valueNode // If populated, implies Records is not populated 44 | 45 | // TransformerName is the name of the transformer. 46 | TransformerName string // If non-empty, implies Value is populated 47 | } 48 | type reportRecord struct { 49 | Key reflect.Value // Invalid for slice element 50 | Value *valueNode 51 | } 52 | 53 | func (parent *valueNode) PushStep(ps PathStep) (child *valueNode) { 54 | vx, vy := ps.Values() 55 | child = &valueNode{parent: parent, Type: ps.Type(), ValueX: vx, ValueY: vy} 56 | switch s := ps.(type) { 57 | case StructField: 58 | assert(parent.Value == nil) 59 | parent.Records = append(parent.Records, reportRecord{Key: reflect.ValueOf(s.Name()), Value: child}) 60 | case SliceIndex: 61 | assert(parent.Value == nil) 62 | parent.Records = append(parent.Records, reportRecord{Value: child}) 63 | case MapIndex: 64 | assert(parent.Value == nil) 65 | parent.Records = append(parent.Records, reportRecord{Key: s.Key(), Value: child}) 66 | case Indirect: 67 | assert(parent.Value == nil && parent.Records == nil) 68 | parent.Value = child 69 | case TypeAssertion: 70 | assert(parent.Value == nil && parent.Records == nil) 71 | parent.Value = child 72 | case Transform: 73 | assert(parent.Value == nil && parent.Records == nil) 74 | parent.Value = child 75 | parent.TransformerName = s.Name() 76 | parent.NumTransformed++ 77 | default: 78 | assert(parent == nil) // Must be the root step 79 | } 80 | return child 81 | } 82 | 83 | func (r *valueNode) Report(rs Result) { 84 | assert(r.MaxDepth == 0) // May only be called on leaf nodes 85 | 86 | if rs.ByIgnore() { 87 | r.NumIgnored++ 88 | } else { 89 | if rs.Equal() { 90 | r.NumSame++ 91 | } else { 92 | r.NumDiff++ 93 | } 94 | } 95 | assert(r.NumSame+r.NumDiff+r.NumIgnored == 1) 96 | 97 | if rs.ByMethod() { 98 | r.NumCompared++ 99 | } 100 | if rs.ByFunc() { 101 | r.NumCompared++ 102 | } 103 | assert(r.NumCompared <= 1) 104 | } 105 | 106 | func (child *valueNode) PopStep() (parent *valueNode) { 107 | if child.parent == nil { 108 | return nil 109 | } 110 | parent = child.parent 111 | parent.NumSame += child.NumSame 112 | parent.NumDiff += child.NumDiff 113 | parent.NumIgnored += child.NumIgnored 114 | parent.NumCompared += child.NumCompared 115 | parent.NumTransformed += child.NumTransformed 116 | parent.NumChildren += child.NumChildren + 1 117 | if parent.MaxDepth < child.MaxDepth+1 { 118 | parent.MaxDepth = child.MaxDepth + 1 119 | } 120 | return parent 121 | } 122 | -------------------------------------------------------------------------------- /vendor/github.com/lithammer/dedent/.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - "1.6" 5 | - "1.7" 6 | - "1.8" 7 | - "1.9" 8 | - "1.10" 9 | - "1.11" 10 | 11 | sudo: false 12 | -------------------------------------------------------------------------------- /vendor/github.com/lithammer/dedent/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Peter Lithammer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /vendor/github.com/lithammer/dedent/README.md: -------------------------------------------------------------------------------- 1 | # Dedent 2 | 3 | [![Build Status](https://travis-ci.org/lithammer/dedent.svg?branch=master)](https://travis-ci.org/lithammer/dedent) 4 | [![Godoc](https://img.shields.io/badge/godoc-reference-blue.svg?style=flat)](https://godoc.org/github.com/lithammer/dedent) 5 | 6 | Removes common leading whitespace from multiline strings. Inspired by [`textwrap.dedent`](https://docs.python.org/3/library/textwrap.html#textwrap.dedent) in Python. 7 | 8 | ## Usage / example 9 | 10 | Imagine the following snippet that prints a multiline string. You want the indentation to both look nice in the code as well as in the actual output. 11 | 12 | ```go 13 | package main 14 | 15 | import ( 16 | "fmt" 17 | 18 | "github.com/lithammer/dedent" 19 | ) 20 | 21 | func main() { 22 | s := ` 23 | Lorem ipsum dolor sit amet, 24 | consectetur adipiscing elit. 25 | Curabitur justo tellus, facilisis nec efficitur dictum, 26 | fermentum vitae ligula. Sed eu convallis sapien.` 27 | fmt.Println(Dedent(s)) 28 | fmt.Println("-------------") 29 | fmt.Println(s) 30 | } 31 | ``` 32 | 33 | To illustrate the difference, here's the output: 34 | 35 | 36 | ```bash 37 | $ go run main.go 38 | Lorem ipsum dolor sit amet, 39 | consectetur adipiscing elit. 40 | Curabitur justo tellus, facilisis nec efficitur dictum, 41 | fermentum vitae ligula. Sed eu convallis sapien. 42 | ------------- 43 | 44 | Lorem ipsum dolor sit amet, 45 | consectetur adipiscing elit. 46 | Curabitur justo tellus, facilisis nec efficitur dictum, 47 | fermentum vitae ligula. Sed eu convallis sapien. 48 | ``` 49 | 50 | ## License 51 | 52 | MIT 53 | -------------------------------------------------------------------------------- /vendor/github.com/lithammer/dedent/dedent.go: -------------------------------------------------------------------------------- 1 | package dedent 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | ) 7 | 8 | var ( 9 | whitespaceOnly = regexp.MustCompile("(?m)^[ \t]+$") 10 | leadingWhitespace = regexp.MustCompile("(?m)(^[ \t]*)(?:[^ \t\n])") 11 | ) 12 | 13 | // Dedent removes any common leading whitespace from every line in text. 14 | // 15 | // This can be used to make multiline strings to line up with the left edge of 16 | // the display, while still presenting them in the source code in indented 17 | // form. 18 | func Dedent(text string) string { 19 | var margin string 20 | 21 | text = whitespaceOnly.ReplaceAllString(text, "") 22 | indents := leadingWhitespace.FindAllStringSubmatch(text, -1) 23 | 24 | // Look for the longest leading string of spaces and tabs common to all 25 | // lines. 26 | for i, indent := range indents { 27 | if i == 0 { 28 | margin = indent[1] 29 | } else if strings.HasPrefix(indent[1], margin) { 30 | // Current line more deeply indented than previous winner: 31 | // no change (previous winner is still on top). 32 | continue 33 | } else if strings.HasPrefix(margin, indent[1]) { 34 | // Current line consistent with and no deeper than previous winner: 35 | // it's the new winner. 36 | margin = indent[1] 37 | } else { 38 | // Current line and previous winner have no common whitespace: 39 | // there is no margin. 40 | margin = "" 41 | break 42 | } 43 | } 44 | 45 | if margin != "" { 46 | text = regexp.MustCompile("(?m)^"+margin).ReplaceAllString(text, "") 47 | } 48 | return text 49 | } 50 | -------------------------------------------------------------------------------- /vendor/modules.txt: -------------------------------------------------------------------------------- 1 | # github.com/google/go-cmp v0.5.9 2 | ## explicit; go 1.13 3 | github.com/google/go-cmp/cmp 4 | github.com/google/go-cmp/cmp/internal/diff 5 | github.com/google/go-cmp/cmp/internal/flags 6 | github.com/google/go-cmp/cmp/internal/function 7 | github.com/google/go-cmp/cmp/internal/value 8 | # github.com/lithammer/dedent v1.1.0 9 | ## explicit 10 | github.com/lithammer/dedent 11 | --------------------------------------------------------------------------------