pax_global_header00006660000000000000000000000064145766141470014531gustar00rootroot0000000000000052 comment=ff14fafe2236e51c2894ac07d4bdfc778e96d682 reference-0.6.0/000077500000000000000000000000001457661414700134725ustar00rootroot00000000000000reference-0.6.0/.gitattributes000066400000000000000000000000211457661414700163560ustar00rootroot00000000000000*.go text eol=lf reference-0.6.0/.github/000077500000000000000000000000001457661414700150325ustar00rootroot00000000000000reference-0.6.0/.github/workflows/000077500000000000000000000000001457661414700170675ustar00rootroot00000000000000reference-0.6.0/.github/workflows/codeql-analysis.yml000066400000000000000000000022411457661414700227010ustar00rootroot00000000000000name: CodeQL concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true on: schedule: - cron: '0 12 * * 6' push: branches: - 'main' - 'release/*' tags: - 'v*' pull_request: permissions: contents: read # to fetch code (actions/checkout) jobs: analyze: permissions: contents: read # to fetch code (actions/checkout) security-events: write # to upload SARIF results (github/codeql-action/analyze) name: Analyze runs-on: ubuntu-latest strategy: fail-fast: false matrix: language: - go steps: - name: Checkout uses: actions/checkout@v3 with: fetch-depth: 2 - name: Checkout HEAD on PR if: ${{ github.event_name == 'pull_request' }} run: | git checkout HEAD^2 - name: Initialize CodeQL uses: github/codeql-action/init@v2 with: languages: ${{ matrix.language }} - name: Autobuild uses: github/codeql-action/autobuild@v2 - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v2 reference-0.6.0/.github/workflows/fossa.yml000066400000000000000000000007751457661414700207360ustar00rootroot00000000000000name: FOSSA License Scanning concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true on: - pull_request - push permissions: contents: read # to fetch code (actions/checkout) jobs: scan-license: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v3 - name: Run FOSSA scan and upload build data uses: fossa-contrib/fossa-action@v2 with: fossa-api-key: cac3dc8d4f2ba86142f6c0f2199a160f reference-0.6.0/.github/workflows/test.yml000066400000000000000000000015511457661414700205730ustar00rootroot00000000000000name: test on: push: branches: [ "main" ] pull_request: branches: [ "main" ] permissions: contents: read env: TESTFLAGS: -v BUILDFLAGS: -v jobs: build: strategy: matrix: go-version: [1.20.x, 1.21.x] os: [ubuntu-latest, windows-latest, macos-latest] runs-on: ${{ matrix.os }} timeout-minutes: 10 steps: - uses: actions/checkout@v3 - name: Set up Go uses: actions/setup-go@v4 with: go-version: ${{ matrix.go-version }} - name: Build run: | make build - name: lint uses: golangci/golangci-lint-action@v3 with: version: v1.53 args: --print-resources-usage --timeout=10m --verbose - name: Test run: | make coverage - name: Codecov uses: codecov/codecov-action@v3 with: directory: ./ reference-0.6.0/.gitignore000066400000000000000000000000271457661414700154610ustar00rootroot00000000000000# Cover profiles *.out reference-0.6.0/.golangci.yml000066400000000000000000000004231457661414700160550ustar00rootroot00000000000000linters: enable: - bodyclose - dupword # Checks for duplicate words in the source code - gofmt - goimports - ineffassign - misspell - revive - staticcheck - unconvert - unused - vet disable: - errcheck run: deadline: 2m reference-0.6.0/CODE-OF-CONDUCT.md000066400000000000000000000003751457661414700161320ustar00rootroot00000000000000# Code of Conduct We follow the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/main/code-of-conduct.md). Please contact the [CNCF Code of Conduct Committee](mailto:conduct@cncf.io) in order to report violations of the Code of Conduct. reference-0.6.0/CONTRIBUTING.md000066400000000000000000000124511457661414700157260ustar00rootroot00000000000000# Contributing to the reference library ## Community help If you need help, please ask in the [#distribution](https://cloud-native.slack.com/archives/C01GVR8SY4R) channel on CNCF community slack. [Click here for an invite to the CNCF community slack](https://slack.cncf.io/) ## Reporting security issues The maintainers take security seriously. If you discover a security issue, please bring it to their attention right away! Please **DO NOT** file a public issue, instead send your report privately to [cncf-distribution-security@lists.cncf.io](mailto:cncf-distribution-security@lists.cncf.io). ## Reporting an issue properly By following these simple rules you will get better and faster feedback on your issue. - search the bugtracker for an already reported issue ### If you found an issue that describes your problem: - please read other user comments first, and confirm this is the same issue: a given error condition might be indicative of different problems - you may also find a workaround in the comments - please refrain from adding "same thing here" or "+1" comments - you don't need to comment on an issue to get notified of updates: just hit the "subscribe" button - comment if you have some new, technical and relevant information to add to the case - __DO NOT__ comment on closed issues or merged PRs. If you think you have a related problem, open up a new issue and reference the PR or issue. ### If you have not found an existing issue that describes your problem: 1. create a new issue, with a succinct title that describes your issue: - bad title: "It doesn't work with my docker" - good title: "Private registry push fail: 400 error with E_INVALID_DIGEST" 2. copy the output of (or similar for other container tools): - `docker version` - `docker info` - `docker exec registry --version` 3. copy the command line you used to launch your Registry 4. restart your docker daemon in debug mode (add `-D` to the daemon launch arguments) 5. reproduce your problem and get your docker daemon logs showing the error 6. if relevant, copy your registry logs that show the error 7. provide any relevant detail about your specific Registry configuration (e.g., storage backend used) 8. indicate if you are using an enterprise proxy, Nginx, or anything else between you and your Registry ## Contributing Code Contributions should be made via pull requests. Pull requests will be reviewed by one or more maintainers or reviewers and merged when acceptable. You should follow the basic GitHub workflow: 1. Use your own [fork](https://help.github.com/en/articles/about-forks) 2. Create your [change](https://github.com/containerd/project/blob/master/CONTRIBUTING.md#successful-changes) 3. Test your code 4. [Commit](https://github.com/containerd/project/blob/master/CONTRIBUTING.md#commit-messages) your work, always [sign your commits](https://github.com/containerd/project/blob/master/CONTRIBUTING.md#commit-messages) 5. Push your change to your fork and create a [Pull Request](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request-from-a-fork) Refer to [containerd's contribution guide](https://github.com/containerd/project/blob/master/CONTRIBUTING.md#successful-changes) for tips on creating a successful contribution. ## Sign your work The sign-off is a simple line at the end of the explanation for the patch. Your signature certifies that you wrote the patch or otherwise have the right to pass it on as an open-source patch. The rules are pretty simple: if you can certify the below (from [developercertificate.org](http://developercertificate.org/)): ``` Developer Certificate of Origin Version 1.1 Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 660 York Street, Suite 102, San Francisco, CA 94110 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Developer's Certificate of Origin 1.1 By making a contribution to this project, I certify that: (a) The contribution was created in whole or in part by me and I have the right to submit it under the open source license indicated in the file; or (b) The contribution is based upon previous work that, to the best of my knowledge, is covered under an appropriate open source license and I have the right under that license to submit that work with modifications, whether created in whole or in part by me, under the same open source license (unless I am permitted to submit under a different license), as indicated in the file; or (c) The contribution was provided directly to me by some other person who certified (a), (b) or (c) and I have not modified it. (d) I understand and agree that this project and the contribution are public and that a record of the contribution (including all personal information I submit with it, including my sign-off) is maintained indefinitely and may be redistributed consistent with this project or the open source license(s) involved. ``` Then you just add a line to every git commit message: Signed-off-by: Joe Smith Use your real name (sorry, no pseudonyms or anonymous contributions.) If you set your `user.name` and `user.email` git configs, you can sign your commit automatically with `git commit -s`. reference-0.6.0/GOVERNANCE.md000066400000000000000000000153761457661414700154570ustar00rootroot00000000000000# distribution/reference Project Governance Distribution [Code of Conduct](./CODE-OF-CONDUCT.md) can be found here. For specific guidance on practical contribution steps please see our [CONTRIBUTING.md](./CONTRIBUTING.md) guide. ## Maintainership There are different types of maintainers, with different responsibilities, but all maintainers have 3 things in common: 1) They share responsibility in the project's success. 2) They have made a long-term, recurring time investment to improve the project. 3) They spend that time doing whatever needs to be done, not necessarily what is the most interesting or fun. Maintainers are often under-appreciated, because their work is harder to appreciate. It's easy to appreciate a really cool and technically advanced feature. It's harder to appreciate the absence of bugs, the slow but steady improvement in stability, or the reliability of a release process. But those things distinguish a good project from a great one. ## Reviewers A reviewer is a core role within the project. They share in reviewing issues and pull requests and their LGTM counts towards the required LGTM count to merge a code change into the project. Reviewers are part of the organization but do not have write access. Becoming a reviewer is a core aspect in the journey to becoming a maintainer. ## Adding maintainers Maintainers are first and foremost contributors that have shown they are committed to the long term success of a project. Contributors wanting to become maintainers are expected to be deeply involved in contributing code, pull request review, and triage of issues in the project for more than three months. Just contributing does not make you a maintainer, it is about building trust with the current maintainers of the project and being a person that they can depend on and trust to make decisions in the best interest of the project. Periodically, the existing maintainers curate a list of contributors that have shown regular activity on the project over the prior months. From this list, maintainer candidates are selected and proposed in a pull request or a maintainers communication channel. After a candidate has been announced to the maintainers, the existing maintainers are given five business days to discuss the candidate, raise objections and cast their vote. Votes may take place on the communication channel or via pull request comment. Candidates must be approved by at least 66% of the current maintainers by adding their vote on the mailing list. The reviewer role has the same process but only requires 33% of current maintainers. Only maintainers of the repository that the candidate is proposed for are allowed to vote. If a candidate is approved, a maintainer will contact the candidate to invite the candidate to open a pull request that adds the contributor to the MAINTAINERS file. The voting process may take place inside a pull request if a maintainer has already discussed the candidacy with the candidate and a maintainer is willing to be a sponsor by opening the pull request. The candidate becomes a maintainer once the pull request is merged. ## Stepping down policy Life priorities, interests, and passions can change. If you're a maintainer but feel you must remove yourself from the list, inform other maintainers that you intend to step down, and if possible, help find someone to pick up your work. At the very least, ensure your work can be continued where you left off. After you've informed other maintainers, create a pull request to remove yourself from the MAINTAINERS file. ## Removal of inactive maintainers Similar to the procedure for adding new maintainers, existing maintainers can be removed from the list if they do not show significant activity on the project. Periodically, the maintainers review the list of maintainers and their activity over the last three months. If a maintainer has shown insufficient activity over this period, a neutral person will contact the maintainer to ask if they want to continue being a maintainer. If the maintainer decides to step down as a maintainer, they open a pull request to be removed from the MAINTAINERS file. If the maintainer wants to remain a maintainer, but is unable to perform the required duties they can be removed with a vote of at least 66% of the current maintainers. In this case, maintainers should first propose the change to maintainers via the maintainers communication channel, then open a pull request for voting. The voting period is five business days. The voting pull request should not come as a surpise to any maintainer and any discussion related to performance must not be discussed on the pull request. ## How are decisions made? Docker distribution is an open-source project with an open design philosophy. This means that the repository is the source of truth for EVERY aspect of the project, including its philosophy, design, road map, and APIs. *If it's part of the project, it's in the repo. If it's in the repo, it's part of the project.* As a result, all decisions can be expressed as changes to the repository. An implementation change is a change to the source code. An API change is a change to the API specification. A philosophy change is a change to the philosophy manifesto, and so on. All decisions affecting distribution, big and small, follow the same 3 steps: * Step 1: Open a pull request. Anyone can do this. * Step 2: Discuss the pull request. Anyone can do this. * Step 3: Merge or refuse the pull request. Who does this depends on the nature of the pull request and which areas of the project it affects. ## Helping contributors with the DCO The [DCO or `Sign your work`](./CONTRIBUTING.md#sign-your-work) requirement is not intended as a roadblock or speed bump. Some contributors are not as familiar with `git`, or have used a web based editor, and thus asking them to `git commit --amend -s` is not the best way forward. In this case, maintainers can update the commits based on clause (c) of the DCO. The most trivial way for a contributor to allow the maintainer to do this, is to add a DCO signature in a pull requests's comment, or a maintainer can simply note that the change is sufficiently trivial that it does not substantially change the existing contribution - i.e., a spelling change. When you add someone's DCO, please also add your own to keep a log. ## I'm a maintainer. Should I make pull requests too? Yes. Nobody should ever push to master directly. All changes should be made through a pull request. ## Conflict Resolution If you have a technical dispute that you feel has reached an impasse with a subset of the community, any contributor may open an issue, specifically calling for a resolution vote of the current core maintainers to resolve the dispute. The same voting quorums required (2/3) for adding and removing maintainers will apply to conflict resolution. reference-0.6.0/LICENSE000066400000000000000000000260751457661414700145110ustar00rootroot00000000000000Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright {yyyy} {name of copyright owner} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. reference-0.6.0/MAINTAINERS000066400000000000000000000021401457661414700151640ustar00rootroot00000000000000# Distribution project maintainers & reviewers # # See GOVERNANCE.md for maintainer versus reviewer roles # # MAINTAINERS (cncf-distribution-maintainers@lists.cncf.io) # GitHub ID, Name, Email address "chrispat","Chris Patterson","chrispat@github.com" "clarkbw","Bryan Clark","clarkbw@github.com" "corhere","Cory Snider","csnider@mirantis.com" "deleteriousEffect","Hayley Swimelar","hswimelar@gitlab.com" "heww","He Weiwei","hweiwei@vmware.com" "joaodrp","João Pereira","jpereira@gitlab.com" "justincormack","Justin Cormack","justin.cormack@docker.com" "squizzi","Kyle Squizzato","ksquizzato@mirantis.com" "milosgajdos","Milos Gajdos","milosthegajdos@gmail.com" "sargun","Sargun Dhillon","sargun@sargun.me" "wy65701436","Wang Yan","wangyan@vmware.com" "stevelasker","Steve Lasker","steve.lasker@microsoft.com" # # REVIEWERS # GitHub ID, Name, Email address "dmcgowan","Derek McGowan","derek@mcgstyle.net" "stevvooe","Stephen Day","stevvooe@gmail.com" "thajeztah","Sebastiaan van Stijn","github@gone.nl" "DavidSpek", "David van der Spek", "vanderspek.david@gmail.com" "Jamstah", "James Hewitt", "james.hewitt@gmail.com" reference-0.6.0/Makefile000066400000000000000000000012661457661414700151370ustar00rootroot00000000000000# Project packages. PACKAGES=$(shell go list ./...) # Flags passed to `go test` BUILDFLAGS ?= TESTFLAGS ?= .PHONY: all build test coverage .DEFAULT: all all: build build: ## no binaries to build, so just check compilation suceeds go build ${BUILDFLAGS} ./... test: ## run tests go test ${TESTFLAGS} ./... coverage: ## generate coverprofiles from the unit tests rm -f coverage.txt go test ${TESTFLAGS} -cover -coverprofile=cover.out ./... .PHONY: help help: @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_\/%-]+:.*?##/ { printf " \033[36m%-27s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) reference-0.6.0/README.md000066400000000000000000000032711457661414700147540ustar00rootroot00000000000000# Distribution reference Go library to handle references to container images. [![Build Status](https://github.com/distribution/reference/actions/workflows/test.yml/badge.svg?branch=main&event=push)](https://github.com/distribution/reference/actions?query=workflow%3ACI) [![GoDoc](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/github.com/distribution/reference) [![License: Apache-2.0](https://img.shields.io/badge/License-Apache--2.0-blue.svg)](LICENSE) [![codecov](https://codecov.io/gh/distribution/reference/branch/main/graph/badge.svg)](https://codecov.io/gh/distribution/reference) [![FOSSA Status](https://app.fossa.com/api/projects/custom%2B162%2Fgithub.com%2Fdistribution%2Freference.svg?type=shield)](https://app.fossa.com/projects/custom%2B162%2Fgithub.com%2Fdistribution%2Freference?ref=badge_shield) This repository contains a library for handling references to container images held in container registries. Please see [godoc](https://pkg.go.dev/github.com/distribution/reference) for details. ## Contribution Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to contribute issues, fixes, and patches to this project. ## Communication For async communication and long running discussions please use issues and pull requests on the github repo. This will be the best place to discuss design and implementation. For sync communication we have a #distribution channel in the [CNCF Slack](https://slack.cncf.io/) that everyone is welcome to join and chat about development. ## Licenses The distribution codebase is released under the [Apache 2.0 license](LICENSE). reference-0.6.0/SECURITY.md000066400000000000000000000004371457661414700152670ustar00rootroot00000000000000# Security Policy ## Reporting a Vulnerability The maintainers take security seriously. If you discover a security issue, please bring it to their attention right away! Please DO NOT file a public issue, instead send your report privately to cncf-distribution-security@lists.cncf.io. reference-0.6.0/distribution-logo.svg000066400000000000000000000211021457661414700176640ustar00rootroot00000000000000reference-0.6.0/fuzz_test.go000066400000000000000000000004241457661414700160560ustar00rootroot00000000000000package reference import ( "testing" ) // fuzzParseNormalizedNamed implements a fuzzer // that targets ParseNormalizedNamed // nolint:deadcode func FuzzParseNormalizedNamed(f *testing.F) { f.Fuzz(func(t *testing.T, data string) { _, _ = ParseNormalizedNamed(data) }) } reference-0.6.0/go.mod000066400000000000000000000001461457661414700146010ustar00rootroot00000000000000module github.com/distribution/reference go 1.20 require github.com/opencontainers/go-digest v1.0.0 reference-0.6.0/go.sum000066400000000000000000000002751457661414700146310ustar00rootroot00000000000000github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= reference-0.6.0/helpers.go000066400000000000000000000021541457661414700154650ustar00rootroot00000000000000package reference import "path" // IsNameOnly returns true if reference only contains a repo name. func IsNameOnly(ref Named) bool { if _, ok := ref.(NamedTagged); ok { return false } if _, ok := ref.(Canonical); ok { return false } return true } // FamiliarName returns the familiar name string // for the given named, familiarizing if needed. func FamiliarName(ref Named) string { if nn, ok := ref.(normalizedNamed); ok { return nn.Familiar().Name() } return ref.Name() } // FamiliarString returns the familiar string representation // for the given reference, familiarizing if needed. func FamiliarString(ref Reference) string { if nn, ok := ref.(normalizedNamed); ok { return nn.Familiar().String() } return ref.String() } // FamiliarMatch reports whether ref matches the specified pattern. // See [path.Match] for supported patterns. func FamiliarMatch(pattern string, ref Reference) (bool, error) { matched, err := path.Match(pattern, FamiliarString(ref)) if namedRef, isNamed := ref.(Named); isNamed && !matched { matched, _ = path.Match(pattern, FamiliarName(namedRef)) } return matched, err } reference-0.6.0/normalize.go000066400000000000000000000215651457661414700160320ustar00rootroot00000000000000package reference import ( "fmt" "strings" "github.com/opencontainers/go-digest" ) const ( // legacyDefaultDomain is the legacy domain for Docker Hub (which was // originally named "the Docker Index"). This domain is still used for // authentication and image search, which were part of the "v1" Docker // registry specification. // // This domain will continue to be supported, but there are plans to consolidate // legacy domains to new "canonical" domains. Once those domains are decided // on, we must update the normalization functions, but preserve compatibility // with existing installs, clients, and user configuration. legacyDefaultDomain = "index.docker.io" // defaultDomain is the default domain used for images on Docker Hub. // It is used to normalize "familiar" names to canonical names, for example, // to convert "ubuntu" to "docker.io/library/ubuntu:latest". // // Note that actual domain of Docker Hub's registry is registry-1.docker.io. // This domain will continue to be supported, but there are plans to consolidate // legacy domains to new "canonical" domains. Once those domains are decided // on, we must update the normalization functions, but preserve compatibility // with existing installs, clients, and user configuration. defaultDomain = "docker.io" // officialRepoPrefix is the namespace used for official images on Docker Hub. // It is used to normalize "familiar" names to canonical names, for example, // to convert "ubuntu" to "docker.io/library/ubuntu:latest". officialRepoPrefix = "library/" // defaultTag is the default tag if no tag is provided. defaultTag = "latest" ) // normalizedNamed represents a name which has been // normalized and has a familiar form. A familiar name // is what is used in Docker UI. An example normalized // name is "docker.io/library/ubuntu" and corresponding // familiar name of "ubuntu". type normalizedNamed interface { Named Familiar() Named } // ParseNormalizedNamed parses a string into a named reference // transforming a familiar name from Docker UI to a fully // qualified reference. If the value may be an identifier // use ParseAnyReference. func ParseNormalizedNamed(s string) (Named, error) { if ok := anchoredIdentifierRegexp.MatchString(s); ok { return nil, fmt.Errorf("invalid repository name (%s), cannot specify 64-byte hexadecimal strings", s) } domain, remainder := splitDockerDomain(s) var remote string if tagSep := strings.IndexRune(remainder, ':'); tagSep > -1 { remote = remainder[:tagSep] } else { remote = remainder } if strings.ToLower(remote) != remote { return nil, fmt.Errorf("invalid reference format: repository name (%s) must be lowercase", remote) } ref, err := Parse(domain + "/" + remainder) if err != nil { return nil, err } named, isNamed := ref.(Named) if !isNamed { return nil, fmt.Errorf("reference %s has no name", ref.String()) } return named, nil } // namedTaggedDigested is a reference that has both a tag and a digest. type namedTaggedDigested interface { NamedTagged Digested } // ParseDockerRef normalizes the image reference following the docker convention, // which allows for references to contain both a tag and a digest. It returns a // reference that is either tagged or digested. For references containing both // a tag and a digest, it returns a digested reference. For example, the following // reference: // // docker.io/library/busybox:latest@sha256:7cc4b5aefd1d0cadf8d97d4350462ba51c694ebca145b08d7d41b41acc8db5aa // // Is returned as a digested reference (with the ":latest" tag removed): // // docker.io/library/busybox@sha256:7cc4b5aefd1d0cadf8d97d4350462ba51c694ebca145b08d7d41b41acc8db5aa // // References that are already "tagged" or "digested" are returned unmodified: // // // Already a digested reference // docker.io/library/busybox@sha256:7cc4b5aefd1d0cadf8d97d4350462ba51c694ebca145b08d7d41b41acc8db5aa // // // Already a named reference // docker.io/library/busybox:latest func ParseDockerRef(ref string) (Named, error) { named, err := ParseNormalizedNamed(ref) if err != nil { return nil, err } if canonical, ok := named.(namedTaggedDigested); ok { // The reference is both tagged and digested; only return digested. newNamed, err := WithName(canonical.Name()) if err != nil { return nil, err } return WithDigest(newNamed, canonical.Digest()) } return TagNameOnly(named), nil } // splitDockerDomain splits a repository name to domain and remote-name. // If no valid domain is found, the default domain is used. Repository name // needs to be already validated before. func splitDockerDomain(name string) (domain, remoteName string) { maybeDomain, maybeRemoteName, ok := strings.Cut(name, "/") if !ok { // Fast-path for single element ("familiar" names), such as "ubuntu" // or "ubuntu:latest". Familiar names must be handled separately, to // prevent them from being handled as "hostname:port". // // Canonicalize them as "docker.io/library/name[:tag]" // FIXME(thaJeztah): account for bare "localhost" or "example.com" names, which SHOULD be considered a domain. return defaultDomain, officialRepoPrefix + name } switch { case maybeDomain == localhost: // localhost is a reserved namespace and always considered a domain. domain, remoteName = maybeDomain, maybeRemoteName case maybeDomain == legacyDefaultDomain: // canonicalize the Docker Hub and legacy "Docker Index" domains. domain, remoteName = defaultDomain, maybeRemoteName case strings.ContainsAny(maybeDomain, ".:"): // Likely a domain or IP-address: // // - contains a "." (e.g., "example.com" or "127.0.0.1") // - contains a ":" (e.g., "example:5000", "::1", or "[::1]:5000") domain, remoteName = maybeDomain, maybeRemoteName case strings.ToLower(maybeDomain) != maybeDomain: // Uppercase namespaces are not allowed, so if the first element // is not lowercase, we assume it to be a domain-name. domain, remoteName = maybeDomain, maybeRemoteName default: // None of the above: it's not a domain, so use the default, and // use the name input the remote-name. domain, remoteName = defaultDomain, name } if domain == defaultDomain && !strings.ContainsRune(remoteName, '/') { // Canonicalize "familiar" names, but only on Docker Hub, not // on other domains: // // "docker.io/ubuntu[:tag]" => "docker.io/library/ubuntu[:tag]" remoteName = officialRepoPrefix + remoteName } return domain, remoteName } // familiarizeName returns a shortened version of the name familiar // to the Docker UI. Familiar names have the default domain // "docker.io" and "library/" repository prefix removed. // For example, "docker.io/library/redis" will have the familiar // name "redis" and "docker.io/dmcgowan/myapp" will be "dmcgowan/myapp". // Returns a familiarized named only reference. func familiarizeName(named namedRepository) repository { repo := repository{ domain: named.Domain(), path: named.Path(), } if repo.domain == defaultDomain { repo.domain = "" // Handle official repositories which have the pattern "library/" if strings.HasPrefix(repo.path, officialRepoPrefix) { // TODO(thaJeztah): this check may be too strict, as it assumes the // "library/" namespace does not have nested namespaces. While this // is true (currently), technically it would be possible for Docker // Hub to use those (e.g. "library/distros/ubuntu:latest"). // See https://github.com/distribution/distribution/pull/3769#issuecomment-1302031785. if remainder := strings.TrimPrefix(repo.path, officialRepoPrefix); !strings.ContainsRune(remainder, '/') { repo.path = remainder } } } return repo } func (r reference) Familiar() Named { return reference{ namedRepository: familiarizeName(r.namedRepository), tag: r.tag, digest: r.digest, } } func (r repository) Familiar() Named { return familiarizeName(r) } func (t taggedReference) Familiar() Named { return taggedReference{ namedRepository: familiarizeName(t.namedRepository), tag: t.tag, } } func (c canonicalReference) Familiar() Named { return canonicalReference{ namedRepository: familiarizeName(c.namedRepository), digest: c.digest, } } // TagNameOnly adds the default tag "latest" to a reference if it only has // a repo name. func TagNameOnly(ref Named) Named { if IsNameOnly(ref) { namedTagged, err := WithTag(ref, defaultTag) if err != nil { // Default tag must be valid, to create a NamedTagged // type with non-validated input the WithTag function // should be used instead panic(err) } return namedTagged } return ref } // ParseAnyReference parses a reference string as a possible identifier, // full digest, or familiar name. func ParseAnyReference(ref string) (Reference, error) { if ok := anchoredIdentifierRegexp.MatchString(ref); ok { return digestReference("sha256:" + ref), nil } if dgst, err := digest.Parse(ref); err == nil { return digestReference(dgst), nil } return ParseNormalizedNamed(ref) } reference-0.6.0/normalize_test.go000066400000000000000000000463271457661414700170740ustar00rootroot00000000000000package reference import ( "strconv" "testing" "github.com/opencontainers/go-digest" ) func TestValidateReferenceName(t *testing.T) { t.Parallel() validRepoNames := []string{ "docker/docker", "library/debian", "debian", "localhost/library/debian", "localhost/debian", "LOCALDOMAIN/library/debian", "LOCALDOMAIN/debian", "docker.io/docker/docker", "docker.io/library/debian", "docker.io/debian", "index.docker.io/docker/docker", "index.docker.io/library/debian", "index.docker.io/debian", "127.0.0.1:5000/docker/docker", "127.0.0.1:5000/library/debian", "127.0.0.1:5000/debian", "192.168.0.1", "192.168.0.1:80", "192.168.0.1:8/debian", "192.168.0.2:25000/debian", "thisisthesongthatneverendsitgoesonandonandonthisisthesongthatnev", "[fc00::1]:5000/docker", "[fc00::1]:5000/docker/docker", "[fc00:1:2:3:4:5:6:7]:5000/library/debian", // This test case was moved from invalid to valid since it is valid input // when specified with a hostname, it removes the ambiguity from about // whether the value is an identifier or repository name "docker.io/1a3f5e7d9c1b3a5f7e9d1c3b5a7f9e1d3c5b7a9f1e3d5d7c9b1a3f5e7d9c1b3a", "Docker/docker", "DOCKER/docker", } invalidRepoNames := []string{ "https://github.com/docker/docker", "docker/Docker", "-docker", "-docker/docker", "-docker.io/docker/docker", "docker///docker", "docker.io/docker/Docker", "docker.io/docker///docker", "[fc00::1]", "[fc00::1]:5000", "fc00::1:5000/debian", "[fe80::1%eth0]:5000/debian", "[2001:db8:3:4::192.0.2.33]:5000/debian", "1a3f5e7d9c1b3a5f7e9d1c3b5a7f9e1d3c5b7a9f1e3d5d7c9b1a3f5e7d9c1b3a", } for _, name := range invalidRepoNames { _, err := ParseNormalizedNamed(name) if err == nil { t.Fatalf("Expected invalid repo name for %q", name) } } for _, name := range validRepoNames { _, err := ParseNormalizedNamed(name) if err != nil { t.Fatalf("Error parsing repo name %s, got: %q", name, err) } } } func TestValidateRemoteName(t *testing.T) { t.Parallel() validRepositoryNames := []string{ // Sanity check. "docker/docker", // Allow 64-character non-hexadecimal names (hexadecimal names are forbidden). "thisisthesongthatneverendsitgoesonandonandonthisisthesongthatnev", // Allow embedded hyphens. "docker-rules/docker", // Allow multiple hyphens as well. "docker---rules/docker", // Username doc and image name docker being tested. "doc/docker", // single character names are now allowed. "d/docker", "jess/t", // Consecutive underscores. "dock__er/docker", } for _, repositoryName := range validRepositoryNames { _, err := ParseNormalizedNamed(repositoryName) if err != nil { t.Errorf("Repository name should be valid: %v. Error: %v", repositoryName, err) } } invalidRepositoryNames := []string{ // Disallow capital letters. "docker/Docker", // Only allow one slash. "docker///docker", // Disallow 64-character hexadecimal. "1a3f5e7d9c1b3a5f7e9d1c3b5a7f9e1d3c5b7a9f1e3d5d7c9b1a3f5e7d9c1b3a", // Disallow leading and trailing hyphens in namespace. "-docker/docker", "docker-/docker", "-docker-/docker", // Don't allow underscores everywhere (as opposed to hyphens). "____/____", "_docker/_docker", // Disallow consecutive periods. "dock..er/docker", "dock_.er/docker", "dock-.er/docker", // No repository. "docker/", // namespace too long "this_is_not_a_valid_namespace_because_its_lenth_is_greater_than_255_this_is_not_a_valid_namespace_because_its_lenth_is_greater_than_255_this_is_not_a_valid_namespace_because_its_lenth_is_greater_than_255_this_is_not_a_valid_namespace_because_its_lenth_is_greater_than_255/docker", } for _, repositoryName := range invalidRepositoryNames { if _, err := ParseNormalizedNamed(repositoryName); err == nil { t.Errorf("Repository name should be invalid: %v", repositoryName) } } } func TestParseRepositoryInfo(t *testing.T) { t.Parallel() type tcase struct { RemoteName, FamiliarName, FullName, AmbiguousName, Domain string } tests := []tcase{ { RemoteName: "fooo", FamiliarName: "localhost/fooo", FullName: "localhost/fooo", AmbiguousName: "localhost/fooo", Domain: "localhost", }, { RemoteName: "fooo/bar", FamiliarName: "localhost/fooo/bar", FullName: "localhost/fooo/bar", AmbiguousName: "localhost/fooo/bar", Domain: "localhost", }, { RemoteName: "fooo", FamiliarName: "LOCALDOMAIN/fooo", FullName: "LOCALDOMAIN/fooo", AmbiguousName: "LOCALDOMAIN/fooo", Domain: "LOCALDOMAIN", }, { RemoteName: "fooo/bar", FamiliarName: "LOCALDOMAIN/fooo/bar", FullName: "LOCALDOMAIN/fooo/bar", AmbiguousName: "LOCALDOMAIN/fooo/bar", Domain: "LOCALDOMAIN", }, { RemoteName: "fooo/bar", FamiliarName: "fooo/bar", FullName: "docker.io/fooo/bar", AmbiguousName: "index.docker.io/fooo/bar", Domain: "docker.io", }, { RemoteName: "library/ubuntu", FamiliarName: "ubuntu", FullName: "docker.io/library/ubuntu", AmbiguousName: "library/ubuntu", Domain: "docker.io", }, { RemoteName: "nonlibrary/ubuntu", FamiliarName: "nonlibrary/ubuntu", FullName: "docker.io/nonlibrary/ubuntu", AmbiguousName: "", Domain: "docker.io", }, { RemoteName: "other/library", FamiliarName: "other/library", FullName: "docker.io/other/library", AmbiguousName: "", Domain: "docker.io", }, { RemoteName: "private/moonbase", FamiliarName: "127.0.0.1:8000/private/moonbase", FullName: "127.0.0.1:8000/private/moonbase", AmbiguousName: "", Domain: "127.0.0.1:8000", }, { RemoteName: "privatebase", FamiliarName: "127.0.0.1:8000/privatebase", FullName: "127.0.0.1:8000/privatebase", AmbiguousName: "", Domain: "127.0.0.1:8000", }, { RemoteName: "private/moonbase", FamiliarName: "example.com/private/moonbase", FullName: "example.com/private/moonbase", AmbiguousName: "", Domain: "example.com", }, { RemoteName: "privatebase", FamiliarName: "example.com/privatebase", FullName: "example.com/privatebase", AmbiguousName: "", Domain: "example.com", }, { RemoteName: "private/moonbase", FamiliarName: "example.com:8000/private/moonbase", FullName: "example.com:8000/private/moonbase", AmbiguousName: "", Domain: "example.com:8000", }, { RemoteName: "privatebasee", FamiliarName: "example.com:8000/privatebasee", FullName: "example.com:8000/privatebasee", AmbiguousName: "", Domain: "example.com:8000", }, { RemoteName: "library/ubuntu-12.04-base", FamiliarName: "ubuntu-12.04-base", FullName: "docker.io/library/ubuntu-12.04-base", AmbiguousName: "index.docker.io/library/ubuntu-12.04-base", Domain: "docker.io", }, { RemoteName: "library/foo", FamiliarName: "foo", FullName: "docker.io/library/foo", AmbiguousName: "docker.io/foo", Domain: "docker.io", }, { RemoteName: "library/foo/bar", FamiliarName: "library/foo/bar", FullName: "docker.io/library/foo/bar", AmbiguousName: "", Domain: "docker.io", }, { RemoteName: "store/foo/bar", FamiliarName: "store/foo/bar", FullName: "docker.io/store/foo/bar", AmbiguousName: "", Domain: "docker.io", }, { RemoteName: "bar", FamiliarName: "Foo/bar", FullName: "Foo/bar", AmbiguousName: "", Domain: "Foo", }, { RemoteName: "bar", FamiliarName: "FOO/bar", FullName: "FOO/bar", AmbiguousName: "", Domain: "FOO", }, } for i, tc := range tests { tc := tc refStrings := []string{tc.FamiliarName, tc.FullName} if tc.AmbiguousName != "" { refStrings = append(refStrings, tc.AmbiguousName) } for _, r := range refStrings { r := r t.Run(strconv.Itoa(i)+"/"+r, func(t *testing.T) { t.Parallel() named, err := ParseNormalizedNamed(r) if err != nil { t.Fatalf("ref=%s: %v", r, err) } t.Run("FamiliarName", func(t *testing.T) { if expected, actual := tc.FamiliarName, FamiliarName(named); expected != actual { t.Errorf("Invalid familiar name for %q. Expected %q, got %q", named, expected, actual) } }) t.Run("FullName", func(t *testing.T) { if expected, actual := tc.FullName, named.String(); expected != actual { t.Errorf("Invalid canonical reference for %q. Expected %q, got %q", named, expected, actual) } }) t.Run("Domain", func(t *testing.T) { if expected, actual := tc.Domain, Domain(named); expected != actual { t.Errorf("Invalid domain for %q. Expected %q, got %q", named, expected, actual) } }) t.Run("RemoteName", func(t *testing.T) { if expected, actual := tc.RemoteName, Path(named); expected != actual { t.Errorf("Invalid remoteName for %q. Expected %q, got %q", named, expected, actual) } }) }) } } } func TestParseReferenceWithTagAndDigest(t *testing.T) { t.Parallel() shortRef := "busybox:latest@sha256:86e0e091d0da6bde2456dbb48306f3956bbeb2eae1b5b9a43045843f69fe4aaa" ref, err := ParseNormalizedNamed(shortRef) if err != nil { t.Fatal(err) } if expected, actual := "docker.io/library/"+shortRef, ref.String(); actual != expected { t.Fatalf("Invalid parsed reference for %q: expected %q, got %q", ref, expected, actual) } if _, isTagged := ref.(NamedTagged); !isTagged { t.Fatalf("Reference from %q should support tag", ref) } if _, isCanonical := ref.(Canonical); !isCanonical { t.Fatalf("Reference from %q should support digest", ref) } if expected, actual := shortRef, FamiliarString(ref); actual != expected { t.Fatalf("Invalid parsed reference for %q: expected %q, got %q", ref, expected, actual) } } func TestInvalidReferenceComponents(t *testing.T) { t.Parallel() if _, err := ParseNormalizedNamed("-foo"); err == nil { t.Fatal("Expected WithName to detect invalid name") } ref, err := ParseNormalizedNamed("busybox") if err != nil { t.Fatal(err) } if _, err := WithTag(ref, "-foo"); err == nil { t.Fatal("Expected WithName to detect invalid tag") } if _, err := WithDigest(ref, digest.Digest("foo")); err == nil { t.Fatal("Expected WithDigest to detect invalid digest") } } func equalReference(r1, r2 Reference) bool { switch v1 := r1.(type) { case digestReference: if v2, ok := r2.(digestReference); ok { return v1 == v2 } case repository: if v2, ok := r2.(repository); ok { return v1 == v2 } case taggedReference: if v2, ok := r2.(taggedReference); ok { return v1 == v2 } case canonicalReference: if v2, ok := r2.(canonicalReference); ok { return v1 == v2 } case reference: if v2, ok := r2.(reference); ok { return v1 == v2 } } return false } func TestParseAnyReference(t *testing.T) { t.Parallel() tests := []struct { Reference string Equivalent string Expected Reference }{ { Reference: "redis", Equivalent: "docker.io/library/redis", }, { Reference: "redis:latest", Equivalent: "docker.io/library/redis:latest", }, { Reference: "docker.io/library/redis:latest", Equivalent: "docker.io/library/redis:latest", }, { Reference: "redis@sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c", Equivalent: "docker.io/library/redis@sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c", }, { Reference: "docker.io/library/redis@sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c", Equivalent: "docker.io/library/redis@sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c", }, { Reference: "dmcgowan/myapp", Equivalent: "docker.io/dmcgowan/myapp", }, { Reference: "dmcgowan/myapp:latest", Equivalent: "docker.io/dmcgowan/myapp:latest", }, { Reference: "docker.io/mcgowan/myapp:latest", Equivalent: "docker.io/mcgowan/myapp:latest", }, { Reference: "dmcgowan/myapp@sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c", Equivalent: "docker.io/dmcgowan/myapp@sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c", }, { Reference: "docker.io/dmcgowan/myapp@sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c", Equivalent: "docker.io/dmcgowan/myapp@sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c", }, { Reference: "dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c", Expected: digestReference("sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c"), Equivalent: "sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c", }, { Reference: "sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c", Expected: digestReference("sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c"), Equivalent: "sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c", }, { Reference: "dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9", Equivalent: "docker.io/library/dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9", }, { Reference: "dbcc1", Equivalent: "docker.io/library/dbcc1", }, } for _, tc := range tests { tc := tc t.Run(tc.Reference, func(t *testing.T) { t.Parallel() var ref Reference var err error ref, err = ParseAnyReference(tc.Reference) if err != nil { t.Fatalf("Error parsing reference %s: %v", tc.Reference, err) } if ref.String() != tc.Equivalent { t.Fatalf("Unexpected string: %s, expected %s", ref.String(), tc.Equivalent) } expected := tc.Expected if expected == nil { expected, err = Parse(tc.Equivalent) if err != nil { t.Fatalf("Error parsing reference %s: %v", tc.Equivalent, err) } } if !equalReference(ref, expected) { t.Errorf("Unexpected reference %#v, expected %#v", ref, expected) } }) } } func TestNormalizedSplitHostname(t *testing.T) { t.Parallel() tests := []struct { input string domain string path string }{ { input: "test.com/foo", domain: "test.com", path: "foo", }, { input: "test_com/foo", domain: "docker.io", path: "test_com/foo", }, { input: "docker/migrator", domain: "docker.io", path: "docker/migrator", }, { input: "test.com:8080/foo", domain: "test.com:8080", path: "foo", }, { input: "test-com:8080/foo", domain: "test-com:8080", path: "foo", }, { input: "foo", domain: "docker.io", path: "library/foo", }, { input: "xn--n3h.com/foo", domain: "xn--n3h.com", path: "foo", }, { input: "xn--n3h.com:18080/foo", domain: "xn--n3h.com:18080", path: "foo", }, { input: "docker.io/foo", domain: "docker.io", path: "library/foo", }, { input: "docker.io/library/foo", domain: "docker.io", path: "library/foo", }, { input: "docker.io/library/foo/bar", domain: "docker.io", path: "library/foo/bar", }, } for _, tc := range tests { tc := tc t.Run(tc.input, func(t *testing.T) { t.Parallel() named, err := ParseNormalizedNamed(tc.input) if err != nil { t.Errorf("error parsing name: %s", err) } if domain := Domain(named); domain != tc.domain { t.Errorf("unexpected domain: got %q, expected %q", domain, tc.domain) } if path := Path(named); path != tc.path { t.Errorf("unexpected name: got %q, expected %q", path, tc.path) } }) } } func TestMatchError(t *testing.T) { t.Parallel() named, err := ParseAnyReference("foo") if err != nil { t.Fatal(err) } _, err = FamiliarMatch("[-x]", named) if err == nil { t.Fatalf("expected an error, got nothing") } } func TestMatch(t *testing.T) { t.Parallel() tests := []struct { reference string pattern string expected bool }{ { reference: "foo", pattern: "foo/**/ba[rz]", expected: false, }, { reference: "foo/any/bat", pattern: "foo/**/ba[rz]", expected: false, }, { reference: "foo/a/bar", pattern: "foo/**/ba[rz]", expected: true, }, { reference: "foo/b/baz", pattern: "foo/**/ba[rz]", expected: true, }, { reference: "foo/c/baz:tag", pattern: "foo/**/ba[rz]", expected: true, }, { reference: "foo/c/baz:tag", pattern: "foo/*/baz:tag", expected: true, }, { reference: "foo/c/baz:tag", pattern: "foo/c/baz:tag", expected: true, }, { reference: "example.com/foo/c/baz:tag", pattern: "*/foo/c/baz", expected: true, }, { reference: "example.com/foo/c/baz:tag", pattern: "example.com/foo/c/baz", expected: true, }, } for _, tc := range tests { tc := tc t.Run(tc.reference, func(t *testing.T) { t.Parallel() named, err := ParseAnyReference(tc.reference) if err != nil { t.Fatal(err) } actual, err := FamiliarMatch(tc.pattern, named) if err != nil { t.Fatal(err) } if actual != tc.expected { t.Fatalf("expected %s match %s to be %v, was %v", tc.reference, tc.pattern, tc.expected, actual) } }) } } func TestParseDockerRef(t *testing.T) { t.Parallel() tests := []struct { name string input string expected string }{ { name: "nothing", input: "busybox", expected: "docker.io/library/busybox:latest", }, { name: "tag only", input: "busybox:latest", expected: "docker.io/library/busybox:latest", }, { name: "digest only", input: "busybox@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582", expected: "docker.io/library/busybox@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582", }, { name: "path only", input: "library/busybox", expected: "docker.io/library/busybox:latest", }, { name: "hostname only", input: "docker.io/busybox", expected: "docker.io/library/busybox:latest", }, { name: "no tag", input: "docker.io/library/busybox", expected: "docker.io/library/busybox:latest", }, { name: "no path", input: "docker.io/busybox:latest", expected: "docker.io/library/busybox:latest", }, { name: "no hostname", input: "library/busybox:latest", expected: "docker.io/library/busybox:latest", }, { name: "full reference with tag", input: "docker.io/library/busybox:latest", expected: "docker.io/library/busybox:latest", }, { name: "gcr reference without tag", input: "gcr.io/library/busybox", expected: "gcr.io/library/busybox:latest", }, { name: "both tag and digest", input: "gcr.io/library/busybox:latest@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582", expected: "gcr.io/library/busybox@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582", }, } for _, tc := range tests { tc := tc t.Run(tc.name, func(t *testing.T) { t.Parallel() normalized, err := ParseDockerRef(tc.input) if err != nil { t.Fatal(err) } output := normalized.String() if output != tc.expected { t.Fatalf("expected %q to be parsed as %v, got %v", tc.input, tc.expected, output) } _, err = Parse(output) if err != nil { t.Fatalf("%q should be a valid reference, but got an error: %v", output, err) } }) } } reference-0.6.0/reference.go000066400000000000000000000261371457661414700157700ustar00rootroot00000000000000// Package reference provides a general type to represent any way of referencing images within the registry. // Its main purpose is to abstract tags and digests (content-addressable hash). // // Grammar // // reference := name [ ":" tag ] [ "@" digest ] // name := [domain '/'] remote-name // domain := host [':' port-number] // host := domain-name | IPv4address | \[ IPv6address \] ; rfc3986 appendix-A // domain-name := domain-component ['.' domain-component]* // domain-component := /([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])/ // port-number := /[0-9]+/ // path-component := alpha-numeric [separator alpha-numeric]* // path (or "remote-name") := path-component ['/' path-component]* // alpha-numeric := /[a-z0-9]+/ // separator := /[_.]|__|[-]*/ // // tag := /[\w][\w.-]{0,127}/ // // digest := digest-algorithm ":" digest-hex // digest-algorithm := digest-algorithm-component [ digest-algorithm-separator digest-algorithm-component ]* // digest-algorithm-separator := /[+.-_]/ // digest-algorithm-component := /[A-Za-z][A-Za-z0-9]*/ // digest-hex := /[0-9a-fA-F]{32,}/ ; At least 128 bit digest value // // identifier := /[a-f0-9]{64}/ package reference import ( "errors" "fmt" "strings" "github.com/opencontainers/go-digest" ) const ( // RepositoryNameTotalLengthMax is the maximum total number of characters in a repository name. RepositoryNameTotalLengthMax = 255 // NameTotalLengthMax is the maximum total number of characters in a repository name. // // Deprecated: use [RepositoryNameTotalLengthMax] instead. NameTotalLengthMax = RepositoryNameTotalLengthMax ) var ( // ErrReferenceInvalidFormat represents an error while trying to parse a string as a reference. ErrReferenceInvalidFormat = errors.New("invalid reference format") // ErrTagInvalidFormat represents an error while trying to parse a string as a tag. ErrTagInvalidFormat = errors.New("invalid tag format") // ErrDigestInvalidFormat represents an error while trying to parse a string as a tag. ErrDigestInvalidFormat = errors.New("invalid digest format") // ErrNameContainsUppercase is returned for invalid repository names that contain uppercase characters. ErrNameContainsUppercase = errors.New("repository name must be lowercase") // ErrNameEmpty is returned for empty, invalid repository names. ErrNameEmpty = errors.New("repository name must have at least one component") // ErrNameTooLong is returned when a repository name is longer than RepositoryNameTotalLengthMax. ErrNameTooLong = fmt.Errorf("repository name must not be more than %v characters", RepositoryNameTotalLengthMax) // ErrNameNotCanonical is returned when a name is not canonical. ErrNameNotCanonical = errors.New("repository name must be canonical") ) // Reference is an opaque object reference identifier that may include // modifiers such as a hostname, name, tag, and digest. type Reference interface { // String returns the full reference String() string } // Field provides a wrapper type for resolving correct reference types when // working with encoding. type Field struct { reference Reference } // AsField wraps a reference in a Field for encoding. func AsField(reference Reference) Field { return Field{reference} } // Reference unwraps the reference type from the field to // return the Reference object. This object should be // of the appropriate type to further check for different // reference types. func (f Field) Reference() Reference { return f.reference } // MarshalText serializes the field to byte text which // is the string of the reference. func (f Field) MarshalText() (p []byte, err error) { return []byte(f.reference.String()), nil } // UnmarshalText parses text bytes by invoking the // reference parser to ensure the appropriately // typed reference object is wrapped by field. func (f *Field) UnmarshalText(p []byte) error { r, err := Parse(string(p)) if err != nil { return err } f.reference = r return nil } // Named is an object with a full name type Named interface { Reference Name() string } // Tagged is an object which has a tag type Tagged interface { Reference Tag() string } // NamedTagged is an object including a name and tag. type NamedTagged interface { Named Tag() string } // Digested is an object which has a digest // in which it can be referenced by type Digested interface { Reference Digest() digest.Digest } // Canonical reference is an object with a fully unique // name including a name with domain and digest type Canonical interface { Named Digest() digest.Digest } // namedRepository is a reference to a repository with a name. // A namedRepository has both domain and path components. type namedRepository interface { Named Domain() string Path() string } // Domain returns the domain part of the [Named] reference. func Domain(named Named) string { if r, ok := named.(namedRepository); ok { return r.Domain() } domain, _ := splitDomain(named.Name()) return domain } // Path returns the name without the domain part of the [Named] reference. func Path(named Named) (name string) { if r, ok := named.(namedRepository); ok { return r.Path() } _, path := splitDomain(named.Name()) return path } // splitDomain splits a named reference into a hostname and path string. // If no valid hostname is found, the hostname is empty and the full value // is returned as name func splitDomain(name string) (string, string) { match := anchoredNameRegexp.FindStringSubmatch(name) if len(match) != 3 { return "", name } return match[1], match[2] } // Parse parses s and returns a syntactically valid Reference. // If an error was encountered it is returned, along with a nil Reference. func Parse(s string) (Reference, error) { matches := ReferenceRegexp.FindStringSubmatch(s) if matches == nil { if s == "" { return nil, ErrNameEmpty } if ReferenceRegexp.FindStringSubmatch(strings.ToLower(s)) != nil { return nil, ErrNameContainsUppercase } return nil, ErrReferenceInvalidFormat } var repo repository nameMatch := anchoredNameRegexp.FindStringSubmatch(matches[1]) if len(nameMatch) == 3 { repo.domain = nameMatch[1] repo.path = nameMatch[2] } else { repo.domain = "" repo.path = matches[1] } if len(repo.path) > RepositoryNameTotalLengthMax { return nil, ErrNameTooLong } ref := reference{ namedRepository: repo, tag: matches[2], } if matches[3] != "" { var err error ref.digest, err = digest.Parse(matches[3]) if err != nil { return nil, err } } r := getBestReferenceType(ref) if r == nil { return nil, ErrNameEmpty } return r, nil } // ParseNamed parses s and returns a syntactically valid reference implementing // the Named interface. The reference must have a name and be in the canonical // form, otherwise an error is returned. // If an error was encountered it is returned, along with a nil Reference. func ParseNamed(s string) (Named, error) { named, err := ParseNormalizedNamed(s) if err != nil { return nil, err } if named.String() != s { return nil, ErrNameNotCanonical } return named, nil } // WithName returns a named object representing the given string. If the input // is invalid ErrReferenceInvalidFormat will be returned. func WithName(name string) (Named, error) { match := anchoredNameRegexp.FindStringSubmatch(name) if match == nil || len(match) != 3 { return nil, ErrReferenceInvalidFormat } if len(match[2]) > RepositoryNameTotalLengthMax { return nil, ErrNameTooLong } return repository{ domain: match[1], path: match[2], }, nil } // WithTag combines the name from "name" and the tag from "tag" to form a // reference incorporating both the name and the tag. func WithTag(name Named, tag string) (NamedTagged, error) { if !anchoredTagRegexp.MatchString(tag) { return nil, ErrTagInvalidFormat } var repo repository if r, ok := name.(namedRepository); ok { repo.domain = r.Domain() repo.path = r.Path() } else { repo.path = name.Name() } if canonical, ok := name.(Canonical); ok { return reference{ namedRepository: repo, tag: tag, digest: canonical.Digest(), }, nil } return taggedReference{ namedRepository: repo, tag: tag, }, nil } // WithDigest combines the name from "name" and the digest from "digest" to form // a reference incorporating both the name and the digest. func WithDigest(name Named, digest digest.Digest) (Canonical, error) { if !anchoredDigestRegexp.MatchString(digest.String()) { return nil, ErrDigestInvalidFormat } var repo repository if r, ok := name.(namedRepository); ok { repo.domain = r.Domain() repo.path = r.Path() } else { repo.path = name.Name() } if tagged, ok := name.(Tagged); ok { return reference{ namedRepository: repo, tag: tagged.Tag(), digest: digest, }, nil } return canonicalReference{ namedRepository: repo, digest: digest, }, nil } // TrimNamed removes any tag or digest from the named reference. func TrimNamed(ref Named) Named { repo := repository{} if r, ok := ref.(namedRepository); ok { repo.domain, repo.path = r.Domain(), r.Path() } else { repo.domain, repo.path = splitDomain(ref.Name()) } return repo } func getBestReferenceType(ref reference) Reference { if ref.Name() == "" { // Allow digest only references if ref.digest != "" { return digestReference(ref.digest) } return nil } if ref.tag == "" { if ref.digest != "" { return canonicalReference{ namedRepository: ref.namedRepository, digest: ref.digest, } } return ref.namedRepository } if ref.digest == "" { return taggedReference{ namedRepository: ref.namedRepository, tag: ref.tag, } } return ref } type reference struct { namedRepository tag string digest digest.Digest } func (r reference) String() string { return r.Name() + ":" + r.tag + "@" + r.digest.String() } func (r reference) Tag() string { return r.tag } func (r reference) Digest() digest.Digest { return r.digest } type repository struct { domain string path string } func (r repository) String() string { return r.Name() } func (r repository) Name() string { if r.domain == "" { return r.path } return r.domain + "/" + r.path } func (r repository) Domain() string { return r.domain } func (r repository) Path() string { return r.path } type digestReference digest.Digest func (d digestReference) String() string { return digest.Digest(d).String() } func (d digestReference) Digest() digest.Digest { return digest.Digest(d) } type taggedReference struct { namedRepository tag string } func (t taggedReference) String() string { return t.Name() + ":" + t.tag } func (t taggedReference) Tag() string { return t.tag } type canonicalReference struct { namedRepository digest digest.Digest } func (c canonicalReference) String() string { return c.Name() + "@" + c.digest.String() } func (c canonicalReference) Digest() digest.Digest { return c.digest } reference-0.6.0/reference_test.go000066400000000000000000000450061457661414700170230ustar00rootroot00000000000000package reference import ( _ "crypto/sha256" _ "crypto/sha512" "encoding/json" "errors" "strings" "testing" "github.com/opencontainers/go-digest" ) func TestReferenceParse(t *testing.T) { t.Parallel() // tests is a unified set of testcases for // testing the parsing of references tests := []struct { // input is the repository name or name component testcase input string // err is the error expected from Parse, or nil err error // repository is the string representation for the reference repository string // domain is the domain expected in the reference domain string // tag is the tag for the reference tag string // digest is the digest for the reference (enforces digest reference) digest string }{ { input: "test_com", repository: "test_com", }, { input: "test.com:tag", repository: "test.com", tag: "tag", }, { input: "test.com:5000", repository: "test.com", tag: "5000", }, { input: "test.com/repo:tag", domain: "test.com", repository: "test.com/repo", tag: "tag", }, { input: "test:5000/repo", domain: "test:5000", repository: "test:5000/repo", }, { input: "test:5000/repo:tag", domain: "test:5000", repository: "test:5000/repo", tag: "tag", }, { input: "test:5000/repo@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", domain: "test:5000", repository: "test:5000/repo", digest: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", }, { input: "test:5000/repo:tag@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", domain: "test:5000", repository: "test:5000/repo", tag: "tag", digest: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", }, { input: "test:5000/repo", domain: "test:5000", repository: "test:5000/repo", }, { input: "", err: ErrNameEmpty, }, { input: ":justtag", err: ErrReferenceInvalidFormat, }, { input: "@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", err: ErrReferenceInvalidFormat, }, { input: "repo@sha256:ffffffffffffffffffffffffffffffffff", err: digest.ErrDigestInvalidLength, }, { input: "validname@invaliddigest:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", err: digest.ErrDigestUnsupported, }, { input: "Uppercase:tag", err: ErrNameContainsUppercase, }, // FIXME "Uppercase" is incorrectly handled as a domain-name here, therefore passes. // See https://github.com/distribution/distribution/pull/1778, and https://github.com/docker/docker/pull/20175 // { // input: "Uppercase/lowercase:tag", // err: ErrNameContainsUppercase, // }, { input: "test:5000/Uppercase/lowercase:tag", err: ErrNameContainsUppercase, }, { input: "lowercase:Uppercase", repository: "lowercase", tag: "Uppercase", }, { input: "domain/" + strings.Repeat("a", 256) + ":tag", err: ErrNameTooLong, }, { input: strings.Repeat("a/", 127) + "a:tag-puts-this-over-max", domain: "a", repository: strings.Repeat("a/", 127) + "a", tag: "tag-puts-this-over-max", }, { input: "aa/asdf$$^/aa", err: ErrReferenceInvalidFormat, }, { input: "sub-dom1.foo.com/bar/baz/quux", domain: "sub-dom1.foo.com", repository: "sub-dom1.foo.com/bar/baz/quux", }, { input: "sub-dom1.foo.com/bar/baz/quux:some-long-tag", domain: "sub-dom1.foo.com", repository: "sub-dom1.foo.com/bar/baz/quux", tag: "some-long-tag", }, { input: "b.gcr.io/test.example.com/my-app:test.example.com", domain: "b.gcr.io", repository: "b.gcr.io/test.example.com/my-app", tag: "test.example.com", }, { input: "xn--n3h.com/myimage:xn--n3h.com", // ☃.com in punycode domain: "xn--n3h.com", repository: "xn--n3h.com/myimage", tag: "xn--n3h.com", }, { input: "xn--7o8h.com/myimage:xn--7o8h.com@sha512:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", // 🐳.com in punycode domain: "xn--7o8h.com", repository: "xn--7o8h.com/myimage", tag: "xn--7o8h.com", digest: "sha512:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", }, { input: "foo_bar.com:8080", repository: "foo_bar.com", tag: "8080", }, { input: "foo/foo_bar.com:8080", domain: "foo", repository: "foo/foo_bar.com", tag: "8080", }, { input: "192.168.1.1", repository: "192.168.1.1", }, { input: "192.168.1.1:tag", repository: "192.168.1.1", tag: "tag", }, { input: "192.168.1.1:5000", repository: "192.168.1.1", tag: "5000", }, { input: "192.168.1.1/repo", domain: "192.168.1.1", repository: "192.168.1.1/repo", }, { input: "192.168.1.1:5000/repo", domain: "192.168.1.1:5000", repository: "192.168.1.1:5000/repo", }, { input: "192.168.1.1:5000/repo:5050", domain: "192.168.1.1:5000", repository: "192.168.1.1:5000/repo", tag: "5050", }, { input: "[2001:db8::1]", err: ErrReferenceInvalidFormat, }, { input: "[2001:db8::1]:5000", err: ErrReferenceInvalidFormat, }, { input: "[2001:db8::1]:tag", err: ErrReferenceInvalidFormat, }, { input: "[2001:db8::1]/repo", domain: "[2001:db8::1]", repository: "[2001:db8::1]/repo", }, { input: "[2001:db8:1:2:3:4:5:6]/repo:tag", domain: "[2001:db8:1:2:3:4:5:6]", repository: "[2001:db8:1:2:3:4:5:6]/repo", tag: "tag", }, { input: "[2001:db8::1]:5000/repo", domain: "[2001:db8::1]:5000", repository: "[2001:db8::1]:5000/repo", }, { input: "[2001:db8::1]:5000/repo:tag", domain: "[2001:db8::1]:5000", repository: "[2001:db8::1]:5000/repo", tag: "tag", }, { input: "[2001:db8::1]:5000/repo@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", domain: "[2001:db8::1]:5000", repository: "[2001:db8::1]:5000/repo", digest: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", }, { input: "[2001:db8::1]:5000/repo:tag@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", domain: "[2001:db8::1]:5000", repository: "[2001:db8::1]:5000/repo", tag: "tag", digest: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", }, { input: "[2001:db8::]:5000/repo", domain: "[2001:db8::]:5000", repository: "[2001:db8::]:5000/repo", }, { input: "[::1]:5000/repo", domain: "[::1]:5000", repository: "[::1]:5000/repo", }, { input: "[fe80::1%eth0]:5000/repo", err: ErrReferenceInvalidFormat, }, { input: "[fe80::1%@invalidzone]:5000/repo", err: ErrReferenceInvalidFormat, }, { input: "example.com/" + strings.Repeat("a", 255) + ":tag", domain: "example.com", repository: "example.com/" + strings.Repeat("a", 255), tag: "tag", }, } for _, tc := range tests { tc := tc t.Run(tc.input, func(t *testing.T) { t.Parallel() repo, err := Parse(tc.input) if tc.err != nil { if err == nil { t.Errorf("missing expected error: %v", tc.err) } else if tc.err != err { t.Errorf("mismatched error: got %v, expected %v", err, tc.err) } return } else if err != nil { t.Errorf("unexpected parse error: %v", err) return } if repo.String() != tc.input { t.Errorf("mismatched repo: got %q, expected %q", repo.String(), tc.input) } if named, ok := repo.(Named); ok { if named.Name() != tc.repository { t.Errorf("unexpected repository: got %q, expected %q", named.Name(), tc.repository) } if domain := Domain(named); domain != tc.domain { t.Errorf("unexpected domain: got %q, expected %q", domain, tc.domain) } } else if tc.repository != "" || tc.domain != "" { t.Errorf("expected named type, got %T", repo) } tagged, ok := repo.(Tagged) if tc.tag != "" { if ok { if tagged.Tag() != tc.tag { t.Errorf("unexpected tag: got %q, expected %q", tagged.Tag(), tc.tag) } } else { t.Errorf("expected tagged type, got %T", repo) } } else if ok { t.Errorf("unexpected tagged type") } digested, ok := repo.(Digested) if tc.digest != "" { if ok { if digested.Digest().String() != tc.digest { t.Errorf("unexpected digest: got %q, expected %q", digested.Digest().String(), tc.digest) } } else { t.Errorf("expected digested type, got %T", repo) } } else if ok { t.Errorf("unexpected digested type") } }) } } // TestWithNameFailure tests cases where WithName should fail. Cases where it // should succeed are covered by TestSplitHostname, below. func TestWithNameFailure(t *testing.T) { t.Parallel() tests := []struct { input string err error }{ { input: "", err: ErrReferenceInvalidFormat, }, { input: ":justtag", err: ErrReferenceInvalidFormat, }, { input: "@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", err: ErrReferenceInvalidFormat, }, { input: "validname@invaliddigest:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", err: ErrReferenceInvalidFormat, }, { input: "example.com/repo:tag", err: ErrReferenceInvalidFormat, }, { input: "example.com/" + strings.Repeat("a", 256), err: ErrNameTooLong, }, { input: "aa/asdf$$^/aa", err: ErrReferenceInvalidFormat, }, } for _, tc := range tests { tc := tc t.Run(tc.input, func(t *testing.T) { t.Parallel() _, err := WithName(tc.input) if !errors.Is(err, tc.err) { t.Errorf("unexpected error parsing name. expected: %s, got: %s", tc.err, err) } }) } } func TestDomainAndPath(t *testing.T) { t.Parallel() tests := []struct { input string domain string path string }{ { input: "test.com/foo", domain: "test.com", path: "foo", }, { input: "test_com/foo", domain: "", path: "test_com/foo", }, { input: "test:8080/foo", domain: "test:8080", path: "foo", }, { input: "test.com:8080/foo", domain: "test.com:8080", path: "foo", }, { input: "test-com:8080/foo", domain: "test-com:8080", path: "foo", }, { input: "xn--n3h.com:18080/foo", domain: "xn--n3h.com:18080", path: "foo", }, } for _, tc := range tests { tc := tc t.Run(tc.input, func(t *testing.T) { t.Parallel() named, err := WithName(tc.input) if err != nil { t.Errorf("error parsing name: %s", err) } if domain := Domain(named); domain != tc.domain { t.Errorf("unexpected domain: got %q, expected %q", domain, tc.domain) } if path := Path(named); path != tc.path { t.Errorf("unexpected name: got %q, expected %q", path, tc.path) } }) } } type serializationType struct { Description string Field Field } func TestSerialization(t *testing.T) { t.Parallel() tests := []struct { description string input string name string tag string digest string err error }{ { description: "empty value", err: ErrNameEmpty, }, { description: "just a name", input: "example.com:8000/named", name: "example.com:8000/named", }, { description: "name with a tag", input: "example.com:8000/named:tagged", name: "example.com:8000/named", tag: "tagged", }, { description: "name with digest", input: "other.com/named@sha256:1234567890098765432112345667890098765432112345667890098765432112", name: "other.com/named", digest: "sha256:1234567890098765432112345667890098765432112345667890098765432112", }, } for _, tc := range tests { tc := tc t.Run(tc.description, func(t *testing.T) { t.Parallel() m := map[string]string{ "Description": tc.description, "Field": tc.input, } b, err := json.Marshal(m) if err != nil { t.Errorf("error marshalling: %v", err) } st := serializationType{} if err := json.Unmarshal(b, &st); err != nil { if tc.err == nil { t.Errorf("error unmarshalling: %v", err) } if err != tc.err { t.Errorf("wrong error, expected %v, got %v", tc.err, err) } return } else if tc.err != nil { t.Errorf("expected error unmarshalling: %v", tc.err) } if st.Description != tc.description { t.Errorf("wrong description, expected %q, got %q", tc.description, st.Description) } ref := st.Field.Reference() if named, ok := ref.(Named); ok { if named.Name() != tc.name { t.Errorf("unexpected repository: got %q, expected %q", named.Name(), tc.name) } } else if tc.name != "" { t.Errorf("expected named type, got %T", ref) } tagged, ok := ref.(Tagged) if tc.tag != "" { if ok { if tagged.Tag() != tc.tag { t.Errorf("unexpected tag: got %q, expected %q", tagged.Tag(), tc.tag) } } else { t.Errorf("expected tagged type, got %T", ref) } } else if ok { t.Errorf("unexpected tagged type") } digested, ok := ref.(Digested) if tc.digest != "" { if ok { if digested.Digest().String() != tc.digest { t.Errorf("unexpected digest: got %q, expected %q", digested.Digest().String(), tc.digest) } } else { t.Errorf("expected digested type, got %T", ref) } } else if ok { t.Errorf("unexpected digested type") } st = serializationType{ Description: tc.description, Field: AsField(ref), } b2, err := json.Marshal(st) if err != nil { t.Errorf("error marshing serialization type: %v", err) } if string(b) != string(b2) { t.Errorf("unexpected serialized value: expected %q, got %q", string(b), string(b2)) } // Ensure st.Field is not implementing "Reference" directly, getting // around the Reference type system var fieldInterface interface{} = st.Field if _, ok := fieldInterface.(Reference); ok { t.Errorf("field should not implement Reference interface") } }) } } func TestWithTag(t *testing.T) { t.Parallel() tests := []struct { name string digest digest.Digest tag string combined string }{ { name: "test.com/foo", tag: "tag", combined: "test.com/foo:tag", }, { name: "foo", tag: "tag2", combined: "foo:tag2", }, { name: "test.com:8000/foo", tag: "tag4", combined: "test.com:8000/foo:tag4", }, { name: "test.com:8000/foo", tag: "TAG5", combined: "test.com:8000/foo:TAG5", }, { name: "test.com:8000/foo", digest: "sha256:1234567890098765432112345667890098765", tag: "TAG5", combined: "test.com:8000/foo:TAG5@sha256:1234567890098765432112345667890098765", }, } for _, tc := range tests { tc := tc t.Run(tc.combined, func(t *testing.T) { t.Parallel() named, err := WithName(tc.name) if err != nil { t.Errorf("error parsing name: %s", err) } if tc.digest != "" { canonical, err := WithDigest(named, tc.digest) if err != nil { t.Errorf("error adding digest") } named = canonical } tagged, err := WithTag(named, tc.tag) if err != nil { t.Errorf("WithTag failed: %s", err) } if tagged.String() != tc.combined { t.Errorf("unexpected: got %q, expected %q", tagged.String(), tc.combined) } }) } } func TestWithDigest(t *testing.T) { t.Parallel() tests := []struct { name string digest digest.Digest tag string combined string }{ { name: "test.com/foo", digest: "sha256:1234567890098765432112345667890098765", combined: "test.com/foo@sha256:1234567890098765432112345667890098765", }, { name: "foo", digest: "sha256:1234567890098765432112345667890098765", combined: "foo@sha256:1234567890098765432112345667890098765", }, { name: "test.com:8000/foo", digest: "sha256:1234567890098765432112345667890098765", combined: "test.com:8000/foo@sha256:1234567890098765432112345667890098765", }, { name: "test.com:8000/foo", digest: "sha256:1234567890098765432112345667890098765", tag: "latest", combined: "test.com:8000/foo:latest@sha256:1234567890098765432112345667890098765", }, } for _, tc := range tests { tc := tc t.Run(tc.combined, func(t *testing.T) { t.Parallel() named, err := WithName(tc.name) if err != nil { t.Errorf("error parsing name: %s", err) } if tc.tag != "" { tagged, err := WithTag(named, tc.tag) if err != nil { t.Errorf("error adding tag") } named = tagged } digested, err := WithDigest(named, tc.digest) if err != nil { t.Errorf("WithDigest failed: %s", err) } if digested.String() != tc.combined { t.Errorf("unexpected: got %q, expected %q", digested.String(), tc.combined) } }) } } func TestParseNamed(t *testing.T) { t.Parallel() tests := []struct { input string domain string path string err error }{ { input: "test.com/foo", domain: "test.com", path: "foo", }, { input: "test:8080/foo", domain: "test:8080", path: "foo", }, { input: "test_com/foo", err: ErrNameNotCanonical, }, { input: "test.com", err: ErrNameNotCanonical, }, { input: "foo", err: ErrNameNotCanonical, }, { input: "library/foo", err: ErrNameNotCanonical, }, { input: "docker.io/library/foo", domain: "docker.io", path: "library/foo", }, // Ambiguous case, parser will add "library/" to foo { input: "docker.io/foo", err: ErrNameNotCanonical, }, } for _, tc := range tests { tc := tc t.Run(tc.input, func(t *testing.T) { t.Parallel() named, err := ParseNamed(tc.input) if err != nil && tc.err == nil { t.Errorf("error parsing name: %s", err) return } else if err == nil && tc.err != nil { t.Errorf("parsing succeeded: expected error %v", tc.err) return } else if err != tc.err { t.Errorf("unexpected error %v, expected %v", err, tc.err) return } else if err != nil { return } if domain := Domain(named); domain != tc.domain { t.Errorf("unexpected domain: got %q, expected %q", domain, tc.domain) } if path := Path(named); path != tc.path { t.Errorf("unexpected name: got %q, expected %q", path, tc.path) } }) } } reference-0.6.0/regexp.go000066400000000000000000000160311457661414700153140ustar00rootroot00000000000000package reference import ( "regexp" "strings" ) // DigestRegexp matches well-formed digests, including algorithm (e.g. "sha256:"). var DigestRegexp = regexp.MustCompile(digestPat) // DomainRegexp matches hostname or IP-addresses, optionally including a port // number. It defines the structure of potential domain components that may be // part of image names. This is purposely a subset of what is allowed by DNS to // ensure backwards compatibility with Docker image names. It may be a subset of // DNS domain name, an IPv4 address in decimal format, or an IPv6 address between // square brackets (excluding zone identifiers as defined by [RFC 6874] or special // addresses such as IPv4-Mapped). // // [RFC 6874]: https://www.rfc-editor.org/rfc/rfc6874. var DomainRegexp = regexp.MustCompile(domainAndPort) // IdentifierRegexp is the format for string identifier used as a // content addressable identifier using sha256. These identifiers // are like digests without the algorithm, since sha256 is used. var IdentifierRegexp = regexp.MustCompile(identifier) // NameRegexp is the format for the name component of references, including // an optional domain and port, but without tag or digest suffix. var NameRegexp = regexp.MustCompile(namePat) // ReferenceRegexp is the full supported format of a reference. The regexp // is anchored and has capturing groups for name, tag, and digest // components. var ReferenceRegexp = regexp.MustCompile(referencePat) // TagRegexp matches valid tag names. From [docker/docker:graph/tags.go]. // // [docker/docker:graph/tags.go]: https://github.com/moby/moby/blob/v1.6.0/graph/tags.go#L26-L28 var TagRegexp = regexp.MustCompile(tag) const ( // alphanumeric defines the alphanumeric atom, typically a // component of names. This only allows lower case characters and digits. alphanumeric = `[a-z0-9]+` // separator defines the separators allowed to be embedded in name // components. This allows one period, one or two underscore and multiple // dashes. Repeated dashes and underscores are intentionally treated // differently. In order to support valid hostnames as name components, // supporting repeated dash was added. Additionally double underscore is // now allowed as a separator to loosen the restriction for previously // supported names. separator = `(?:[._]|__|[-]+)` // localhost is treated as a special value for domain-name. Any other // domain-name without a "." or a ":port" are considered a path component. localhost = `localhost` // domainNameComponent restricts the registry domain component of a // repository name to start with a component as defined by DomainRegexp. domainNameComponent = `(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])` // optionalPort matches an optional port-number including the port separator // (e.g. ":80"). optionalPort = `(?::[0-9]+)?` // tag matches valid tag names. From docker/docker:graph/tags.go. tag = `[\w][\w.-]{0,127}` // digestPat matches well-formed digests, including algorithm (e.g. "sha256:"). // // TODO(thaJeztah): this should follow the same rules as https://pkg.go.dev/github.com/opencontainers/go-digest@v1.0.0#DigestRegexp // so that go-digest defines the canonical format. Note that the go-digest is // more relaxed: // - it allows multiple algorithms (e.g. "sha256+b64:") to allow // future expansion of supported algorithms. // - it allows the "" value to use urlsafe base64 encoding as defined // in [rfc4648, section 5]. // // [rfc4648, section 5]: https://www.rfc-editor.org/rfc/rfc4648#section-5. digestPat = `[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}` // identifier is the format for a content addressable identifier using sha256. // These identifiers are like digests without the algorithm, since sha256 is used. identifier = `([a-f0-9]{64})` // ipv6address are enclosed between square brackets and may be represented // in many ways, see rfc5952. Only IPv6 in compressed or uncompressed format // are allowed, IPv6 zone identifiers (rfc6874) or Special addresses such as // IPv4-Mapped are deliberately excluded. ipv6address = `\[(?:[a-fA-F0-9:]+)\]` ) var ( // domainName defines the structure of potential domain components // that may be part of image names. This is purposely a subset of what is // allowed by DNS to ensure backwards compatibility with Docker image // names. This includes IPv4 addresses on decimal format. domainName = domainNameComponent + anyTimes(`\.`+domainNameComponent) // host defines the structure of potential domains based on the URI // Host subcomponent on rfc3986. It may be a subset of DNS domain name, // or an IPv4 address in decimal format, or an IPv6 address between square // brackets (excluding zone identifiers as defined by rfc6874 or special // addresses such as IPv4-Mapped). host = `(?:` + domainName + `|` + ipv6address + `)` // allowed by the URI Host subcomponent on rfc3986 to ensure backwards // compatibility with Docker image names. domainAndPort = host + optionalPort // anchoredTagRegexp matches valid tag names, anchored at the start and // end of the matched string. anchoredTagRegexp = regexp.MustCompile(anchored(tag)) // anchoredDigestRegexp matches valid digests, anchored at the start and // end of the matched string. anchoredDigestRegexp = regexp.MustCompile(anchored(digestPat)) // pathComponent restricts path-components to start with an alphanumeric // character, with following parts able to be separated by a separator // (one period, one or two underscore and multiple dashes). pathComponent = alphanumeric + anyTimes(separator+alphanumeric) // remoteName matches the remote-name of a repository. It consists of one // or more forward slash (/) delimited path-components: // // pathComponent[[/pathComponent] ...] // e.g., "library/ubuntu" remoteName = pathComponent + anyTimes(`/`+pathComponent) namePat = optional(domainAndPort+`/`) + remoteName // anchoredNameRegexp is used to parse a name value, capturing the // domain and trailing components. anchoredNameRegexp = regexp.MustCompile(anchored(optional(capture(domainAndPort), `/`), capture(remoteName))) referencePat = anchored(capture(namePat), optional(`:`, capture(tag)), optional(`@`, capture(digestPat))) // anchoredIdentifierRegexp is used to check or match an // identifier value, anchored at start and end of string. anchoredIdentifierRegexp = regexp.MustCompile(anchored(identifier)) ) // optional wraps the expression in a non-capturing group and makes the // production optional. func optional(res ...string) string { return `(?:` + strings.Join(res, "") + `)?` } // anyTimes wraps the expression in a non-capturing group that can occur // any number of times. func anyTimes(res ...string) string { return `(?:` + strings.Join(res, "") + `)*` } // capture wraps the expression in a capturing group. func capture(res ...string) string { return `(` + strings.Join(res, "") + `)` } // anchored anchors the regular expression by adding start and end delimiters. func anchored(res ...string) string { return `^` + strings.Join(res, "") + `$` } reference-0.6.0/regexp_bench_test.go000066400000000000000000000074631457661414700175230ustar00rootroot00000000000000package reference import ( "strings" "testing" ) func BenchmarkParse(b *testing.B) { tests := []regexpMatch{ { input: "", match: false, }, { input: "short", match: true, }, { input: "simple/name", match: true, }, { input: "library/ubuntu", match: true, }, { input: "docker/stevvooe/app", match: true, }, { input: "aa/aa/aa/aa/aa/aa/aa/aa/aa/bb/bb/bb/bb/bb/bb", match: true, }, { input: "aa/aa/bb/bb/bb", match: true, }, { input: "a/a/a/a", match: true, }, { input: "a/a/a/a/", match: false, }, { input: "a//a/a", match: false, }, { input: "a", match: true, }, { input: "a/aa", match: true, }, { input: "a/aa/a", match: true, }, { input: "foo.com", match: true, }, { input: "foo.com/", match: false, }, { input: "foo.com:8080/bar", match: true, }, { input: "foo.com:http/bar", match: false, }, { input: "foo.com/bar", match: true, }, { input: "foo.com/bar/baz", match: true, }, { input: "localhost:8080/bar", match: true, }, { input: "sub-dom1.foo.com/bar/baz/quux", match: true, }, { input: "blog.foo.com/bar/baz", match: true, }, { input: "a^a", match: false, }, { input: "aa/asdf$$^/aa", match: false, }, { input: "asdf$$^/aa", match: false, }, { input: "aa-a/a", match: true, }, { input: strings.Repeat("a/", 128) + "a", match: true, }, { input: "a-/a/a/a", match: false, }, { input: "foo.com/a-/a/a", match: false, }, { input: "-foo/bar", match: false, }, { input: "foo/bar-", match: false, }, { input: "foo-/bar", match: false, }, { input: "foo/-bar", match: false, }, { input: "_foo/bar", match: false, }, { input: "foo_bar", match: true, }, { input: "foo_bar.com", match: true, }, { input: "foo_bar.com:8080", match: false, }, { input: "foo_bar.com:8080/app", match: false, }, { input: "foo.com/foo_bar", match: true, }, { input: "____/____", match: false, }, { input: "_docker/_docker", match: false, }, { input: "docker_/docker_", match: false, }, { input: "b.gcr.io/test.example.com/my-app", match: true, }, { input: "xn--n3h.com/myimage", // ☃.com in punycode match: true, }, { input: "xn--7o8h.com/myimage", // 🐳.com in punycode match: true, }, { input: "example.com/xn--7o8h.com/myimage", // 🐳.com in punycode match: true, }, { input: "example.com/some_separator__underscore/myimage", match: true, }, { input: "example.com/__underscore/myimage", match: false, }, { input: "example.com/..dots/myimage", match: false, }, { input: "example.com/.dots/myimage", match: false, }, { input: "example.com/nodouble..dots/myimage", match: false, }, { input: "example.com/nodouble..dots/myimage", match: false, }, { input: "docker./docker", match: false, }, { input: ".docker/docker", match: false, }, { input: "docker-/docker", match: false, }, { input: "-docker/docker", match: false, }, { input: "do..cker/docker", match: false, }, { input: "do__cker:8080/docker", match: false, }, { input: "do__cker/docker", match: true, }, { input: "b.gcr.io/test.example.com/my-app", match: true, }, { input: "registry.io/foo/project--id.module--name.ver---sion--name", match: true, }, { input: "Asdf.com/foo/bar", // uppercase character in hostname match: true, }, { input: "Foo/FarB", // uppercase characters in remote name match: false, }, } b.ReportAllocs() for i := 0; i < b.N; i++ { for _, tc := range tests { _, _ = Parse(tc.input) } } } reference-0.6.0/regexp_test.go000066400000000000000000000274521457661414700163640ustar00rootroot00000000000000package reference import ( "regexp" "strings" "testing" ) type regexpMatch struct { input string match bool subs []string } func checkRegexp(t *testing.T, r *regexp.Regexp, m regexpMatch) { t.Helper() matches := r.FindStringSubmatch(m.input) if m.match && matches != nil { if len(matches) != (r.NumSubexp()+1) || matches[0] != m.input { t.Fatalf("Bad match result %#v for %q", matches, m.input) } if len(matches) < (len(m.subs) + 1) { t.Errorf("Expected %d sub matches, only have %d for %q", len(m.subs), len(matches)-1, m.input) } for i := range m.subs { if m.subs[i] != matches[i+1] { t.Errorf("Unexpected submatch %d: %q, expected %q for %q", i+1, matches[i+1], m.subs[i], m.input) } } } else if m.match { t.Errorf("Expected match for %q", m.input) } else if matches != nil { t.Errorf("Unexpected match for %q", m.input) } } func TestDomainRegexp(t *testing.T) { t.Parallel() tests := []struct { input string match bool }{ { input: "test.com", match: true, }, { input: "test.com:10304", match: true, }, { input: "test.com:http", match: false, }, { input: "localhost", match: true, }, { input: "localhost:8080", match: true, }, { input: "a", match: true, }, { input: "a.b", match: true, }, { input: "ab.cd.com", match: true, }, { input: "a-b.com", match: true, }, { input: "-ab.com", match: false, }, { input: "ab-.com", match: false, }, { input: "ab.c-om", match: true, }, { input: "ab.-com", match: false, }, { input: "ab.com-", match: false, }, { input: "0101.com", match: true, // TODO(dmcgowan): valid if this should be allowed }, { input: "001a.com", match: true, }, { input: "b.gbc.io:443", match: true, }, { input: "b.gbc.io", match: true, }, { input: "xn--n3h.com", // ☃.com in punycode match: true, }, { input: "Asdf.com", // uppercase character match: true, }, { input: "192.168.1.1:75050", // ipv4 match: true, }, { input: "192.168.1.1:750050", // port with more than 5 digits, it will fail on validation match: true, }, { input: "[fd00:1:2::3]:75050", // ipv6 compressed match: true, }, { input: "[fd00:1:2::3]75050", // ipv6 wrong port separator match: false, }, { input: "[fd00:1:2::3]::75050", // ipv6 wrong port separator match: false, }, { input: "[fd00:1:2::3%eth0]:75050", // ipv6 with zone match: false, }, { input: "[fd00123123123]:75050", // ipv6 wrong format, will fail in validation match: true, }, { input: "[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:75050", // ipv6 long format match: true, }, { input: "[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:750505", // ipv6 long format and invalid port, it will fail in validation match: true, }, { input: "fd00:1:2::3:75050", // bad ipv6 without square brackets match: false, }, } r := regexp.MustCompile(`^` + DomainRegexp.String() + `$`) for _, tc := range tests { tc := tc t.Run(tc.input, func(t *testing.T) { t.Parallel() match := r.MatchString(tc.input) if match != tc.match { t.Errorf("Expected match=%t, got %t", tc.match, match) } }) } } func TestFullNameRegexp(t *testing.T) { t.Parallel() if anchoredNameRegexp.NumSubexp() != 2 { t.Fatalf("anchored name regexp should have two submatches: %v, %v != 2", anchoredNameRegexp, anchoredNameRegexp.NumSubexp()) } tests := []regexpMatch{ { input: "", match: false, }, { input: "short", match: true, subs: []string{"", "short"}, }, { input: "simple/name", match: true, subs: []string{"simple", "name"}, }, { input: "library/ubuntu", match: true, subs: []string{"library", "ubuntu"}, }, { input: "docker/stevvooe/app", match: true, subs: []string{"docker", "stevvooe/app"}, }, { input: "aa/aa/aa/aa/aa/aa/aa/aa/aa/bb/bb/bb/bb/bb/bb", match: true, subs: []string{"aa", "aa/aa/aa/aa/aa/aa/aa/aa/bb/bb/bb/bb/bb/bb"}, }, { input: "aa/aa/bb/bb/bb", match: true, subs: []string{"aa", "aa/bb/bb/bb"}, }, { input: "a/a/a/a", match: true, subs: []string{"a", "a/a/a"}, }, { input: "a/a/a/a/", match: false, }, { input: "a//a/a", match: false, }, { input: "a", match: true, subs: []string{"", "a"}, }, { input: "a/aa", match: true, subs: []string{"a", "aa"}, }, { input: "a/aa/a", match: true, subs: []string{"a", "aa/a"}, }, { input: "foo.com", match: true, subs: []string{"", "foo.com"}, }, { input: "foo.com/", match: false, }, { input: "foo.com:8080/bar", match: true, subs: []string{"foo.com:8080", "bar"}, }, { input: "foo.com:http/bar", match: false, }, { input: "foo.com/bar", match: true, subs: []string{"foo.com", "bar"}, }, { input: "foo.com/bar/baz", match: true, subs: []string{"foo.com", "bar/baz"}, }, { input: "localhost:8080/bar", match: true, subs: []string{"localhost:8080", "bar"}, }, { input: "sub-dom1.foo.com/bar/baz/quux", match: true, subs: []string{"sub-dom1.foo.com", "bar/baz/quux"}, }, { input: "blog.foo.com/bar/baz", match: true, subs: []string{"blog.foo.com", "bar/baz"}, }, { input: "a^a", match: false, }, { input: "aa/asdf$$^/aa", match: false, }, { input: "asdf$$^/aa", match: false, }, { input: "aa-a/a", match: true, subs: []string{"aa-a", "a"}, }, { input: strings.Repeat("a/", 128) + "a", match: true, subs: []string{"a", strings.Repeat("a/", 127) + "a"}, }, { input: "a-/a/a/a", match: false, }, { input: "foo.com/a-/a/a", match: false, }, { input: "-foo/bar", match: false, }, { input: "foo/bar-", match: false, }, { input: "foo-/bar", match: false, }, { input: "foo/-bar", match: false, }, { input: "_foo/bar", match: false, }, { input: "foo_bar", match: true, subs: []string{"", "foo_bar"}, }, { input: "foo_bar.com", match: true, subs: []string{"", "foo_bar.com"}, }, { input: "foo_bar.com:8080", match: false, }, { input: "foo_bar.com:8080/app", match: false, }, { input: "foo.com/foo_bar", match: true, subs: []string{"foo.com", "foo_bar"}, }, { input: "____/____", match: false, }, { input: "_docker/_docker", match: false, }, { input: "docker_/docker_", match: false, }, { input: "b.gcr.io/test.example.com/my-app", match: true, subs: []string{"b.gcr.io", "test.example.com/my-app"}, }, { input: "xn--n3h.com/myimage", // ☃.com in punycode match: true, subs: []string{"xn--n3h.com", "myimage"}, }, { input: "xn--7o8h.com/myimage", // 🐳.com in punycode match: true, subs: []string{"xn--7o8h.com", "myimage"}, }, { input: "example.com/xn--7o8h.com/myimage", // 🐳.com in punycode match: true, subs: []string{"example.com", "xn--7o8h.com/myimage"}, }, { input: "example.com/some_separator__underscore/myimage", match: true, subs: []string{"example.com", "some_separator__underscore/myimage"}, }, { input: "example.com/__underscore/myimage", match: false, }, { input: "example.com/..dots/myimage", match: false, }, { input: "example.com/.dots/myimage", match: false, }, { input: "example.com/nodouble..dots/myimage", match: false, }, { input: "example.com/nodouble..dots/myimage", match: false, }, { input: "docker./docker", match: false, }, { input: ".docker/docker", match: false, }, { input: "docker-/docker", match: false, }, { input: "-docker/docker", match: false, }, { input: "do..cker/docker", match: false, }, { input: "do__cker:8080/docker", match: false, }, { input: "do__cker/docker", match: true, subs: []string{"", "do__cker/docker"}, }, { input: "b.gcr.io/test.example.com/my-app", match: true, subs: []string{"b.gcr.io", "test.example.com/my-app"}, }, { input: "registry.io/foo/project--id.module--name.ver---sion--name", match: true, subs: []string{"registry.io", "foo/project--id.module--name.ver---sion--name"}, }, { input: "Asdf.com/foo/bar", // uppercase character in hostname match: true, }, { input: "Foo/FarB", // uppercase characters in remote name match: false, }, } for _, tc := range tests { tc := tc t.Run(tc.input, func(t *testing.T) { t.Parallel() checkRegexp(t, anchoredNameRegexp, tc) }) } } func TestReferenceRegexp(t *testing.T) { t.Parallel() if ReferenceRegexp.NumSubexp() != 3 { t.Fatalf("anchored name regexp should have three submatches: %v, %v != 3", ReferenceRegexp, ReferenceRegexp.NumSubexp()) } tests := []regexpMatch{ { input: "registry.com:8080/myapp:tag", match: true, subs: []string{"registry.com:8080/myapp", "tag", ""}, }, { input: "registry.com:8080/myapp@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912", match: true, subs: []string{"registry.com:8080/myapp", "", "sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912"}, }, { input: "registry.com:8080/myapp:tag2@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912", match: true, subs: []string{"registry.com:8080/myapp", "tag2", "sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912"}, }, { input: "registry.com:8080/myapp@sha256:badbadbadbad", match: false, }, { input: "registry.com:8080/myapp:invalid~tag", match: false, }, { input: "bad_hostname.com:8080/myapp:tag", match: false, }, { input:// localhost treated as name, missing tag with 8080 as tag "localhost:8080@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912", match: true, subs: []string{"localhost", "8080", "sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912"}, }, { input: "localhost:8080/name@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912", match: true, subs: []string{"localhost:8080/name", "", "sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912"}, }, { input: "localhost:http/name@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912", match: false, }, { // localhost will be treated as an image name without a host input: "localhost@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912", match: true, subs: []string{"localhost", "", "sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912"}, }, { input: "registry.com:8080/myapp@bad", match: false, }, { input: "registry.com:8080/myapp@2bad", match: false, // TODO(dmcgowan): Support this as valid }, } for _, tc := range tests { tc := tc t.Run(tc.input, func(t *testing.T) { t.Parallel() checkRegexp(t, ReferenceRegexp, tc) }) } } func TestIdentifierRegexp(t *testing.T) { t.Parallel() tests := []struct { input string match bool }{ { input: "da304e823d8ca2b9d863a3c897baeb852ba21ea9a9f1414736394ae7fcaf9821", match: true, }, { input: "7EC43B381E5AEFE6E04EFB0B3F0693FF2A4A50652D64AEC573905F2DB5889A1C", match: false, }, { input: "da304e823d8ca2b9d863a3c897baeb852ba21ea9a9f1414736394ae7fcaf", match: false, }, { input: "sha256:da304e823d8ca2b9d863a3c897baeb852ba21ea9a9f1414736394ae7fcaf9821", match: false, }, { input: "da304e823d8ca2b9d863a3c897baeb852ba21ea9a9f1414736394ae7fcaf98218482", match: false, }, } for _, tc := range tests { tc := tc t.Run(tc.input, func(t *testing.T) { t.Parallel() match := anchoredIdentifierRegexp.MatchString(tc.input) if match != tc.match { t.Errorf("Expected match=%t, got %t", tc.match, match) } }) } } reference-0.6.0/sort.go000066400000000000000000000037221457661414700150140ustar00rootroot00000000000000/* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package reference import ( "sort" ) // Sort sorts string references preferring higher information references. // // The precedence is as follows: // // 1. [Named] + [Tagged] + [Digested] (e.g., "docker.io/library/busybox:latest@sha256:") // 2. [Named] + [Tagged] (e.g., "docker.io/library/busybox:latest") // 3. [Named] + [Digested] (e.g., "docker.io/library/busybo@sha256:") // 4. [Named] (e.g., "docker.io/library/busybox") // 5. [Digested] (e.g., "docker.io@sha256:") // 6. Parse error func Sort(references []string) []string { var prefs []Reference var bad []string for _, ref := range references { pref, err := ParseAnyReference(ref) if err != nil { bad = append(bad, ref) } else { prefs = append(prefs, pref) } } sort.Slice(prefs, func(a, b int) bool { ar := refRank(prefs[a]) br := refRank(prefs[b]) if ar == br { return prefs[a].String() < prefs[b].String() } return ar < br }) sort.Strings(bad) var refs []string for _, pref := range prefs { refs = append(refs, pref.String()) } return append(refs, bad...) } func refRank(ref Reference) uint8 { if _, ok := ref.(Named); ok { if _, ok = ref.(Tagged); ok { if _, ok = ref.(Digested); ok { return 1 } return 2 } if _, ok = ref.(Digested); ok { return 3 } return 4 } return 5 } reference-0.6.0/sort_test.go000066400000000000000000000051011457661414700160440ustar00rootroot00000000000000/* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package reference import ( "io" "math/rand" "testing" "github.com/opencontainers/go-digest" ) func TestReferenceSorting(t *testing.T) { t.Parallel() digested := func(seed int64) string { b, err := io.ReadAll(io.LimitReader(rand.New(rand.NewSource(seed)), 64)) if err != nil { panic(err) } return digest.FromBytes(b).String() } // Add z. prefix to string sort after "sha256:" r1 := func(name, tag string, seed int64) string { return "z.containerd.io/" + name + ":" + tag + "@" + digested(seed) } r2 := func(name, tag string) string { return "z.containerd.io/" + name + ":" + tag } r3 := func(name string, seed int64) string { return "z.containerd.io/" + name + "@" + digested(seed) } for i, tc := range []struct { unsorted []string expected []string }{ { unsorted: []string{r2("name", "latest"), r3("name", 1), r1("name", "latest", 1)}, expected: []string{r1("name", "latest", 1), r2("name", "latest"), r3("name", 1)}, }, { unsorted: []string{"can't parse this:latest", r3("name", 1), r2("name", "latest")}, expected: []string{r2("name", "latest"), r3("name", 1), "can't parse this:latest"}, }, { unsorted: []string{digested(1), r3("name", 1), r2("name", "latest")}, expected: []string{r2("name", "latest"), r3("name", 1), digested(1)}, }, { unsorted: []string{r2("name", "tag2"), r2("name", "tag3"), r2("name", "tag1")}, expected: []string{r2("name", "tag1"), r2("name", "tag2"), r2("name", "tag3")}, }, { unsorted: []string{r2("name-2", "tag"), r2("name-3", "tag"), r2("name-1", "tag")}, expected: []string{r2("name-1", "tag"), r2("name-2", "tag"), r2("name-3", "tag")}, }, } { sorted := Sort(tc.unsorted) if len(sorted) != len(tc.expected) { t.Errorf("[%d]: Mismatched sized, got %d, expected %d", i, len(sorted), len(tc.expected)) continue } for j := range sorted { if sorted[j] != tc.expected[j] { t.Errorf("[%d]: Wrong value at %d, got %q, expected %q", i, j, sorted[j], tc.expected[j]) break } } } }