pax_global_header00006660000000000000000000000064152135310330014506gustar00rootroot0000000000000052 comment=bfb0bdcf3ce01c8cc0eb945f843f618037361850 golang-go.yaml-yaml-v4-4.0.0~rc5/000077500000000000000000000000001521353103300164215ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc5/.github/000077500000000000000000000000001521353103300177615ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc5/.github/dependabot.yaml000066400000000000000000000013361521353103300227550ustar00rootroot00000000000000# Copyright 2025 The go-yaml Project Contributors # SPDX-License-Identifier: Apache-2.0 version: 2 updates: - package-ecosystem: gomod directory: / # Location of package manifests cooldown: # Supply chain attacks are more likely to be detected and mitigated after a # few days, so this can help reduce the risk of introducing malicious code. default-days: 10 schedule: interval: weekly - package-ecosystem: github-actions directory: / groups: actions: patterns: ['*'] cooldown: # Supply chain attacks are more likely to be detected and mitigated after a # few days, so this can help reduce the risk of introducing malicious code. default-days: 10 schedule: interval: weekly golang-go.yaml-yaml-v4-4.0.0~rc5/.github/workflows/000077500000000000000000000000001521353103300220165ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc5/.github/workflows/check-commit-message.yaml000066400000000000000000000014251521353103300266710ustar00rootroot00000000000000# Copyright 2025 The go-yaml Project Contributors # SPDX-License-Identifier: Apache-2.0 name: Commit Message Check on: pull_request: types: [opened, edited, reopened, synchronize] permissions: read-all jobs: check-commit-message: name: Check Commit Message runs-on: ubuntu-latest if: github.event.pull_request.user.login != 'dependabot[bot]' steps: - name: Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 # Fetch all history to ensure all SHAs are available persist-credentials: false - name: Check PR Commits run: util/check-commit-messages ${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }} golang-go.yaml-yaml-v4-4.0.0~rc5/.github/workflows/codeql.yaml000066400000000000000000000024741521353103300241600ustar00rootroot00000000000000# Copyright 2025 The go-yaml Project Contributors # SPDX-License-Identifier: Apache-2.0 name: CodeQL Advanced on: push: branches: [ main, v3, v2, v1 ] pull_request: branches: [ main, v3, v2, v1 ] schedule: - cron: 17 13 * * 3 permissions: read-all jobs: analyze: name: Analyze (${{ matrix.language }}) runs-on: ubuntu-latest permissions: # required for all workflows security-events: write strategy: fail-fast: false matrix: include: - language: actions build-mode: none - language: go build-mode: autobuild steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL # yamllint disable-line rule:line-length uses: github/codeql-action/init@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} - name: Perform CodeQL Analysis # yamllint disable-line rule:line-length uses: github/codeql-action/analyze@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4 with: category: /language:${{matrix.language}} golang-go.yaml-yaml-v4-4.0.0~rc5/.github/workflows/files.yaml000066400000000000000000000030721521353103300240060ustar00rootroot00000000000000# Copyright 2025 The go-yaml Project Contributors # SPDX-License-Identifier: Apache-2.0 name: Files Lint on: push: branches: [ main ] pull_request: branches: [ main ] permissions: read-all jobs: action-lint: runs-on: ubuntu-latest steps: - name: checkout-action uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: actionlint # yamllint disable-line rule:line-length uses: raven-actions/actionlint@205b530c5d9fa8f44ae9ed59f341a0db994aa6f8 # v2.1.2 yamllint: runs-on: ubuntu-latest steps: - name: checkout-action uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: yamllint # yamllint disable-line rule:line-length uses: ibiqlik/action-yamllint@2576378a8e339169678f9939646ee3ee325e845c # v3.1.1 ls-lint: runs-on: ubuntu-latest steps: - name: checkout-action uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: ls-lint uses: ls-lint/action@02e380fe8733d499cbfc9e22276de5085508a5bd # v2.3.1 with: config: .ls-lint.yaml typos: runs-on: ubuntu-latest steps: - name: checkout-action uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: typos-action uses: crate-ci/typos@5374cbf686e897b15713110e233094e2874de7ef # v1.46.1 golang-go.yaml-yaml-v4-4.0.0~rc5/.github/workflows/forbid-merge-commits.yaml000066400000000000000000000010201521353103300267060ustar00rootroot00000000000000# Copyright 2025 The go-yaml Project Contributors # SPDX-License-Identifier: Apache-2.0 name: Forbid Merge Commits on: pull_request: types: [opened, synchronize, reopened, ready_for_review] permissions: read-all jobs: forbid-merge-commits: runs-on: ubuntu-latest steps: - name: Run Forbid Merge Commits Action # yamllint disable-line rule:line-length uses: motlin/forbid-merge-commits-action@d508a7b3148cc98b51a5f062d60396f09f5bc269 # v1.0.7 with: fail-on-merge-commits: true golang-go.yaml-yaml-v4-4.0.0~rc5/.github/workflows/go.yaml000066400000000000000000000047321521353103300233150ustar00rootroot00000000000000# Copyright 2025 The go-yaml Project Contributors # SPDX-License-Identifier: Apache-2.0 name: Go on: push: branches: [ main, v3, v2, v1 ] pull_request: branches: [ main, v3, v2, v1 ] permissions: read-all jobs: test: runs-on: ubuntu-latest needs: go-versions continue-on-error: true # make sure to run all versions, even if one fails strategy: fail-fast: false matrix: go-version: ${{ fromJSON(needs.go-versions.outputs.versions) }} steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: {persist-credentials: false} - name: Run all unit tests w/ -cover, all YTS tests and go lint run: make test lint v=1 cover=1 GO-VERSION=${{ matrix.go-version }} test-cross-platform: name: Test on ${{ matrix.GOOS }}/${{ matrix.GOARCH }} runs-on: ${{ matrix.os }} continue-on-error: true # make sure to run all platforms, even if one fails strategy: fail-fast: false matrix: include: - os: macos-latest GOOS: darwin GOARCH: amd64 test-args: -race - os: macos-latest GOOS: darwin GOARCH: arm64 test-args: -race - os: windows-latest GOOS: windows GOARCH: amd64 test-args: -race - os: ubuntu-latest GOOS: linux GOARCH: 386 test-args: '' # race is not available on 32 bits architecture - os: windows-latest GOOS: windows GOARCH: 386 test-args: '' # race is not available on 32 bits architecture env: GOARCH: ${{ matrix.GOARCH }} GOOS: ${{ matrix.GOOS }} steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: {persist-credentials: false} - name: Run tests run: | bash -c 'echo + $OSTYPE + $MACHTYPE +' make test-main test-internal v=1 o=${{ matrix.test-args }} go-versions: name: Lookup Go versions runs-on: ubuntu-latest outputs: versions: ${{ steps.latest-patch-versions.outputs.matrix }} steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Get latest patch for each minor version # yamllint disable-line rule:line-length uses: arnested/go-version-action@6343454db06c63467680c5dc1f4f382fe9fe6303 # v2.1.2 id: latest-patch-versions with: latest-patches-only: true patch-level: true golang-go.yaml-yaml-v4-4.0.0~rc5/.gitignore000066400000000000000000000002441521353103300204110ustar00rootroot00000000000000# Copyright 2025 The go-yaml Project Contributors # SPDX-License-Identifier: Apache-2.0 /.cache/ /.claude/ /note/ /worktree/ /yts/testdata/ /go-yaml /*.md /*.yaml golang-go.yaml-yaml-v4-4.0.0~rc5/.golangci.yaml000066400000000000000000000020471521353103300211510ustar00rootroot00000000000000# Copyright 2025 The go-yaml Project Contributors # SPDX-License-Identifier: Apache-2.0 version: '2' linters: enable: - dupword - godoclint - govet - mirror - misspell - modernize - nilnesserr - nolintlint - staticcheck - thelper - unconvert disable: - errcheck - ineffassign - unused settings: dupword: ignore: - 'NULL' - DOCUMENT-START - BLOCK-END godoclint: default: none enable: - require-stdlib-doclink govet: enable-all: true disable: - fieldalignment - shadow misspell: locale: US nolintlint: allow-unused: false require-specific: true require-explanation: true staticcheck: checks: # enable all rules - all # disable some of them - -QF1001 # De Morgan's laws is too opinionated. - -ST1003 # The codebase has too many underscores in identifiers for now. formatters: enable: - gofumpt issues: max-issues-per-linter: 0 max-same-issues: 0 uniq-by-line: false golang-go.yaml-yaml-v4-4.0.0~rc5/.ls-lint.yaml000066400000000000000000000010171521353103300207440ustar00rootroot00000000000000# Copyright 2025 The go-yaml Project Contributors # SPDX-License-Identifier: Apache-2.0 # The ls-lint configuration file. # More information on the file format can be found on https://ls-lint.org/ ls: # root directory .yml: exists:0 # .yml files .*.yml: exists:0 # .yml dotfiles # any subdirectory, even dotfile directory '**': .yml: exists:0 # .yml files .*.yml: exists:0 # .yml dotfiles ignore: - .git # git folder - .cache # cache folder - yts/testdata # third-party folder golang-go.yaml-yaml-v4-4.0.0~rc5/.typos.toml000066400000000000000000000022061521353103300205520ustar00rootroot00000000000000# Copyright 2025 The go-yaml Project Contributors # SPDX-License-Identifier: Apache-2.0 # This is the configuration file of typos (spell checker) # https://github.com/crate-ci/typos [files] # excluded file extend-exclude = [ "yts/testdata", # third-party test data ] # setting for Go files configuration [type.go] extend-ignore-re = [ 'ba-dum-tss\W+', # this one can be found in test files '"yYnNtTfFoO', # this one can be found in test files 'ba\?r', # this one can be found in test files ] [type.go.extend-words] # Here is a list of words we want to ignore in Go files typ = "typ" # commonly used abbreviation for "type" in Go as "type" is a reserved identifier # setting for YAML files configuration [type.yaml] extend-ignore-re = [ 'ba\?r', # this one can be found in test files ] [default.extend-words] caf = "caf" # part of "café" shown as "caf\u00e9" in Unicode escape examples deprecat = "deprecat" # Used as part of a command in a docs/ file Desolver = "Desolver" # Type name: inverse of Resolver (removes tags) Desolve = "Desolve" # Function name: inverse of Resolve desolve = "desolve" # Function name variants golang-go.yaml-yaml-v4-4.0.0~rc5/.yamllint.yaml000066400000000000000000000012261521353103300212150ustar00rootroot00000000000000# Copyright 2025 The go-yaml Project Contributors # SPDX-License-Identifier: Apache-2.0 extends: default ignore: /**/testdata/ # ignore testdata files that are not relevant for linting rules: brackets: min-spaces-inside: 0 max-spaces-inside: 1 # allow us to use space in brackets document-start: present: false indentation: spaces: 2 indent-sequences: false # make sure there is no indentation for sequences truthy: check-keys: false # there is a problem with the "on" key in GitHub Actions quoted-strings: check-keys: true # by default, only values are checked quote-type: single required: only-when-needed golang-go.yaml-yaml-v4-4.0.0~rc5/CONTRIBUTING.md000066400000000000000000000141621521353103300206560ustar00rootroot00000000000000Contributing to go-yaml ======================= Thank you for your interest in contributing to go-yaml! This document provides guidelines and instructions for contributing to this project. ## Code of Conduct By participating in this project, you agree to follow our Code of Conduct. We expect all contributors to: - Be respectful and inclusive - Use welcoming and inclusive language - Be collaborative and constructive - Focus on what is best for both the Go and YAML communities ## How to Contribute ### Reporting Issues Before submitting an issue, please: - Check if the issue already exists in our issue tracker - Use a clear and descriptive title - Provide detailed steps to reproduce the issue - Include relevant code samples and error messages - Specify your Go version and operating system - Use the `go-yaml` CLI tool described below ### Using the `go-yaml` CLI Tool This tool can be used to inspect both the internal stages and final results of YAML processing with the go-yaml library. It should be used when reporting most bugs. The `go-yaml` CLI tool uses the `go.yaml.in/yaml/v4` library to decode and encode YAML. Decoding YAML is a multi-stage process that involves tokens, events, and nodes. The `go-yaml` CLI tool lets you see all of these intermediate stages of the decoding process. This is crucial for understanding what go-yaml is doing internally. The `go-yaml` CLI tool can be built with the `make go-yaml` command or installed with the `go install go.yaml.in/yaml/v4/cmd/go-yaml@latest` command. You can learn about all of its options with the `go-yaml -h` command. Here is an example of using it on a small piece of YAML: ```bash ./go-yaml -t <<< ' foo: &a1 bar *a1: baz ``` ### Coding Conventions - Follow standard Go coding conventions - Use `make fmt` to format your code - Write descriptive comments for non-obvious code - Add tests for your work - Keep line length to 80 characters - Use meaningful variable and function names - Start doc and comment sentences on a new line - Test your changes with the `go-yaml` CLI tool when working on parsing logic ### Commit Conventions - No merge commits - Commit subject line should: - Start with a capital letter - Not end with a period - Be no more than 50 characters ### Pull Requests 1. Fork the repository 1. Create a new branch for your changes 1. Make your changes following our coding conventions - If you are not sure about the coding conventions, please ask - Look at the existing code for examples 1. Write clear commit messages 1. Update tests and documentation 1. Submit a pull request ### Testing - Ensure all tests pass with `make test` - Add new tests for new functionality - Update existing tests when modifying functionality ## Development Process - This project makes use of a GNU makefile (`GNUmakefile`) for many dev tasks - The makefile doesn't use your locally installed Go commands; it auto-installs them, so that all results are deterministic - Fork and clone the repository - Make your changes - Run tests, linters and formatters - `make fmt` - `make tidy` - `make lint` - `make test` - You can use `make check` to run all of the above - Submit a [Pull Request](https://github.com/yaml/go-yaml/pulls) ### Using Your Own Go with the Makefile We ask that you always test with the makefile installed Go before committing, since it is deterministic and uses the exact same flow as the go-yaml CI. We also realize that many Go devs need to run their locally installed Go commands for their development environment, and might want to use them with the go-yaml makefile. If you need to use your own Go utils with the makefile, set `GO_YAML_PATH` to the directory(s) containing them (either by exporting it or passing it to `make`). Something like this: ```bash export GO_YAML_PATH=$(dirname "$(command -v go)") make test # or make test GO_YAML_PATH=$(dirname "$(command -v go)") ``` **Note:** `GO-VERSION` and `GO_YAML_PATH` are mutually exclusive. When `GO_YAML_PATH` is set, the makefile uses your own Go environment and ignores any `GO-VERSION` setting. ### Using the Makefile Environment as a Shell Sometimes you might want to run your own shell commands using the same binaries that the makefile installs. To get a subshell with this environment, run one of: ```bash make shell make bash make zsh make shell GO-VERSION=1.23.4 ``` ## Makefile Targets The repository's makefile (`GNUmakefile`) provides a number of useful targets: - `make test` runs all tests including yaml-test-suite tests - `make test-unit` runs just the unit tests - `make test-internal` runs just the internal tests - `make test-yts` runs just the yaml-test-suite tests - `make test v=1 count=3` runs the tests with options - `make test GO-VERSION=1.23.4` runs the tests with a specific Go version - `make test GO_YAML_PATH=/path/to/go/bin` uses your own Go installation - `make shell` opens a shell with the project's dependencies set up - `make shell GO-VERSION=1.23.4` opens a shell with a specific Go version - `make fmt` runs `golangci-lint fmt ./...` - `make lint` runs `golangci-lint run` - `make tidy` runs `go mod tidy` - `make distclean` cleans the project completely ## Getting Help If you need help, you can: - Open an [issue](https://github.com/yaml/go-yaml/issues) with your question - Start a [discussion](https://github.com/yaml/go-yaml/discussions) - Read through our [documentation](https://pkg.go.dev/go.yaml.in/yaml/v4) - Check the [migration guide](docs/v3-to-v4-migration.md) if upgrading from v3 - Join our [Slack channel](https://cloud-native.slack.com/archives/C08PPAT8PS7) ## We are a Work in Progress This project is very much a team effort. We are just getting things rolling and trying to get the foundations in place. There are lots of opinions and ideas about how to do things, even within the core team. Once our process is more mature, we will likely change the rules here. We'll make the new rules as a team. For now, please stick to the rules as they are. This project is focused on serving the needs of both the Go and YAML communities. Sometimes those needs can be in conflict, but we'll try to find common ground. ## Thank You Thank you for contributing to go-yaml! golang-go.yaml-yaml-v4-4.0.0~rc5/GNUmakefile000066400000000000000000000106021521353103300204720ustar00rootroot00000000000000# Copyright 2025 The go-yaml Project Contributors # SPDX-License-Identifier: Apache-2.0 # Auto-install https://github.com/makeplus/makes at specific commit: MAKES := .cache/makes MAKES-LOCAL := .cache/local MAKES-COMMIT ?= 4e48a743c3652b88adc4a257398d895a801e6d11 $(shell [ -d $(MAKES) ] || ( \ git clone -q https://github.com/makeplus/makes $(MAKES) && \ git -C $(MAKES) reset -q --hard $(MAKES-COMMIT))) ifneq ($(shell git -C $(MAKES) rev-parse HEAD), \ $(shell git -C $(MAKES) rev-parse $(MAKES-COMMIT))) $(error $(MAKES) is not at the correct commit: $(MAKES-COMMIT). \ Remove $(MAKES) and try again.) endif include $(MAKES)/init.mk include $(MAKES)/shellcheck.mk # Auto-install go unless GO_YAML_PATH is set: ifdef GO_YAML_PATH override export PATH := $(GO_YAML_PATH):$(PATH) else GO-VERSION ?= 1.25.5 endif GO-VERSION-NEEDED := $(GO-VERSION) # yaml-test-suite info: YTS-URL ?= https://github.com/yaml/yaml-test-suite YTS-TAG ?= data-2022-01-17 YTS-DIR := yts/testdata/$(YTS-TAG) CLI-BINARY := go-yaml # Pager for viewing documentation: PAGER ?= less -FRX # Setup and include go.mk and shell.mk: # We need to limit `find` to avoid dirs like `.cache/` and any git worktrees, # as this makes `make` operations very slow: REPO-DIRS := $(shell find * -maxdepth 0 -type d \ ! -exec test -f {}/.git \; -print) GO-FILES := $(shell find $(REPO-DIRS) -name '*.go') ifndef GO-VERSION-NEEDED GO-NO-DEP-GO := true endif include $(MAKES)/go.mk # Set this from the `make` command to override: GOLANGCI-LINT-VERSION ?= v2.8.0 GOLANGCI-LINT-INSTALLER := \ https://github.com/golangci/golangci-lint/raw/main/install.sh GOLANGCI-LINT := $(LOCAL-BIN)/golangci-lint GOLANGCI-LINT-VERSIONED := $(GOLANGCI-LINT)-$(GOLANGCI-LINT-VERSION) SHELL-DEPS += $(GOLANGCI-LINT-VERSIONED) ifdef GO-VERSION-NEEDED GO-DEPS += $(GO) else SHELL-DEPS := $(filter-out $(GO),$(SHELL-DEPS)) endif SHELL-NAME := makes go-yaml include $(MAKES)/clean.mk include $(MAKES)/shell.mk MAKES-CLEAN := $(CLI-BINARY) $(GOLANGCI-LINT) MAKES-REALCLEAN := $(dir $(YTS-DIR)) SHELL-SCRIPTS = \ util/common.bash \ $(shell grep -rl '^.!/usr/bin/env bash' util | \ grep -v '\.sw') COVER-TESTS := \ . \ ./cmd/... \ ./internal/... \ # v=1 for verbose MAKE := $(MAKE) --no-print-directory v ?= cover ?= fuzz ?= time ?= 60s opts ?= TEST-OPTS := \ $(if $v, -v)\ $(if $(cover), --cover)\ $(if $(fuzz), --fuzz=FuzzEncodeFromJSON --fuzztime=$(time))\ $(if $(opts), $(opts))\ # Test rules: test: test-main test-internal test-cmd test-yts-all test-shell @echo 'ALL TESTS PASS' check: $(MAKE) fmt $(MAKE) tidy $(MAKE) lint $(MAKE) test test-main: $(GO-DEPS) go test .$(TEST-OPTS) @echo 'ALL MAIN FILES PASS' test-cmd: $(GO-DEPS) go test ./cmd/...$(TEST-OPTS) @echo 'ALL CMD FILES PASS' test-internal: $(GO-DEPS) go test ./internal/...$(TEST-OPTS) @echo 'ALL INTERNAL FILES PASS' test-cover: $(GO-DEPS) go test . $(COVER-TESTS) -vet=off --cover$(TEST-OPTS) test-yts: $(GO-DEPS) $(YTS-DIR) go test ./yts$(TEST-OPTS) test-yts-all: $(GO-DEPS) $(YTS-DIR) @echo 'Testing yaml-test-suite' util/yaml-test-suite all test-yts-fail: $(GO-DEPS) $(YTS-DIR) @echo 'Testing yaml-test-suite failures' util/yaml-test-suite fail test-shell: $(SHELLCHECK) shellcheck $(SHELL-SCRIPTS) @echo 'ALL SHELL FILES PASS' test-count: $(GO-DEPS) util/test-count yts-dir: $(YTS-DIR) get-test-data: $(YTS-DIR) # Install golangci-lint for GitHub Actions: golangci-lint-install: $(GOLANGCI-LINT) fmt: $(GOLANGCI-LINT-VERSIONED) $< fmt ./... lint: $(GOLANGCI-LINT-VERSIONED) $< run ./... tidy: $(GO-DEPS) go mod tidy cli: $(CLI-BINARY) $(CLI-BINARY): $(GO) go build -o $@ ./cmd/$@ run-examples: $(GO) @for dir in example/*/; do \ (set -x; go run "$${dir}main.go") || \ { echo "$$dir failed"; break; }; \ done # CLI documentation (go doc) - view in terminal: doc: $(GO-DEPS) @go doc -all . | $(PAGER) # HTTP documentation server - opens browser: doc-http: $(GO-DEPS) go doc -http -all # Setup rules: $(YTS-DIR): git clone -q $(YTS-URL) $@ git -C $@ checkout -q $(YTS-TAG) # Downloads golangci-lint binary and moves to versioned path # (.cache/local/bin/golangci-lint-). $(GOLANGCI-LINT-VERSIONED): $(GO-DEPS) curl -sSfL $(GOLANGCI-LINT-INSTALLER) | \ bash -s -- -b $(LOCAL-BIN) $(GOLANGCI-LINT-VERSION) mv $(GOLANGCI-LINT) $@ # Moves golangci-lint- to golangci-lint for CI requirement $(GOLANGCI-LINT): $(GOLANGCI-LINT-VERSIONED) cp $< $@ golang-go.yaml-yaml-v4-4.0.0~rc5/LICENSE000066400000000000000000000261451521353103300174360ustar00rootroot00000000000000 Apache 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 2025 - The go-yaml Project Contributors 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. golang-go.yaml-yaml-v4-4.0.0~rc5/NOTICE000066400000000000000000000012761521353103300173330ustar00rootroot00000000000000The following files were ported to Go from C files of libyaml, and thus are still covered by their original MIT license, with the additional copyright starting in 2011 when the project was ported over: - internal/libyaml/api.go - internal/libyaml/emitter.go - internal/libyaml/parser.go - internal/libyaml/reader.go - internal/libyaml/scanner.go - internal/libyaml/writer.go - internal/libyaml/yaml.go - internal/libyaml/yamlprivate.go Copyright 2006-2010 Kirill Simonov https://opensource.org/license/mit All the remaining project files are covered by the Apache license: Copyright 2011-2019 Canonical Ltd Copyright 2025 The go-yaml Project Contributors http://www.apache.org/licenses/LICENSE-2.0 golang-go.yaml-yaml-v4-4.0.0~rc5/README.md000066400000000000000000000140761521353103300177100ustar00rootroot00000000000000go.yaml.in/yaml =============== YAML Support for the Go Language ## Introduction The `yaml` package enables [Go](https://go.dev/) programs to comfortably encode and decode [YAML](https://yaml.org/) values. It was originally developed within [Canonical](https://www.canonical.com) as part of the [juju](https://juju.ubuntu.com) project, and is based on a pure Go port of the well-known [libyaml](http://pyyaml.org/wiki/LibYAML) C library to parse and generate YAML data quickly and reliably. ## Project Status This project started as a fork of the extremely popular [go-yaml]( https://github.com/go-yaml/yaml/) project, and is being maintained by the official [YAML organization]( https://github.com/yaml/). The YAML team took over ongoing maintenance and development of the project after discussion with go-yaml's author, @niemeyer, following his decision to [label the project repository as "unmaintained"]( https://github.com/go-yaml/yaml/blob/944c86a7d2/README.md) in April 2025. We have put together a team of dedicated maintainers including representatives of go-yaml's most important downstream projects. We will strive to earn the trust of the various go-yaml forks to switch back to this repository as their upstream. Please [contact us](https://cloud-native.slack.com/archives/C08PPAT8PS7) if you would like to contribute or be involved. ### Version Intentions Versions `v1`, `v2`, and `v3` will remain as **frozen legacy**. They will receive **security-fixes only** so that existing consumers keep working without breaking changes. All ongoing work, including new features and routine bug-fixes, will happen in **`v4`**. If you’re starting a new project or upgrading an existing one, please use the `go.yaml.in/yaml/v4` import path. ## Compatibility The `yaml` package supports most of YAML 1.2, but preserves some behavior from 1.1 for backwards compatibility. Specifically, v3 of the `yaml` package: * Supports YAML 1.1 bools (`yes`/`no`, `on`/`off`) as long as they are being decoded into a typed bool value. Otherwise they behave as a string. Booleans in YAML 1.2 are `true`/`false` only. * Supports octals encoded and decoded as `0777` per YAML 1.1, rather than `0o777` as specified in YAML 1.2, because most parsers still use the old format. Octals in the `0o777` format are supported though, so new files work. * Does not support base-60 floats. These are gone from YAML 1.2, and were actually never supported by this package as it's clearly a poor choice. ## Installation and Usage The import path for the package is *go.yaml.in/yaml/v4*. To install it, run: ```bash go get go.yaml.in/yaml/v4 ``` ## API Documentation See: ## API Stability The package API for yaml v3 will remain stable as described in [gopkg.in]( https://gopkg.in). ## Example ```go package main import ( "fmt" "log" "go.yaml.in/yaml/v4" ) var data = ` a: Easy! b: c: 2 d: [3, 4] ` // Note: struct fields must be public in order for unmarshal to // correctly populate the data. type T struct { A string B struct { RenamedC int `yaml:"c"` D []int `yaml:",flow"` } } func main() { t := T{} err := yaml.Unmarshal([]byte(data), &t) if err != nil { log.Fatalf("error: %v", err) } fmt.Printf("--- t:\n%v\n\n", t) d, err := yaml.Marshal(&t) if err != nil { log.Fatalf("error: %v", err) } fmt.Printf("--- t dump:\n%s\n\n", string(d)) m := make(map[any]any) err = yaml.Unmarshal([]byte(data), &m) if err != nil { log.Fatalf("error: %v", err) } fmt.Printf("--- m:\n%v\n\n", m) d, err = yaml.Marshal(&m) if err != nil { log.Fatalf("error: %v", err) } fmt.Printf("--- m dump:\n%s\n\n", string(d)) } ``` This example will generate the following output: ``` --- t: {Easy! {2 [3 4]}} --- t dump: a: Easy! b: c: 2 d: [3, 4] --- m: map[a:Easy! b:map[c:2 d:[3 4]]] --- m dump: a: Easy! b: c: 2 d: - 3 - 4 ``` ## Development and Testing with `make` This project's makefile (`GNUmakefile`) is set up to support all of the project's testing, automation and development tasks in a completely deterministic way. Some `make` commands are: * `make test` * `make lint tidy` * `make test-shell` * `make test v=1` * `make test o='-foo --bar=baz'` # Add extra CLI options * `make test GO-VERSION=1.2.34` * `make test GO_YAML_PATH=/usr/local/go/bin` * `make shell` # Start a shell with the local `go` environment * `make shell GO-VERSION=1.2.34` * `make distclean` # Remove all generated files including `.cache/` ### Dependency Auto-install By default, this makefile will not use your system's Go installation, or any other system tools that it needs. The only things from your system that it relies on are: * Linux or macOS * GNU `make` (3.81+) * `git` * `bash` * `curl` Everything else, including Go and Go utils, are installed and cached as they are needed by the makefile (under `.cache/`). > **Note**: Use `make shell` to get a subshell with the same environment that > the makefile set up for its commands. ### Using your own Go If you want to use your own Go installation and utils, export `GO_YAML_PATH` to the directory containing the `go` binary. Use something like this: ``` export GO_YAML_PATH=$(dirname "$(command -v go)") make # or: make GO_YAML_PATH=$(dirname "$(command -v go)") ``` > **Note:** `GO-VERSION` and `GO_YAML_PATH` are mutually exclusive. > When `GO_YAML_PATH` is set, the Makefile uses your own Go installation and > ignores any `GO-VERSION` setting. ## The `go-yaml` CLI Tool This repository includes a `go-yaml` CLI tool which can be used to understand the internal stages and final results of YAML processing with the go-yaml library. We strongly encourage you to show pertinent output from this command when reporting and discussing issues. ```bash make go-yaml ./go-yaml --help ./go-yaml <<< ' foo: &a1 bar *a1: baz ' -n # Show value on decoded Node structs (formatted in YAML) ``` You can also install it with: ```bash go install go.yaml.in/yaml/v4/cmd/go-yaml@latest ``` ## License The yaml package is licensed under the MIT and Apache License 2.0 licenses. Please see the LICENSE file for details. golang-go.yaml-yaml-v4-4.0.0~rc5/cmd/000077500000000000000000000000001521353103300171645ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc5/cmd/go-yaml/000077500000000000000000000000001521353103300205315ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc5/cmd/go-yaml/README.md000066400000000000000000000027731521353103300220210ustar00rootroot00000000000000# go-yaml The `go-yaml` binary is a YAML node inspection tool that provides various modes for analyzing and transforming YAML data. Below is a summary of its capabilities: ## License The `go-yaml` project is licensed under the Apache License 2.0. See the [LICENSE](LICENSE) file for more details. ## Features ### YAML Parsing and Encoding - `-y` / `--yaml`: Outputs YAML in a compact format. - `-Y` / `--YAML`: Outputs YAML while preserving styles and comments. ### JSON Conversion - `-j` / `--json`: Outputs JSON in a compact format. - `-J` / `--JSON`: Outputs JSON in a pretty-printed format. ### Token Inspection - `-t` / `--token`: Outputs tokens from the YAML input. - `-T` / `--TOKEN`: Outputs tokens with line information. ### Event Inspection - `-e` / `--event`: Outputs events from the YAML input. - `-E` / `--EVENT`: Outputs events with line information. ### Node Representation - `-n` / `--node`: Outputs a detailed representation of the YAML node structure. ### Formatting Options - `-l` / `--long`: Enables long (block) formatted output. ### Processing Modes - `-u` / `--unmarshal`: Uses `Unmarshal` instead of `Decode` for YAML input. - `-m` / `--marshal`: Uses `Marshal` instead of `Encode` for YAML output. ### Help and Version - `-h` / `--help`: Displays help information. - `--version`: Displays the version of the tool. ## Usage The tool reads YAML data from `stdin` and processes it based on the specified flags. It validates flag combinations and provides error messages for incompatible options.golang-go.yaml-yaml-v4-4.0.0~rc5/cmd/go-yaml/cli_test.go000066400000000000000000000234041521353103300226710ustar00rootroot00000000000000package main import ( "bytes" "fmt" "os" "os/exec" "path/filepath" "strings" "testing" "go.yaml.in/yaml/v4" ) // testBinary holds the path to the pre-built CLI binary for testing var testBinary string // TestMain builds the CLI binary once before running tests func TestMain(m *testing.M) { // Create a temporary directory for the test binary tmpDir, err := os.MkdirTemp("", "go-yaml-test") if err != nil { fmt.Fprintf(os.Stderr, "Failed to create temp dir: %v\n", err) os.Exit(1) } // Build the binary once testBinary = filepath.Join(tmpDir, "go-yaml") cmd := exec.Command("go", "build", "-o", testBinary, ".") cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { fmt.Fprintf(os.Stderr, "Failed to build test binary: %v\n", err) os.RemoveAll(tmpDir) os.Exit(1) } // Run tests code := m.Run() // Cleanup before exit (defer won't run with os.Exit) os.RemoveAll(tmpDir) os.Exit(code) } // TestCase represents a single test case from a test file type TestCase struct { Name string `yaml:"name"` Text string `yaml:"text"` Token string `yaml:"token,omitempty"` TOKEN string `yaml:"TOKEN,omitempty"` Event string `yaml:"event,omitempty"` EVENT string `yaml:"EVENT,omitempty"` Node string `yaml:"node,omitempty"` NODE string `yaml:"NODE,omitempty"` Yaml string `yaml:"yaml,omitempty"` YAML string `yaml:"YAML,omitempty"` Json string `yaml:"json,omitempty"` JSON string `yaml:"JSON,omitempty"` // Option flags V2Yaml string `yaml:"v2-yaml,omitempty"` V3Yaml string `yaml:"v3-yaml,omitempty"` Indent3Yaml string `yaml:"indent3-yaml,omitempty"` Width40Yaml string `yaml:"width40-yaml,omitempty"` CompactYaml string `yaml:"compact-yaml,omitempty"` V3Indent2 string `yaml:"v3-indent2-yaml,omitempty"` CanonicalYaml string `yaml:"canonical-yaml,omitempty"` ExplicitYaml string `yaml:"explicit-yaml,omitempty"` ExplicitStartYaml string `yaml:"explicit-start-yaml,omitempty"` NoUnicodeYaml string `yaml:"no-unicode-yaml,omitempty"` CanonicalExplicitYaml string `yaml:"canonical-explicit-yaml,omitempty"` V2Indent4Yaml string `yaml:"v2-indent4-yaml,omitempty"` // Stream option flags StreamNode string `yaml:"stream-node,omitempty"` StreamNODE string `yaml:"stream-NODE,omitempty"` } // TestSuite is a sequence of test cases type TestSuite []TestCase // flagMapping maps test file field names to CLI flags var flagMapping = map[string]string{ "token": "-t", "TOKEN": "-T", "event": "-e", "EVENT": "-E", "node": "-n", "NODE": "-N", "yaml": "-y", "YAML": "-Y", "json": "-j", "JSON": "-J", "v2-yaml": "-o v2 -y", "v3-yaml": "-o v3 -y", "indent3-yaml": "-o indent=3 -y", "width40-yaml": "-o width=40 -y", "compact-yaml": "-o compact -y", "v3-indent2-yaml": "-o v3,indent=2 -y", "canonical-yaml": "-o canonical -y", "explicit-yaml": "-o explicit -y", "explicit-start-yaml": "-o explicit-start -y", "no-unicode-yaml": "-o no-unicode -y", "canonical-explicit-yaml": "-o canonical,explicit -y", "v2-indent4-yaml": "-o v2,indent=4 -y", "stream-node": "-o stream -n", "stream-NODE": "-o stream -N", } func TestCLI(t *testing.T) { // Find all test files in testdata/ testFiles, err := filepath.Glob(filepath.Join("testdata", "*.yaml")) if err != nil { t.Fatalf("Failed to find test files: %v", err) } if len(testFiles) == 0 { t.Fatal("No test files found in testdata/") } // Process each test file for _, testFile := range testFiles { testFileName := filepath.Base(testFile) t.Run(testFileName, func(t *testing.T) { runTestFile(t, testFile) }) } } func runTestFile(t *testing.T, testFile string) { t.Helper() // Read and parse the test file data, err := os.ReadFile(testFile) if err != nil { t.Fatalf("Failed to read test file %s: %v", testFile, err) } var suite TestSuite if err := yaml.Load(data, &suite); err != nil { t.Fatalf("Failed to parse test file %s: %v", testFile, err) } // Run each test case for _, testCase := range suite { t.Run(testCase.Name, func(t *testing.T) { runTestCase(t, testCase) }) } } func runTestCase(t *testing.T, tc TestCase) { t.Helper() // Test each output format that has an expected value tests := []struct { field string flag string expected string }{ {"token", flagMapping["token"], tc.Token}, {"TOKEN", flagMapping["TOKEN"], tc.TOKEN}, {"event", flagMapping["event"], tc.Event}, {"EVENT", flagMapping["EVENT"], tc.EVENT}, {"node", flagMapping["node"], tc.Node}, {"NODE", flagMapping["NODE"], tc.NODE}, {"yaml", flagMapping["yaml"], tc.Yaml}, {"YAML", flagMapping["YAML"], tc.YAML}, {"json", flagMapping["json"], tc.Json}, {"JSON", flagMapping["JSON"], tc.JSON}, {"v2-yaml", flagMapping["v2-yaml"], tc.V2Yaml}, {"v3-yaml", flagMapping["v3-yaml"], tc.V3Yaml}, {"indent3-yaml", flagMapping["indent3-yaml"], tc.Indent3Yaml}, {"width40-yaml", flagMapping["width40-yaml"], tc.Width40Yaml}, {"compact-yaml", flagMapping["compact-yaml"], tc.CompactYaml}, {"v3-indent2-yaml", flagMapping["v3-indent2-yaml"], tc.V3Indent2}, {"canonical-yaml", flagMapping["canonical-yaml"], tc.CanonicalYaml}, {"explicit-yaml", flagMapping["explicit-yaml"], tc.ExplicitYaml}, {"explicit-start-yaml", flagMapping["explicit-start-yaml"], tc.ExplicitStartYaml}, {"no-unicode-yaml", flagMapping["no-unicode-yaml"], tc.NoUnicodeYaml}, {"canonical-explicit-yaml", flagMapping["canonical-explicit-yaml"], tc.CanonicalExplicitYaml}, {"v2-indent4-yaml", flagMapping["v2-indent4-yaml"], tc.V2Indent4Yaml}, {"stream-node", flagMapping["stream-node"], tc.StreamNode}, {"stream-NODE", flagMapping["stream-NODE"], tc.StreamNODE}, } for _, test := range tests { if test.expected == "" { continue // Skip if no expected output for this format } t.Run(test.field, func(t *testing.T) { // Run the pre-built CLI binary args := strings.Fields(test.flag) cmd := exec.Command(testBinary, args...) cmd.Stdin = strings.NewReader(tc.Text) var stdout, stderr bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = &stderr if err := cmd.Run(); err != nil { t.Fatalf("Command failed: %v\nStderr: %s", err, stderr.String()) } // Normalize output for comparison actual := normalizeOutput(stdout.String()) expected := normalizeOutput(test.expected) if actual != expected { t.Errorf("Output mismatch for flag %s\nExpected:\n%s\n\nActual:\n%s\n\nDiff:\n%s", test.flag, expected, actual, diff(expected, actual)) } }) } } // normalizeOutput trims whitespace and ensures consistent line endings func normalizeOutput(s string) string { s = strings.TrimSpace(s) s = strings.ReplaceAll(s, "\r\n", "\n") return s } // CmdTestCase represents a command-based test case type CmdTestCase struct { Name string `yaml:"name"` Cmd string `yaml:"cmd"` Out string `yaml:"out"` } // CmdTestSuite is a sequence of command test cases type CmdTestSuite []CmdTestCase // TestCLICommands runs command-based tests from testdata/cmd/*.yaml func TestCLICommands(t *testing.T) { // Find all test files in testdata/cmd/ testFiles, err := filepath.Glob(filepath.Join("testdata", "cmd", "*.yaml")) if err != nil { t.Fatalf("Failed to find test files: %v", err) } if len(testFiles) == 0 { t.Fatal("No command test files found in testdata/cmd/") } // Process each test file for _, testFile := range testFiles { testFileName := filepath.Base(testFile) t.Run(testFileName, func(t *testing.T) { runCmdTestFile(t, testFile) }) } } func runCmdTestFile(t *testing.T, testFile string) { t.Helper() // Read and parse the test file data, err := os.ReadFile(testFile) if err != nil { t.Fatalf("Failed to read test file %s: %v", testFile, err) } var suite CmdTestSuite if err := yaml.Load(data, &suite); err != nil { t.Fatalf("Failed to parse test file %s: %v", testFile, err) } // Run each test case for _, tc := range suite { t.Run(tc.Name, func(t *testing.T) { runCmdTestCase(t, tc) }) } } func runCmdTestCase(t *testing.T, tc CmdTestCase) { t.Helper() // Replace "go-yaml" with actual test binary path in the command cmdStr := strings.Replace(tc.Cmd, "go-yaml", testBinary, 1) // Run the command through bash to handle pipes, heredocs, etc. cmd := exec.Command("bash", "-c", cmdStr) var stdout, stderr bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = &stderr if err := cmd.Run(); err != nil { t.Fatalf("Command failed: %v\nCommand: %s\nStderr: %s", err, tc.Cmd, stderr.String()) } // Normalize output for comparison actual := normalizeOutput(stdout.String()) expected := normalizeOutput(tc.Out) if actual != expected { t.Errorf("Output mismatch\nCommand: %s\nExpected:\n%s\n\nActual:\n%s\n\nDiff:\n%s", tc.Cmd, expected, actual, diff(expected, actual)) } } // diff provides a simple diff output for debugging func diff(expected, actual string) string { expLines := strings.Split(expected, "\n") actLines := strings.Split(actual, "\n") maxLines := len(expLines) if len(actLines) > maxLines { maxLines = len(actLines) } var result strings.Builder for i := 0; i < maxLines; i++ { expLine := "" actLine := "" if i < len(expLines) { expLine = expLines[i] } if i < len(actLines) { actLine = actLines[i] } if expLine != actLine { fmt.Fprintf(&result, "Line %d:\n", i+1) if expLine != "" { result.WriteString("- ") result.WriteString(expLine) result.WriteString("\n") } if actLine != "" { result.WriteString("+ ") result.WriteString(actLine) result.WriteString("\n") } } } return result.String() } golang-go.yaml-yaml-v4-4.0.0~rc5/cmd/go-yaml/event.go000066400000000000000000000334721521353103300222120ustar00rootroot00000000000000// Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Package main provides YAML event formatting utilities for the go-yaml tool. package main import ( "bytes" "fmt" "io" "go.yaml.in/yaml/v4" "go.yaml.in/yaml/v4/internal/libyaml" ) // EventType represents the type of a YAML event type EventType string // Event type constants for CLI output formatting. const ( EventDocumentStart EventType = "DOCUMENT-START" EventDocumentEnd EventType = "DOCUMENT-END" EventScalar EventType = "SCALAR" EventSequenceStart EventType = "SEQUENCE-START" EventSequenceEnd EventType = "SEQUENCE-END" EventMappingStart EventType = "MAPPING-START" EventMappingEnd EventType = "MAPPING-END" ) // Event represents a YAML event type Event struct { Type EventType Value string Anchor string Tag string Style string Implicit bool StartLine int StartColumn int EndLine int EndColumn int HeadComment string LineComment string FootComment string } // EventInfo represents the information about a YAML event for YAML encoding type EventInfo struct { Event string `yaml:"event"` Value string `yaml:"value,omitempty"` Style string `yaml:"style,omitempty"` Tag string `yaml:"tag,omitempty"` Anchor string `yaml:"anchor,omitempty"` Implicit *bool `yaml:"implicit,omitempty"` Explicit *bool `yaml:"explicit,omitempty"` Head string `yaml:"head,omitempty"` Line string `yaml:"line,omitempty"` Foot string `yaml:"foot,omitempty"` Pos string `yaml:"pos,omitempty"` } // ProcessEvents reads YAML from reader and outputs event information func ProcessEvents(reader io.Reader, profuse, compact, unmarshal bool) error { if unmarshal { return processEventsUnmarshal(reader, profuse, compact) } return processEventsDecode(reader, profuse, compact) } // processEventsDecode uses libyaml.Parser.Parse for YAML processing func processEventsDecode(reader io.Reader, profuse, compact bool) error { // Read all input from reader input, err := io.ReadAll(reader) if err != nil { return fmt.Errorf("failed to read input: %w", err) } // Get events from parser directly events, err := getEventsFromParser(input, profuse) if err != nil { return err } if compact { // For compact mode, output each event as a flow style mapping in a sequence for _, event := range events { info := formatEventInfo(event, profuse) // Create a YAML node with flow style for the mapping compactNode := &yaml.Node{ Kind: yaml.MappingNode, Style: yaml.FlowStyle, } // Add the Event field compactNode.Content = append(compactNode.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: "event"}, &yaml.Node{Kind: yaml.ScalarNode, Value: info.Event}) // Add other fields if they exist if info.Value != "" { compactNode.Content = append(compactNode.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: "value"}, &yaml.Node{Kind: yaml.ScalarNode, Value: info.Value}) } if info.Style != "" { compactNode.Content = append(compactNode.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: "style"}, &yaml.Node{Kind: yaml.ScalarNode, Value: info.Style}) } if info.Tag != "" { compactNode.Content = append(compactNode.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: "tag"}, &yaml.Node{Kind: yaml.ScalarNode, Value: info.Tag}) } if info.Anchor != "" { compactNode.Content = append(compactNode.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: "anchor"}, &yaml.Node{Kind: yaml.ScalarNode, Value: info.Anchor}) } if info.Implicit != nil { compactNode.Content = append(compactNode.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: "implicit"}, &yaml.Node{Kind: yaml.ScalarNode, Value: fmt.Sprintf("%t", *info.Implicit)}) } if info.Explicit != nil { compactNode.Content = append(compactNode.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: "explicit"}, &yaml.Node{Kind: yaml.ScalarNode, Value: fmt.Sprintf("%t", *info.Explicit)}) } if info.Head != "" { compactNode.Content = append(compactNode.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: "head"}, &yaml.Node{Kind: yaml.ScalarNode, Value: info.Head}) } if info.Line != "" { compactNode.Content = append(compactNode.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: "line"}, &yaml.Node{Kind: yaml.ScalarNode, Value: info.Line}) } if info.Foot != "" { compactNode.Content = append(compactNode.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: "foot"}, &yaml.Node{Kind: yaml.ScalarNode, Value: info.Foot}) } if info.Pos != "" { compactNode.Content = append(compactNode.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: "pos"}, &yaml.Node{Kind: yaml.ScalarNode, Value: info.Pos}) } var buf bytes.Buffer enc, err := yaml.NewDumper(&buf) if err != nil { return fmt.Errorf("failed to create dumper: %w", err) } if err := enc.Dump([]*yaml.Node{compactNode}); err != nil { enc.Close() return fmt.Errorf("failed to dump compact event info: %w", err) } if err := enc.Close(); err != nil { return fmt.Errorf("failed to close dumper: %w", err) } fmt.Print(buf.String()) } } else { // For non-compact mode, output each event as a separate mapping for _, event := range events { info := formatEventInfo(event, profuse) var buf bytes.Buffer enc, err := yaml.NewDumper(&buf) if err != nil { return fmt.Errorf("failed to create dumper: %w", err) } if err := enc.Dump([]*EventInfo{info}); err != nil { enc.Close() return fmt.Errorf("failed to dump event info: %w", err) } if err := enc.Close(); err != nil { return fmt.Errorf("failed to close dumper: %w", err) } fmt.Print(buf.String()) } } return nil } // processEventsUnmarshal uses libyaml.Parser.Parse for YAML processing func processEventsUnmarshal(reader io.Reader, profuse, compact bool) error { // Read all input from reader input, err := io.ReadAll(reader) if err != nil { return fmt.Errorf("failed to read input: %w", err) } // Get events from parser directly events, err := getEventsFromParser(input, profuse) if err != nil { return err } if compact { // For compact mode, output each event as a flow style mapping in a sequence for _, event := range events { info := formatEventInfo(event, profuse) // Create a YAML node with flow style for the mapping compactNode := &yaml.Node{ Kind: yaml.MappingNode, Style: yaml.FlowStyle, } // Add the Event field compactNode.Content = append(compactNode.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: "event"}, &yaml.Node{Kind: yaml.ScalarNode, Value: info.Event}) // Add other fields if they exist if info.Value != "" { compactNode.Content = append(compactNode.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: "value"}, &yaml.Node{Kind: yaml.ScalarNode, Value: info.Value}) } if info.Style != "" { compactNode.Content = append(compactNode.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: "style"}, &yaml.Node{Kind: yaml.ScalarNode, Value: info.Style}) } if info.Tag != "" { compactNode.Content = append(compactNode.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: "tag"}, &yaml.Node{Kind: yaml.ScalarNode, Value: info.Tag}) } if info.Anchor != "" { compactNode.Content = append(compactNode.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: "anchor"}, &yaml.Node{Kind: yaml.ScalarNode, Value: info.Anchor}) } if info.Implicit != nil { compactNode.Content = append(compactNode.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: "implicit"}, &yaml.Node{Kind: yaml.ScalarNode, Value: fmt.Sprintf("%t", *info.Implicit)}) } if info.Explicit != nil { compactNode.Content = append(compactNode.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: "explicit"}, &yaml.Node{Kind: yaml.ScalarNode, Value: fmt.Sprintf("%t", *info.Explicit)}) } if info.Head != "" { compactNode.Content = append(compactNode.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: "head"}, &yaml.Node{Kind: yaml.ScalarNode, Value: info.Head}) } if info.Line != "" { compactNode.Content = append(compactNode.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: "line"}, &yaml.Node{Kind: yaml.ScalarNode, Value: info.Line}) } if info.Foot != "" { compactNode.Content = append(compactNode.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: "foot"}, &yaml.Node{Kind: yaml.ScalarNode, Value: info.Foot}) } if info.Pos != "" { compactNode.Content = append(compactNode.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: "pos"}, &yaml.Node{Kind: yaml.ScalarNode, Value: info.Pos}) } var buf bytes.Buffer enc, err := yaml.NewDumper(&buf) if err != nil { return fmt.Errorf("failed to create dumper: %w", err) } if err := enc.Dump([]*yaml.Node{compactNode}); err != nil { enc.Close() return fmt.Errorf("failed to dump compact event info: %w", err) } if err := enc.Close(); err != nil { return fmt.Errorf("failed to close dumper: %w", err) } fmt.Print(buf.String()) } } else { // For non-compact mode, output each event as a separate mapping for _, event := range events { info := formatEventInfo(event, profuse) var buf bytes.Buffer enc, err := yaml.NewDumper(&buf) if err != nil { return fmt.Errorf("failed to create dumper: %w", err) } if err := enc.Dump([]*EventInfo{info}); err != nil { enc.Close() return fmt.Errorf("failed to dump event info: %w", err) } if err := enc.Close(); err != nil { return fmt.Errorf("failed to close dumper: %w", err) } fmt.Print(buf.String()) } } return nil } // formatEventInfo converts an Event to an EventInfo struct for YAML encoding func formatEventInfo(event *Event, profuse bool) *EventInfo { info := &EventInfo{ Event: string(event.Type), } if event.Value != "" { info.Value = event.Value } if event.Style != "" { info.Style = event.Style } if event.Tag != "" { info.Tag = event.Tag } if event.Anchor != "" { info.Anchor = event.Anchor } if event.HeadComment != "" { info.Head = event.HeadComment } if event.LineComment != "" { info.Line = event.LineComment } if event.FootComment != "" { info.Foot = event.FootComment } if profuse { if event.StartLine == event.EndLine && event.StartColumn == event.EndColumn { // Single position info.Pos = fmt.Sprintf("%d:%d", event.StartLine, event.StartColumn) } else if event.StartLine == event.EndLine { // Range on same line info.Pos = fmt.Sprintf("%d:%d-%d", event.StartLine, event.StartColumn, event.EndColumn) } else { // Range across different lines info.Pos = fmt.Sprintf("%d:%d-%d:%d", event.StartLine, event.StartColumn, event.EndLine, event.EndColumn) } } // Handle implicit/explicit for document start/end events if event.Type == "DOCUMENT-START" || event.Type == "DOCUMENT-END" { if profuse { // For -E mode: show implicit: true when implicit if event.Implicit { trueVal := true info.Implicit = &trueVal } } else { // For -e mode: show explicit: true when NOT implicit if !event.Implicit { trueVal := true info.Explicit = &trueVal } } } return info } // getEventsFromParser parses YAML input and extracts events with implicit field information func getEventsFromParser(input []byte, profuse bool) ([]*Event, error) { p := libyaml.NewParser() if len(input) == 0 { input = []byte{'\n'} } p.SetInputString(input) var events []*Event var ev libyaml.Event for { if err := p.Parse(&ev); err != nil { return nil, fmt.Errorf("failed to parse YAML: %w", err) } event := convertLibyamlEvent(&ev, profuse) if event != nil { events = append(events, event) } if ev.Type == libyaml.STREAM_END_EVENT { ev.Delete() break } ev.Delete() } return events, nil } // convertLibyamlEvent converts a libyaml event to our Event struct func convertLibyamlEvent(ev *libyaml.Event, profuse bool) *Event { // Skip stream events if ev.Type == libyaml.STREAM_START_EVENT || ev.Type == libyaml.STREAM_END_EVENT { return nil } event := &Event{ StartLine: ev.StartMark.Line, StartColumn: ev.StartMark.Column, EndLine: ev.EndMark.Line, EndColumn: ev.EndMark.Column, HeadComment: string(ev.HeadComment), LineComment: string(ev.LineComment), FootComment: string(ev.FootComment), } switch ev.Type { case libyaml.DOCUMENT_START_EVENT: event.Type = "DOCUMENT-START" event.Implicit = ev.Implicit case libyaml.DOCUMENT_END_EVENT: event.Type = "DOCUMENT-END" event.Implicit = ev.Implicit case libyaml.MAPPING_START_EVENT: event.Type = "MAPPING-START" event.Anchor = string(ev.Anchor) event.Tag = string(ev.Tag) // Style handling for mapping if ev.MappingStyle() == libyaml.FLOW_MAPPING_STYLE { event.Style = "Flow" } case libyaml.MAPPING_END_EVENT: event.Type = "MAPPING-END" case libyaml.SEQUENCE_START_EVENT: event.Type = "SEQUENCE-START" event.Anchor = string(ev.Anchor) event.Tag = string(ev.Tag) // Style handling for sequence if ev.SequenceStyle() == libyaml.FLOW_SEQUENCE_STYLE { event.Style = "Flow" } case libyaml.SEQUENCE_END_EVENT: event.Type = "SEQUENCE-END" case libyaml.SCALAR_EVENT: event.Type = "SCALAR" event.Value = string(ev.Value) event.Anchor = string(ev.Anchor) event.Tag = string(ev.Tag) event.Implicit = ev.Implicit // Style handling for scalar switch ev.ScalarStyle() { case libyaml.PLAIN_SCALAR_STYLE: if profuse { event.Style = "Plain" } case libyaml.DOUBLE_QUOTED_SCALAR_STYLE: event.Style = "Double" case libyaml.SINGLE_QUOTED_SCALAR_STYLE: event.Style = "Single" case libyaml.LITERAL_SCALAR_STYLE: event.Style = "Literal" case libyaml.FOLDED_SCALAR_STYLE: event.Style = "Folded" } case libyaml.ALIAS_EVENT: event.Type = "ALIAS" event.Anchor = string(ev.Anchor) } return event } golang-go.yaml-yaml-v4-4.0.0~rc5/cmd/go-yaml/json.go000066400000000000000000000056151521353103300220400ustar00rootroot00000000000000// Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Package main provides YAML to JSON conversion utilities for the go-yaml tool. package main import ( "bytes" "encoding/json" "fmt" "io" "os" "go.yaml.in/yaml/v4" ) // ProcessJSON reads YAML from reader and outputs JSON encoding func ProcessJSON(reader io.Reader, pretty, unmarshalMode, decodeMode bool, opts ...yaml.Option) error { if unmarshalMode { return processJSONUnmarshal(reader, pretty) } if decodeMode { return processJSONDecode(reader, pretty, nil) // Decode API doesn't support options } // Default: use Load API with options return processJSONLoad(reader, pretty, opts...) } // processJSONLoad uses Loader.Load for YAML processing with options func processJSONLoad(reader io.Reader, pretty bool, opts ...yaml.Option) error { loader, err := yaml.NewLoader(reader, opts...) if err != nil { return fmt.Errorf("failed to create loader: %w", err) } for { // Read each document var data any err := loader.Load(&data) if err != nil { if err.Error() == "EOF" { break } return fmt.Errorf("failed to decode YAML: %w", err) } // Encode as JSON encoder := json.NewEncoder(os.Stdout) if pretty { encoder.SetIndent("", " ") } if err := encoder.Encode(data); err != nil { return fmt.Errorf("failed to encode JSON: %w", err) } } return nil } // processJSONDecode uses deprecated Decoder.Decode for YAML processing (no options support) func processJSONDecode(reader io.Reader, pretty bool, opts ...yaml.Option) error { decoder := yaml.NewDecoder(reader) for { // Read each document var data any err := decoder.Decode(&data) if err != nil { if err == io.EOF || err.Error() == "EOF" { break } return fmt.Errorf("failed to decode YAML: %w", err) } // Encode as JSON encoder := json.NewEncoder(os.Stdout) if pretty { encoder.SetIndent("", " ") } if err := encoder.Encode(data); err != nil { return fmt.Errorf("failed to encode JSON: %w", err) } } return nil } // processJSONUnmarshal uses yaml.Unmarshal for YAML processing func processJSONUnmarshal(reader io.Reader, pretty bool) error { // Read all input from reader input, err := io.ReadAll(reader) if err != nil { return fmt.Errorf("failed to read input: %w", err) } // Split input into documents documents := bytes.Split(input, []byte("---")) for _, doc := range documents { // Skip empty documents if len(bytes.TrimSpace(doc)) == 0 { continue } // For unmarshal mode, always use `any` to avoid preserving comments var data any err := yaml.Load(doc, &data) if err != nil { return fmt.Errorf("failed to load YAML: %w", err) } // Encode as JSON encoder := json.NewEncoder(os.Stdout) if pretty { encoder.SetIndent("", " ") } if err := encoder.Encode(data); err != nil { return fmt.Errorf("failed to encode JSON: %w", err) } } return nil } golang-go.yaml-yaml-v4-4.0.0~rc5/cmd/go-yaml/main.go000066400000000000000000000553751521353103300220230ustar00rootroot00000000000000// Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // This binary provides a YAML node inspection tool that reads YAML from stdin // and outputs a detailed analysis of its node structure, including comments // and content organization. package main import ( "bytes" "errors" "flag" "fmt" "io" "log" "os" "strings" "go.yaml.in/yaml/v4" ) // version is the current version of the go-yaml CLI tool. const version = "4.0.0.1" // stringSlice is a custom flag type for collecting multiple -o flags type stringSlice []string // String returns the string representation of the slice for [flag.Value] interface. func (s *stringSlice) String() string { if s == nil { return "" } return fmt.Sprint(*s) } // Set appends a value to the slice for [flag.Value] interface. func (s *stringSlice) Set(value string) error { // Special case: empty value or explicit help request if value == "" || value == "help" || value == "?" { printAvailableOptions() os.Exit(0) } *s = append(*s, value) return nil } // optionSpec defines metadata for an option type optionSpec struct { typ string // "bool", "int", "string", "multi" handler func(value string) ([]yaml.Option, error) } // optionRegistry maps option names (including short aliases) to their specs var optionRegistry map[string]optionSpec // initOptionRegistry initializes the option registry func initOptionRegistry() { optionRegistry = map[string]optionSpec{ // Version presets "v2": {typ: "preset", handler: func(string) ([]yaml.Option, error) { return []yaml.Option{yaml.WithV2Defaults()}, nil }}, "v3": {typ: "preset", handler: func(string) ([]yaml.Option, error) { return []yaml.Option{yaml.WithV3Defaults()}, nil }}, "v4": {typ: "preset", handler: func(string) ([]yaml.Option, error) { return []yaml.Option{yaml.WithV4Defaults()}, nil }}, // Formatting options "indent": {typ: "int", handler: func(value string) ([]yaml.Option, error) { var val int if _, err := fmt.Sscanf(value, "%d", &val); err != nil { return nil, fmt.Errorf("indent requires integer value (2-9)") } if val < 2 || val > 9 { return nil, fmt.Errorf("indent must be between 2 and 9") } return []yaml.Option{yaml.WithIndent(val)}, nil }}, "compact-seq-indent": {typ: "bool", handler: func(value string) ([]yaml.Option, error) { val := value == "true" return []yaml.Option{yaml.WithCompactSeqIndent(val)}, nil }}, "compact": {typ: "bool", handler: func(value string) ([]yaml.Option, error) { val := value == "true" return []yaml.Option{yaml.WithCompactSeqIndent(val)}, nil }}, "line-width": {typ: "int", handler: func(value string) ([]yaml.Option, error) { var val int if _, err := fmt.Sscanf(value, "%d", &val); err != nil { return nil, fmt.Errorf("line-width requires integer value") } return []yaml.Option{yaml.WithLineWidth(val)}, nil }}, "width": {typ: "int", handler: func(value string) ([]yaml.Option, error) { var val int if _, err := fmt.Sscanf(value, "%d", &val); err != nil { return nil, fmt.Errorf("width requires integer value") } return []yaml.Option{yaml.WithLineWidth(val)}, nil }}, "unicode": {typ: "bool", handler: func(value string) ([]yaml.Option, error) { val := value == "true" return []yaml.Option{yaml.WithUnicode(val)}, nil }}, "unique-keys": {typ: "bool", handler: func(value string) ([]yaml.Option, error) { val := value == "true" return []yaml.Option{yaml.WithUniqueKeys(val)}, nil }}, "unique": {typ: "bool", handler: func(value string) ([]yaml.Option, error) { val := value == "true" return []yaml.Option{yaml.WithUniqueKeys(val)}, nil }}, "canonical": {typ: "bool", handler: func(value string) ([]yaml.Option, error) { val := value == "true" return []yaml.Option{yaml.WithCanonical(val)}, nil }}, "line-break": {typ: "string", handler: func(value string) ([]yaml.Option, error) { var lb yaml.LineBreak switch value { case "ln": lb = yaml.LineBreakLN case "cr": lb = yaml.LineBreakCR case "crln": lb = yaml.LineBreakCRLN default: return nil, fmt.Errorf("line-break must be ln, cr, or crln") } return []yaml.Option{yaml.WithLineBreak(lb)}, nil }}, "explicit-start": {typ: "bool", handler: func(value string) ([]yaml.Option, error) { val := value == "true" return []yaml.Option{yaml.WithExplicitStart(val)}, nil }}, "explicit-end": {typ: "bool", handler: func(value string) ([]yaml.Option, error) { val := value == "true" return []yaml.Option{yaml.WithExplicitEnd(val)}, nil }}, "explicit": {typ: "multi", handler: func(value string) ([]yaml.Option, error) { val := value == "true" return []yaml.Option{yaml.WithExplicitStart(val), yaml.WithExplicitEnd(val)}, nil }}, "flow-simple-coll": {typ: "bool", handler: func(value string) ([]yaml.Option, error) { val := value == "true" return []yaml.Option{yaml.WithFlowSimpleCollections(val)}, nil }}, "known-fields": {typ: "bool", handler: func(value string) ([]yaml.Option, error) { val := value == "true" return []yaml.Option{yaml.WithKnownFields(val)}, nil }}, "single-document": {typ: "bool", handler: func(value string) ([]yaml.Option, error) { val := value == "true" return []yaml.Option{yaml.WithSingleDocument(val)}, nil }}, "single": {typ: "bool", handler: func(value string) ([]yaml.Option, error) { val := value == "true" return []yaml.Option{yaml.WithSingleDocument(val)}, nil }}, "all-documents": {typ: "bool", handler: func(value string) ([]yaml.Option, error) { val := value == "true" return []yaml.Option{yaml.WithAllDocuments(val)}, nil }}, "all": {typ: "bool", handler: func(value string) ([]yaml.Option, error) { val := value == "true" return []yaml.Option{yaml.WithAllDocuments(val)}, nil }}, "stream-nodes": {typ: "bool", handler: func(value string) ([]yaml.Option, error) { val := value == "true" return []yaml.Option{yaml.WithStreamNodes(val)}, nil }}, "stream": {typ: "bool", handler: func(value string) ([]yaml.Option, error) { val := value == "true" return []yaml.Option{yaml.WithStreamNodes(val)}, nil }}, } } // parseOneOption parses a single option (name=value, name, no-name, or v2/v3/v4) func parseOneOption(s string) ([]yaml.Option, error) { // Special case: help if s == "help" || s == "?" { printAvailableOptions() os.Exit(0) } // Check for version presets first if s == "v2" || s == "v3" || s == "v4" { spec, ok := optionRegistry[s] if !ok { return nil, fmt.Errorf("unknown option: %s", s) } return spec.handler("") } // Check for "no-" prefix for boolean false if len(s) > 3 && s[:3] == "no-" { name := s[3:] spec, ok := optionRegistry[name] if !ok { return nil, fmt.Errorf("unknown option: %s", name) } if spec.typ != "bool" && spec.typ != "multi" { return nil, fmt.Errorf("option %s is not boolean, cannot use no- prefix", name) } return spec.handler("false") } // Check for "name=value" format if name, value, found := strings.Cut(s, "="); found { spec, ok := optionRegistry[name] if !ok { return nil, fmt.Errorf("unknown option: %s", name) } if spec.typ == "bool" || spec.typ == "multi" { // For boolean options with explicit value if value != "true" && value != "false" { return nil, fmt.Errorf("option %s requires true or false value", name) } } return spec.handler(value) } // Must be "name" alone (boolean true) spec, ok := optionRegistry[s] if !ok { return nil, fmt.Errorf("unknown option: %s", s) } if spec.typ != "bool" && spec.typ != "multi" && spec.typ != "preset" { return nil, fmt.Errorf("option %s requires a value (use %s=value)", s, s) } return spec.handler("true") } // parseOptionFlags parses comma-separated options string into individual options func parseOptionFlags(s string) ([]yaml.Option, error) { parts := strings.Split(s, ",") var opts []yaml.Option for _, part := range parts { trimmed := strings.TrimSpace(part) if trimmed == "" { continue } opt, err := parseOneOption(trimmed) if err != nil { return nil, err } opts = append(opts, opt...) } return opts, nil } // printAvailableOptions prints the list of available options for -o flag func printAvailableOptions() { fmt.Print(`Available options for -o/--option: Version presets: v2 V2 defaults (indent:2, no compact-seq-indent) v3 V3 defaults (indent:4, no compact-seq-indent) v4 V4 defaults (indent:2, compact-seq-indent) [default] Formatting options: indent=NUM Indentation spaces (2-9) compact-seq-indent '- ' counts as indentation (short: compact) line-width=NUM Preferred line width, -1=unlimited (short: width) unicode Allow non-ASCII in output canonical Canonical YAML output format line-break=TYPE Line ending: ln, cr, or crln explicit-start Always emit '---' marker explicit-end Always emit '...' marker explicit Both explicit-start and explicit-end flow-simple-coll Flow style for simple collections Loading options: unique-keys Duplicate key detection (short: unique) known-fields Strict field checking single-document Only process first document (short: single) all-documents Multi-document mode (short: all) stream-nodes Enable stream boundary nodes (short: stream) Boolean options: use 'name' for true, 'no-name' for false Multiple options: comma-separated or repeat -o flag Examples: go-yaml -y -o indent=4,canonical go-yaml -y -o v3,width=120,explicit go-yaml -y -o no-unicode,no-compact `) } // buildOptions creates the yaml.Option slice based on config file and -o flags func buildOptions(configFile string, optionFlags []string) ([]yaml.Option, error) { var opts []yaml.Option // Default to V4 preset opts = append(opts, yaml.WithV4Defaults()) // Load config file if specified if configFile != "" { configData, err := os.ReadFile(configFile) if err != nil { return nil, fmt.Errorf("failed to read config file: %w", err) } configOpts, err := yaml.OptsYAML(string(configData)) if err != nil { return nil, fmt.Errorf("failed to parse config file: %w", err) } opts = append(opts, configOpts) } // Process -o flags (can override default preset and config) for _, optStr := range optionFlags { parsedOpts, err := parseOptionFlags(optStr) if err != nil { return nil, err } opts = append(opts, parsedOpts...) } return opts, nil } // main reads YAML from stdin, parses it, and outputs the node structure func main() { // Initialize option registry initOptionRegistry() // Parse command line flags showHelp := flag.Bool("h", false, "Show this help information") // YAML modes yamlMode := flag.Bool("y", false, "YAML encoding output") yamlPreserveMode := flag.Bool("Y", false, "YAML style and comments preserved") // JSON modes jsonMode := flag.Bool("j", false, "JSON compact output") jsonPrettyMode := flag.Bool("J", false, "JSON pretty output") // Token modes tokenMode := flag.Bool("t", false, "Token output") tokenProfuseMode := flag.Bool("T", false, "Token with line info") // Event modes eventMode := flag.Bool("e", false, "Event output") eventProfuseMode := flag.Bool("E", false, "Event with line info") // Node modes nodeMode := flag.Bool("n", false, "Node representation output") nodeProfuseMode := flag.Bool("N", false, "Node with tag and style for all scalars") // Shared flags longMode := flag.Bool("l", false, "Long (block) formatted output") // Config file flag configFile := flag.String("C", "", "Load options from YAML config file") // Option flags (-o/--option) var optionFlags stringSlice flag.Var(&optionFlags, "o", "Set option (name=value, name, no-name)") flag.Var(&optionFlags, "option", "Set option (name=value, name, no-name)") // API selection flags (long form only) var unmarshalMode bool var decodeMode bool var marshalMode bool var encodeMode bool // Long flag aliases flag.BoolVar(showHelp, "help", false, "Show this help information") flag.BoolVar(yamlMode, "yaml", false, "YAML encoding output") flag.BoolVar(yamlPreserveMode, "YAML", false, "YAML style and comments preserved") flag.BoolVar(jsonMode, "json", false, "JSON compact output") flag.BoolVar(jsonPrettyMode, "JSON", false, "JSON pretty output") flag.BoolVar(tokenMode, "token", false, "Token output") flag.BoolVar(tokenProfuseMode, "TOKEN", false, "Token with line info") flag.BoolVar(eventMode, "event", false, "Event output") flag.BoolVar(eventProfuseMode, "EVENT", false, "Event with line info") flag.BoolVar(nodeMode, "node", false, "Node representation output") flag.BoolVar(nodeProfuseMode, "NODE", false, "Node with tag and style for all scalars") flag.BoolVar(longMode, "long", false, "Long (block) formatted output") flag.StringVar(configFile, "config", "", "Load options from YAML config file") // API selection flags (long form only) flag.BoolVar(&unmarshalMode, "unmarshal", false, "Use Unmarshal API for input") flag.BoolVar(&decodeMode, "decode", false, "Use Decode API for input") flag.BoolVar(&marshalMode, "marshal", false, "Use Marshal API for output") flag.BoolVar(&encodeMode, "encode", false, "Use Encode API for output") // Custom usage function to provide helpful message when -o is used without value flag.Usage = func() { // Check if the error is about -o needing an argument if len(os.Args) > 1 { for i, arg := range os.Args[1:] { if (arg == "-o" || arg == "--option") && (i+2 >= len(os.Args) || os.Args[i+2][0] == '-') { fmt.Fprintf(os.Stderr, "The -o flag requires a value.\n") fmt.Fprintf(os.Stderr, "Use '-o help' or '-o ?' to see available options.\n\n") printAvailableOptions() return } } } // Default usage fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0]) flag.PrintDefaults() } flag.Parse() // Validate flag combinations // Check that marshal/encode flags only work with YAML output modes if (marshalMode || encodeMode) && !*yamlMode && !*yamlPreserveMode { fmt.Fprintf(os.Stderr, "Error: --marshal/--encode flags only make sense with YAML output modes (-y/--yaml or -Y/--YAML)\n") os.Exit(1) } // Check that unmarshal doesn't work with preserve mode if unmarshalMode && *yamlPreserveMode { fmt.Fprintf(os.Stderr, "Error: --unmarshal flag doesn't make sense with preserving mode (-Y/--YAML) since unmarshal mode strips comments and styles\n") os.Exit(1) } // Build options for Load API (only when not using explicit old APIs) var opts []yaml.Option if !unmarshalMode && !decodeMode { var err error opts, err = buildOptions(*configFile, optionFlags) if err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n\n", err) printAvailableOptions() os.Exit(1) } } // Show help and exit if *showHelp { printHelp() return } // Get file argument (if any) args := flag.Args() var input io.Reader var inputFile *os.File if len(args) == 0 || (len(args) == 1 && args[0] == "-") { // No file argument or explicit stdin ("-") input = os.Stdin // Check if stdin has data stat, err := os.Stdin.Stat() if err != nil { log.Fatal("Failed to stat stdin:", err) } // If no stdin and no flags, show help if (stat.Mode()&os.ModeCharDevice) != 0 && !*nodeMode && !*nodeProfuseMode && !*eventMode && !*eventProfuseMode && !*tokenMode && !*tokenProfuseMode && !*jsonMode && !*jsonPrettyMode && !*yamlMode && !*yamlPreserveMode && !*longMode { printHelp() return } // Error if stdin has data but no mode flags are provided if (stat.Mode()&os.ModeCharDevice) == 0 && !*nodeMode && !*nodeProfuseMode && !*eventMode && !*eventProfuseMode && !*tokenMode && !*tokenProfuseMode && !*jsonMode && !*jsonPrettyMode && !*yamlMode && !*yamlPreserveMode && !*longMode { fmt.Fprintf(os.Stderr, "Error: stdin has data but no mode specified. Use -n/--node, -N/--NODE, -e/--event, -E/--EVENT, -t/--token, -T/--TOKEN, -j/--json, -J/--JSON, -y/--yaml, -Y/--YAML flag.\n") os.Exit(1) } } else if len(args) == 1 { // File argument provided var err error inputFile, err = os.Open(args[0]) if err != nil { log.Fatal("Failed to open file:", err) } defer inputFile.Close() input = inputFile } else { // Multiple files not supported fmt.Fprintf(os.Stderr, "Error: only one file argument supported\n") os.Exit(1) } // Process YAML input if *eventMode { // Use event formatting mode (compact by default) compact := !*longMode // compact is default, long mode negates it if err := ProcessEvents(input, false, compact, unmarshalMode); err != nil { log.Fatal("Failed to process events:", err) } } else if *eventProfuseMode { // Use event formatting mode with profuse output compact := !*longMode // compact is default, long mode negates it if err := ProcessEvents(input, true, compact, unmarshalMode); err != nil { log.Fatal("Failed to process events:", err) } } else if *tokenMode { // Use token formatting mode (compact by default) compact := !*longMode // compact is default, long mode negates it if err := ProcessTokens(input, false, compact, unmarshalMode); err != nil { log.Fatal("Failed to process tokens:", err) } } else if *tokenProfuseMode { // Use token formatting mode with profuse output compact := !*longMode // compact is default, long mode negates it if err := ProcessTokens(input, true, compact, unmarshalMode); err != nil { log.Fatal("Failed to process tokens:", err) } } else if *jsonMode { // Use JSON formatting mode (compact by default) if err := ProcessJSON(input, false, unmarshalMode, decodeMode, opts...); err != nil { log.Fatal("Failed to process JSON:", err) } } else if *jsonPrettyMode { // Use pretty JSON formatting mode if err := ProcessJSON(input, true, unmarshalMode, decodeMode, opts...); err != nil { log.Fatal("Failed to process JSON:", err) } } else if *yamlMode { // Use YAML formatting mode (clean by default) if err := ProcessYAML(input, false, unmarshalMode, decodeMode, marshalMode, encodeMode, opts); err != nil { log.Fatal("Failed to process YAML:", err) } } else if *yamlPreserveMode { // Use YAML formatting mode with preserve if err := ProcessYAML(input, true, unmarshalMode, decodeMode, marshalMode, encodeMode, opts); err != nil { log.Fatal("Failed to process YAML:", err) } } else { // Use node formatting mode (default) profuse := *nodeProfuseMode if unmarshalMode { // Use Unmarshal mode if err := ProcessNodeUnmarshal(input, profuse); err != nil { log.Fatal("Failed to process YAML node:", err) } } else { // Use Loader mode with options loader, err := yaml.NewLoader(input, opts...) if err != nil { log.Fatal("Failed to create loader:", err) } // Collect all documents var docs []any for { var node yaml.Node err := loader.Load(&node) if errors.Is(err, io.EOF) { break } if err != nil { log.Fatal("Failed to load YAML node:", err) } var info any if profuse { info = FormatNode(node, profuse) } else { info = FormatNodeCompact(node) } docs = append(docs, info) } // Output as sequence if multiple documents, otherwise output single document var output any if len(docs) == 1 { output = docs[0] } else { output = docs } // Use dumper for output var buf bytes.Buffer enc, err := yaml.NewDumper(&buf) if err != nil { log.Fatal("Failed to create dumper:", err) } if err := enc.Dump(output); err != nil { log.Fatal("Failed to dump node info:", err) } if err := enc.Close(); err != nil { log.Fatal("Failed to close dumper:", err) } fmt.Print(buf.String()) } } } // ProcessNodeUnmarshal reads YAML from reader using Unmarshal and outputs node structure func ProcessNodeUnmarshal(reader io.Reader, profuse bool) error { // Read all input from reader input, err := io.ReadAll(reader) if err != nil { return fmt.Errorf("failed to read input: %w", err) } // Split input into documents documents := bytes.Split(input, []byte("---")) // Collect all documents var docs []any for _, doc := range documents { // Skip empty documents if len(bytes.TrimSpace(doc)) == 0 { continue } // For unmarshal mode, use any first to avoid preserving comments var data any if err := yaml.Load(doc, &data); err != nil { return fmt.Errorf("failed to load YAML: %w", err) } // Convert to yaml.Node for node processing var node yaml.Node if err := yaml.Load(doc, &node); err != nil { return fmt.Errorf("failed to load YAML to node: %w", err) } var info any if profuse { info = FormatNode(node, profuse) } else { info = FormatNodeCompact(node) } docs = append(docs, info) } // Output as sequence if multiple documents, otherwise output single document var output any if len(docs) == 1 { output = docs[0] } else { output = docs } // Use dumper for output var buf bytes.Buffer enc, err := yaml.NewDumper(&buf) if err != nil { return fmt.Errorf("failed to create dumper: %w", err) } if err := enc.Dump(output); err != nil { enc.Close() return fmt.Errorf("failed to dump node info: %w", err) } if err := enc.Close(); err != nil { return fmt.Errorf("failed to close dumper: %w", err) } fmt.Print(buf.String()) return nil } // printHelp displays the help information for the program func printHelp() { fmt.Printf(`go-yaml version %s The 'go-yaml' tool shows how the go.yaml.in/yaml/v4 library handles YAML both internally and externally. It is a tool for testing and debugging the library. It reads YAML input text from stdin or a file and writes results to stdout. The go-yaml API has three sets of functions for reading/writing YAML: - Load/Dump (default, new API with options support - v4 defaults) - Decode/Encode (deprecated, no options) - Unmarshal/Marshal (deprecated, no options) Usage: go-yaml [options] [file] Output Mode Options: -y, --yaml YAML encoding output -Y, --YAML YAML w/ style and comments preserved -j, --json JSON compact output -J, --JSON JSON pretty output -t, --token Token output -T, --TOKEN Token with line info -e, --event Event output -E, --EVENT Event with line info -n, --node Node representation output -N, --NODE Node with tag and style for all scalars -l, --long Long (block) formatted output Formatting Options: -o, --option OPT Set option (use without value to see all options) Multiple: -o opt1,opt2 or -o opt1 -o opt2 Booleans: name (true) or no-name (false) Presets: v2, v3, v4 (default: v4) API Selection Options: --unmarshal Use Unmarshal API for input (deprecated, v3 defaults) --decode Use Decode API for input (deprecated) --marshal Use Marshal API for output (deprecated) --encode Use Encode API for output (deprecated) Configuration: -C, --config Load options from YAML config file Other Options: -h, --help Show this help information `, version) } golang-go.yaml-yaml-v4-4.0.0~rc5/cmd/go-yaml/node.go000066400000000000000000000265751521353103300220240ustar00rootroot00000000000000// Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // YAML node formatting utilities for the go-yaml tool. package main import ( "fmt" "go.yaml.in/yaml/v4" ) // TagDirectiveInfo represents a YAML %TAG directive type TagDirectiveInfo struct { Handle string `yaml:"handle"` Prefix string `yaml:"prefix"` } // MapItem is a single item in a MapSlice. type MapItem struct { Key string Value any } // MapSlice is a slice of MapItems that preserves order when marshaled to YAML. type MapSlice []MapItem // MarshalYAML implements yaml.Marshaler for MapSlice to preserve key order. func (ms MapSlice) MarshalYAML() (any, error) { // Convert MapSlice to yaml.Node with MappingNode kind to preserve order node := &yaml.Node{ Kind: yaml.MappingNode, } for _, item := range ms { // Add key node keyNode := &yaml.Node{ Kind: yaml.ScalarNode, Value: item.Key, } // Add value node - let yaml dumper handle the value // Dump item.Value directly into a yaml.Node valueNode := &yaml.Node{} if err := valueNode.Dump(item.Value); err != nil { return nil, err } node.Content = append(node.Content, keyNode, valueNode) } return node, nil } // NodeInfo represents the information about a YAML node type NodeInfo struct { Kind string `yaml:"kind"` Style string `yaml:"style,omitempty"` Anchor string `yaml:"anchor,omitempty"` Tag string `yaml:"tag,omitempty"` Head string `yaml:"head,omitempty"` Line string `yaml:"line,omitempty"` Foot string `yaml:"foot,omitempty"` Text string `yaml:"text,omitempty"` Content []*NodeInfo `yaml:"content,omitempty"` Encoding string `yaml:"encoding,omitempty"` Version string `yaml:"version,omitempty"` TagDirectives []TagDirectiveInfo `yaml:"tag-directives,omitempty"` } // FormatNode converts a YAML node into a NodeInfo structure func FormatNode(n yaml.Node, profuse bool) *NodeInfo { info := &NodeInfo{ Kind: formatKind(n.Kind), } // Don't set style for Document or Stream nodes if n.Kind != yaml.DocumentNode && n.Kind != yaml.StreamNode { if style := formatStyle(n.Style, profuse); style != "" { info.Style = style } } if n.Anchor != "" { info.Anchor = n.Anchor } if tag := formatTag(n.Tag, n.Style, profuse); tag != "" { info.Tag = tag } if n.HeadComment != "" { info.Head = n.HeadComment } if n.LineComment != "" { info.Line = n.LineComment } if n.FootComment != "" { info.Foot = n.FootComment } if info.Kind == "Scalar" { info.Text = n.Value } else if n.Content != nil { info.Content = make([]*NodeInfo, len(n.Content)) for i, node := range n.Content { info.Content[i] = FormatNode(*node, profuse) } } // Handle StreamNode-specific fields if info.Kind == "Stream" && n.Stream != nil { if n.Stream.Encoding != 0 { info.Encoding = formatEncoding(n.Stream.Encoding) } if n.Stream.Version != nil { info.Version = formatVersion(n.Stream.Version) } if len(n.Stream.TagDirectives) > 0 { info.TagDirectives = make([]TagDirectiveInfo, len(n.Stream.TagDirectives)) for i, td := range n.Stream.TagDirectives { info.TagDirectives[i] = TagDirectiveInfo{ Handle: td.Handle, Prefix: td.Prefix, } } } } return info } // formatEncoding converts an encoding constant to its string representation. func formatEncoding(e yaml.Encoding) string { switch e { case yaml.EncodingAny: return "Any" case yaml.EncodingUTF8: return "UTF-8" case yaml.EncodingUTF16LE: return "UTF-16LE" case yaml.EncodingUTF16BE: return "UTF-16BE" default: return "Unknown" } } // formatVersion converts a VersionDirective to its string representation. func formatVersion(v *yaml.VersionDirective) string { if v == nil { return "" } return fmt.Sprintf("%d.%d", v.Major, v.Minor) } // formatKind converts a YAML node kind into its string representation. func formatKind(k yaml.Kind) string { switch k { case yaml.DocumentNode: return "Document" case yaml.SequenceNode: return "Sequence" case yaml.MappingNode: return "Mapping" case yaml.ScalarNode: return "Scalar" case yaml.AliasNode: return "Alias" case yaml.StreamNode: return "Stream" default: return "Unknown" } } // formatStyle converts a YAML node style into its string representation. func formatStyle(s yaml.Style, profuse bool) string { // Remove tagged style bit for checking base style baseStyle := s &^ yaml.TaggedStyle switch baseStyle { case yaml.DoubleQuotedStyle: return "Double" case yaml.SingleQuotedStyle: return "Single" case yaml.LiteralStyle: return "Literal" case yaml.FoldedStyle: return "Folded" case yaml.FlowStyle: return "Flow" case 0: // Plain style - only show if profuse if profuse { return "Plain" } } return "" } // formatStyleName converts a YAML node style into a lowercase style name. // Always returns a style name (defaults to "plain" for style 0). func formatStyleName(s yaml.Style) string { // Remove tagged style bit for checking base style baseStyle := s &^ yaml.TaggedStyle switch baseStyle { case yaml.DoubleQuotedStyle: return "double" case yaml.SingleQuotedStyle: return "single" case yaml.LiteralStyle: return "literal" case yaml.FoldedStyle: return "folded" case yaml.FlowStyle: return "flow" case 0: return "plain" default: return "unknown" } } // formatTag converts a YAML tag string to its string representation. func formatTag(tag string, style yaml.Style, profuse bool) string { // Check if the tag was explicit in the input tagWasExplicit := style&yaml.TaggedStyle != 0 // In profuse mode, always show tag if profuse { return tag } // Default YAML tags - only show if they were explicit in the input switch tag { case "!!str", "!!map", "!!seq", "!!int", "!!float", "!!bool", "!!null": if tagWasExplicit { return tag } return "" } // Show all other tags (custom tags) return tag } // FormatNodeCompact converts a YAML node into a compact representation. // Document nodes return their content directly. // Mapping/Sequence nodes use lowercase keys: "mapping:", "sequence:". // Scalar nodes use style as key: "plain:", "double:", etc. func FormatNodeCompact(n yaml.Node) any { switch n.Kind { case yaml.DocumentNode: // Check if document has properties that need to be preserved hasProperties := n.Anchor != "" || n.HeadComment != "" || n.LineComment != "" || n.FootComment != "" if tag := formatTag(n.Tag, n.Style, false); tag != "" && tag != "!!str" { hasProperties = true } // If document has no properties, return content directly (unwrap) if !hasProperties { if len(n.Content) > 0 { return FormatNodeCompact(*n.Content[0]) } return nil } // Document has properties - create a result map with ordered keys result := MapSlice{} // Add optional fields in order: anchor, tag, comments if n.Anchor != "" { result = append(result, MapItem{Key: "anchor", Value: n.Anchor}) } if tag := formatTag(n.Tag, n.Style, false); tag != "" && tag != "!!str" { result = append(result, MapItem{Key: "tag", Value: tag}) } if n.HeadComment != "" { result = append(result, MapItem{Key: "head", Value: n.HeadComment}) } if n.LineComment != "" { result = append(result, MapItem{Key: "line", Value: n.LineComment}) } if n.FootComment != "" { result = append(result, MapItem{Key: "foot", Value: n.FootComment}) } // Add content if present if len(n.Content) > 0 { content := FormatNodeCompact(*n.Content[0]) // Merge the content into result at the top level if contentMap, ok := content.(MapSlice); ok { result = append(result, contentMap...) } } return result case yaml.MappingNode: result := MapSlice{} // Add optional fields in order: anchor, tag, comments if n.Anchor != "" { result = append(result, MapItem{Key: "anchor", Value: n.Anchor}) } if tag := formatTag(n.Tag, n.Style, false); tag != "" && tag != "!!str" { result = append(result, MapItem{Key: "tag", Value: tag}) } if n.HeadComment != "" { result = append(result, MapItem{Key: "head", Value: n.HeadComment}) } if n.LineComment != "" { result = append(result, MapItem{Key: "line", Value: n.LineComment}) } if n.FootComment != "" { result = append(result, MapItem{Key: "foot", Value: n.FootComment}) } // Convert content (added last) var content []any for _, node := range n.Content { content = append(content, FormatNodeCompact(*node)) } result = append(result, MapItem{Key: "mapping", Value: content}) return result case yaml.SequenceNode: result := MapSlice{} // Add optional fields in order: anchor, tag, comments if n.Anchor != "" { result = append(result, MapItem{Key: "anchor", Value: n.Anchor}) } if tag := formatTag(n.Tag, n.Style, false); tag != "" && tag != "!!str" { result = append(result, MapItem{Key: "tag", Value: tag}) } if n.HeadComment != "" { result = append(result, MapItem{Key: "head", Value: n.HeadComment}) } if n.LineComment != "" { result = append(result, MapItem{Key: "line", Value: n.LineComment}) } if n.FootComment != "" { result = append(result, MapItem{Key: "foot", Value: n.FootComment}) } // Convert content (added last) var content []any for _, node := range n.Content { content = append(content, FormatNodeCompact(*node)) } result = append(result, MapItem{Key: "sequence", Value: content}) return result case yaml.ScalarNode: result := MapSlice{} // Add optional fields in order: anchor, tag, comments if n.Anchor != "" { result = append(result, MapItem{Key: "anchor", Value: n.Anchor}) } if tag := formatTag(n.Tag, n.Style, false); tag != "" && tag != "!!str" { result = append(result, MapItem{Key: "tag", Value: tag}) } if n.HeadComment != "" { result = append(result, MapItem{Key: "head", Value: n.HeadComment}) } if n.LineComment != "" { result = append(result, MapItem{Key: "line", Value: n.LineComment}) } if n.FootComment != "" { result = append(result, MapItem{Key: "foot", Value: n.FootComment}) } // Use style name as the key (added last) styleName := formatStyleName(n.Style) result = append(result, MapItem{Key: styleName, Value: n.Value}) return result case yaml.AliasNode: result := MapSlice{} result = append(result, MapItem{Key: "alias", Value: n.Value}) return result case yaml.StreamNode: result := MapSlice{} // Build stream content content := MapSlice{} if n.Stream != nil { // Add encoding if present if n.Stream.Encoding != 0 { content = append(content, MapItem{Key: "encoding", Value: formatEncoding(n.Stream.Encoding)}) } // Add version if present if n.Stream.Version != nil { content = append(content, MapItem{Key: "version", Value: formatVersion(n.Stream.Version)}) } // Add tag directives if present if len(n.Stream.TagDirectives) > 0 { var directives []TagDirectiveInfo for _, td := range n.Stream.TagDirectives { directives = append(directives, TagDirectiveInfo{ Handle: td.Handle, Prefix: td.Prefix, }) } content = append(content, MapItem{Key: "tag-directives", Value: directives}) } } // Use content as value, or null if empty if len(content) > 0 { result = append(result, MapItem{Key: "stream", Value: content}) } else { result = append(result, MapItem{Key: "stream", Value: nil}) } return result default: return nil } } golang-go.yaml-yaml-v4-4.0.0~rc5/cmd/go-yaml/parser.go000066400000000000000000000116041521353103300223560ustar00rootroot00000000000000// Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Parser wrapper for CLI YAML token/event processing. Provides a simplified // interface for the command-line tool to access internal parser functionality. package main import ( "errors" "fmt" "io" "go.yaml.in/yaml/v4/internal/libyaml" ) // Parser provides access to the internal YAML Parser for CLI use type Parser struct { parser libyaml.Parser done bool pendingTokens []*Token commentsHead int } // NewParser creates a new YAML parser reading from the given reader for CLI use func NewParser(reader io.Reader) (*Parser, error) { p := &Parser{ parser: libyaml.NewParser(), } p.parser.SetInputReader(reader) return p, nil } // Next returns the next token in the YAML stream func (p *Parser) Next() (*Token, error) { // Return pending tokens first if len(p.pendingTokens) > 0 { token := p.pendingTokens[0] p.pendingTokens = p.pendingTokens[1:] return token, nil } if p.done { return nil, nil } var yamlToken libyaml.Token if err := p.parser.Scan(&yamlToken); err != nil { if !errors.Is(err, io.EOF) { return nil, fmt.Errorf("parser error: %w", err) } p.done = true return nil, nil } token := &Token{ StartLine: yamlToken.StartMark.Line, StartColumn: yamlToken.StartMark.Column, EndLine: yamlToken.EndMark.Line, EndColumn: yamlToken.EndMark.Column, } switch yamlToken.Type { case libyaml.STREAM_START_TOKEN: token.Type = "STREAM-START" case libyaml.STREAM_END_TOKEN: token.Type = "STREAM-END" p.done = true case libyaml.DOCUMENT_START_TOKEN: token.Type = "DOCUMENT-START" case libyaml.DOCUMENT_END_TOKEN: token.Type = "DOCUMENT-END" case libyaml.BLOCK_SEQUENCE_START_TOKEN: token.Type = "BLOCK-SEQUENCE-START" case libyaml.BLOCK_MAPPING_START_TOKEN: token.Type = "BLOCK-MAPPING-START" case libyaml.BLOCK_END_TOKEN: token.Type = "BLOCK-END" case libyaml.FLOW_SEQUENCE_START_TOKEN: token.Type = "FLOW-SEQUENCE-START" case libyaml.FLOW_SEQUENCE_END_TOKEN: token.Type = "FLOW-SEQUENCE-END" case libyaml.FLOW_MAPPING_START_TOKEN: token.Type = "FLOW-MAPPING-START" case libyaml.FLOW_MAPPING_END_TOKEN: token.Type = "FLOW-MAPPING-END" case libyaml.BLOCK_ENTRY_TOKEN: token.Type = "BLOCK-ENTRY" case libyaml.FLOW_ENTRY_TOKEN: token.Type = "FLOW-ENTRY" case libyaml.KEY_TOKEN: token.Type = "KEY" case libyaml.VALUE_TOKEN: token.Type = "VALUE" case libyaml.ALIAS_TOKEN: token.Type = "ALIAS" token.Value = string(yamlToken.Value) case libyaml.ANCHOR_TOKEN: token.Type = "ANCHOR" token.Value = string(yamlToken.Value) case libyaml.TAG_TOKEN: token.Type = "TAG" token.Value = string(yamlToken.Value) case libyaml.SCALAR_TOKEN: token.Type = "SCALAR" token.Value = string(yamlToken.Value) token.Style = yamlToken.Style.String() case libyaml.VERSION_DIRECTIVE_TOKEN: token.Type = "VERSION-DIRECTIVE" case libyaml.TAG_DIRECTIVE_TOKEN: token.Type = "TAG-DIRECTIVE" default: token.Type = "UNKNOWN" } // Process comments that should be emitted before this token p.processComments(&yamlToken, token) // Return first pending token if comments were queued, otherwise return the main token if len(p.pendingTokens) > 0 { // Add the main token to the end of pending tokens p.pendingTokens = append(p.pendingTokens, token) // Return the first pending token result := p.pendingTokens[0] p.pendingTokens = p.pendingTokens[1:] return result, nil } return token, nil } // processComments extracts comments from the parser and creates COMMENT tokens func (p *Parser) processComments(yamlToken *libyaml.Token, mainToken *Token) { comments := p.parser.GetPendingComments() for p.commentsHead < len(comments) { comment := &comments[p.commentsHead] // Check if this comment should be emitted before the current token // Comments are associated with tokens based on their TokenMark if yamlToken.StartMark.Index < comment.TokenMark.Index { // This comment is for a future token, stop processing break } // Create comment tokens for head, line, and foot comments p.appendCommentTokenIfNotEmpty(comment.Head, "head", comment) p.appendCommentTokenIfNotEmpty(comment.Line, "line", comment) p.appendCommentTokenIfNotEmpty(comment.Foot, "foot", comment) p.commentsHead++ } } // appendCommentTokenIfNotEmpty creates and appends a comment token if the value is not empty. func (p *Parser) appendCommentTokenIfNotEmpty(value []byte, commentType string, comment *libyaml.Comment) { if len(value) > 0 { commentToken := &Token{ Type: "COMMENT", Value: string(value), CommentType: commentType, StartLine: comment.StartMark.Line, StartColumn: comment.StartMark.Column, EndLine: comment.EndMark.Line, EndColumn: comment.EndMark.Column, } p.pendingTokens = append(p.pendingTokens, commentToken) } } // Close releases the parser resources func (p *Parser) Close() { p.parser.Delete() } golang-go.yaml-yaml-v4-4.0.0~rc5/cmd/go-yaml/testdata/000077500000000000000000000000001521353103300223425ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc5/cmd/go-yaml/testdata/basic.yaml000066400000000000000000000043211521353103300243070ustar00rootroot00000000000000# Basic functionality tests for go-yaml CLI - name: Simple scalar mapping text: | key: value token: | - {token: STREAM-START} - {token: BLOCK-MAPPING-START} - {token: KEY} - {token: SCALAR, value: key} - {token: VALUE} - {token: SCALAR, value: value} - {token: BLOCK-END} - {token: STREAM-END} event: | - {event: DOCUMENT-START} - {event: MAPPING-START} - {event: SCALAR, value: key} - {event: SCALAR, value: value} - {event: MAPPING-END} - {event: DOCUMENT-END} node: | mapping: - plain: key - plain: value - name: Simple sequence text: | - item1 - item2 token: | - {token: STREAM-START} - {token: BLOCK-SEQUENCE-START} - {token: BLOCK-ENTRY} - {token: SCALAR, value: item1} - {token: BLOCK-ENTRY} - {token: SCALAR, value: item2} - {token: BLOCK-END} - {token: STREAM-END} event: | - {event: DOCUMENT-START} - {event: SEQUENCE-START} - {event: SCALAR, value: item1} - {event: SCALAR, value: item2} - {event: SEQUENCE-END} - {event: DOCUMENT-END} - name: Nested mapping and sequence text: | list: - a - b token: | - {token: STREAM-START} - {token: BLOCK-MAPPING-START} - {token: KEY} - {token: SCALAR, value: list} - {token: VALUE} - {token: BLOCK-SEQUENCE-START} - {token: BLOCK-ENTRY} - {token: SCALAR, value: a} - {token: BLOCK-ENTRY} - {token: SCALAR, value: b} - {token: BLOCK-END} - {token: BLOCK-END} - {token: STREAM-END} event: | - {event: DOCUMENT-START} - {event: MAPPING-START} - {event: SCALAR, value: list} - {event: SEQUENCE-START} - {event: SCALAR, value: a} - {event: SCALAR, value: b} - {event: SEQUENCE-END} - {event: MAPPING-END} - {event: DOCUMENT-END} - name: Flow style mapping text: | {a: b, c: d} token: | - {token: STREAM-START} - {token: FLOW-MAPPING-START} - {token: KEY} - {token: SCALAR, value: a} - {token: VALUE} - {token: SCALAR, value: b} - {token: FLOW-ENTRY} - {token: KEY} - {token: SCALAR, value: c} - {token: VALUE} - {token: SCALAR, value: d} - {token: FLOW-MAPPING-END} - {token: STREAM-END} golang-go.yaml-yaml-v4-4.0.0~rc5/cmd/go-yaml/testdata/cmd/000077500000000000000000000000001521353103300231055ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc5/cmd/go-yaml/testdata/cmd/stream.yaml000066400000000000000000000024641521353103300252720ustar00rootroot00000000000000# Command-based tests for stream option - name: Stream option with pipe input cmd: | <<<' a: b ' go-yaml -o stream -n out: | - stream: encoding: UTF-8 - mapping: - plain: a - plain: b - stream: encoding: UTF-8 - name: Stream option profuse with pipe input cmd: | <<<' a: b ' go-yaml -o stream -N out: | - kind: Stream encoding: UTF-8 - kind: Document content: - kind: Mapping style: Plain tag: '!!map' content: - kind: Scalar style: Plain tag: '!!str' text: a - kind: Scalar style: Plain tag: '!!str' text: b - kind: Stream encoding: UTF-8 - name: Without stream option no stream nodes cmd: | <<<' key: value ' go-yaml -n out: | mapping: - plain: key - plain: value - name: Stream option with scalar cmd: | <<<' hello ' go-yaml -o stream -n out: | - stream: encoding: UTF-8 - plain: hello - stream: encoding: UTF-8 - name: Stream option with sequence cmd: | <<<' - a - b ' go-yaml -o stream -n out: | - stream: encoding: UTF-8 - sequence: - plain: a - plain: b - stream: encoding: UTF-8 golang-go.yaml-yaml-v4-4.0.0~rc5/cmd/go-yaml/testdata/comments.yaml000066400000000000000000000040431521353103300250540ustar00rootroot00000000000000# Comment-related tests for go-yaml CLI - name: Simple line comment on a pair text: | a: b #c token: | - {token: STREAM-START} - {token: BLOCK-MAPPING-START} - {token: KEY} - {token: SCALAR, value: a} - {token: VALUE} - {token: COMMENT, line: '#c'} - {token: SCALAR, value: b} - {token: BLOCK-END} - {token: STREAM-END} TOKEN: | - {token: STREAM-START, pos: '1:1'} - {token: BLOCK-MAPPING-START, pos: '1:1'} - {token: KEY, pos: '1:1'} - {token: SCALAR, value: a, pos: '1:1-2'} - {token: VALUE, pos: '1:2-3'} - {token: COMMENT, line: '#c', pos: '1:6-8'} - {token: SCALAR, value: b, pos: '1:4-5'} - {token: BLOCK-END, pos: '2:1'} - {token: STREAM-END, pos: '2:1'} event: | - {event: DOCUMENT-START} - {event: MAPPING-START} - {event: SCALAR, value: a} - {event: SCALAR, value: b, line: '#c'} - {event: MAPPING-END} - {event: DOCUMENT-END} - name: Head, line, and foot comments text: | # head comment key: value # line comment # foot comment token: | - {token: COMMENT, head: '# head comment'} - {token: STREAM-START} - {token: BLOCK-MAPPING-START} - {token: KEY} - {token: SCALAR, value: key} - {token: VALUE} - {token: COMMENT, line: '# line comment'} - {token: COMMENT, foot: '# foot comment'} - {token: SCALAR, value: value} - {token: BLOCK-END} - {token: STREAM-END} event: | - {event: DOCUMENT-START} - {event: MAPPING-START} - {event: SCALAR, value: key, head: '# head comment'} - {event: SCALAR, value: value, line: '# line comment', foot: '# foot comment'} - {event: MAPPING-END} - {event: DOCUMENT-END} - name: Multiple head comments text: | # comment1 # comment2 key: value token: | - {token: COMMENT, head: "# comment1\n# comment2"} - {token: STREAM-START} - {token: BLOCK-MAPPING-START} - {token: KEY} - {token: SCALAR, value: key} - {token: VALUE} - {token: SCALAR, value: value} - {token: BLOCK-END} - {token: STREAM-END} golang-go.yaml-yaml-v4-4.0.0~rc5/cmd/go-yaml/testdata/document-implicit.yaml000066400000000000000000000176251521353103300266670ustar00rootroot00000000000000# Tests for implicit/explicit document markers in -e and -E modes - name: Single implicit document text: | key: value event: | - {event: DOCUMENT-START} - {event: MAPPING-START} - {event: SCALAR, value: key} - {event: SCALAR, value: value} - {event: MAPPING-END} - {event: DOCUMENT-END} EVENT: | - {event: DOCUMENT-START, implicit: true, pos: '1:1'} - {event: MAPPING-START, pos: '1:1'} - {event: SCALAR, value: key, style: Plain, pos: '1:1-4'} - {event: SCALAR, value: value, style: Plain, pos: '1:6-11'} - {event: MAPPING-END, pos: '2:1'} - {event: DOCUMENT-END, implicit: true, pos: '2:1'} - name: Single explicit document text: | --- key: value event: | - {event: DOCUMENT-START, explicit: true} - {event: MAPPING-START} - {event: SCALAR, value: key} - {event: SCALAR, value: value} - {event: MAPPING-END} - {event: DOCUMENT-END} EVENT: | - {event: DOCUMENT-START, pos: '1:1-4'} - {event: MAPPING-START, pos: '2:1'} - {event: SCALAR, value: key, style: Plain, pos: '2:1-4'} - {event: SCALAR, value: value, style: Plain, pos: '2:6-11'} - {event: MAPPING-END, pos: '3:1'} - {event: DOCUMENT-END, implicit: true, pos: '3:1'} - name: Implicit then explicit document text: | first --- second event: | - {event: DOCUMENT-START} - {event: SCALAR, value: first} - {event: DOCUMENT-END} - {event: DOCUMENT-START, explicit: true} - {event: SCALAR, value: second} - {event: DOCUMENT-END} EVENT: | - {event: DOCUMENT-START, implicit: true, pos: '1:1-6'} - {event: SCALAR, value: first, style: Plain, pos: '1:1-6'} - {event: DOCUMENT-END, implicit: true, pos: '2:1'} - {event: DOCUMENT-START, pos: '2:1-4'} - {event: SCALAR, value: second, style: Plain, pos: '3:1-7'} - {event: DOCUMENT-END, implicit: true, pos: '4:1'} - name: Two explicit documents text: | --- first --- second event: | - {event: DOCUMENT-START, explicit: true} - {event: SCALAR, value: first} - {event: DOCUMENT-END} - {event: DOCUMENT-START, explicit: true} - {event: SCALAR, value: second} - {event: DOCUMENT-END} EVENT: | - {event: DOCUMENT-START, pos: '1:1-4'} - {event: SCALAR, value: first, style: Plain, pos: '2:1-6'} - {event: DOCUMENT-END, implicit: true, pos: '3:1'} - {event: DOCUMENT-START, pos: '3:1-4'} - {event: SCALAR, value: second, style: Plain, pos: '4:1-7'} - {event: DOCUMENT-END, implicit: true, pos: '5:1'} - name: Document with explicit end marker text: | --- key: value ... event: | - {event: DOCUMENT-START, explicit: true} - {event: MAPPING-START} - {event: SCALAR, value: key} - {event: SCALAR, value: value} - {event: MAPPING-END} - {event: DOCUMENT-END, explicit: true} EVENT: | - {event: DOCUMENT-START, pos: '1:1-4'} - {event: MAPPING-START, pos: '2:1'} - {event: SCALAR, value: key, style: Plain, pos: '2:1-4'} - {event: SCALAR, value: value, style: Plain, pos: '2:6-11'} - {event: MAPPING-END, pos: '3:1'} - {event: DOCUMENT-END, pos: '3:1-4'} - name: Implicit doc with explicit end then explicit doc text: | first ... --- second event: | - {event: DOCUMENT-START} - {event: SCALAR, value: first} - {event: DOCUMENT-END, explicit: true} - {event: DOCUMENT-START, explicit: true} - {event: SCALAR, value: second} - {event: DOCUMENT-END} EVENT: | - {event: DOCUMENT-START, implicit: true, pos: '1:1-6'} - {event: SCALAR, value: first, style: Plain, pos: '1:1-6'} - {event: DOCUMENT-END, pos: '2:1-4'} - {event: DOCUMENT-START, pos: '3:1-4'} - {event: SCALAR, value: second, style: Plain, pos: '4:1-7'} - {event: DOCUMENT-END, implicit: true, pos: '5:1'} - name: Three documents with mixed markers text: | first --- second --- third event: | - {event: DOCUMENT-START} - {event: SCALAR, value: first} - {event: DOCUMENT-END} - {event: DOCUMENT-START, explicit: true} - {event: SCALAR, value: second} - {event: DOCUMENT-END} - {event: DOCUMENT-START, explicit: true} - {event: SCALAR, value: third} - {event: DOCUMENT-END} EVENT: | - {event: DOCUMENT-START, implicit: true, pos: '1:1-6'} - {event: SCALAR, value: first, style: Plain, pos: '1:1-6'} - {event: DOCUMENT-END, implicit: true, pos: '2:1'} - {event: DOCUMENT-START, pos: '2:1-4'} - {event: SCALAR, value: second, style: Plain, pos: '3:1-7'} - {event: DOCUMENT-END, implicit: true, pos: '4:1'} - {event: DOCUMENT-START, pos: '4:1-4'} - {event: SCALAR, value: third, style: Plain, pos: '5:1-6'} - {event: DOCUMENT-END, implicit: true, pos: '6:1'} - name: Explicit document with mapping text: | --- doc1: value1 doc2: value2 ... event: | - {event: DOCUMENT-START, explicit: true} - {event: MAPPING-START} - {event: SCALAR, value: doc1} - {event: SCALAR, value: value1} - {event: SCALAR, value: doc2} - {event: SCALAR, value: value2} - {event: MAPPING-END} - {event: DOCUMENT-END, explicit: true} EVENT: | - {event: DOCUMENT-START, pos: '1:1-4'} - {event: MAPPING-START, pos: '2:1'} - {event: SCALAR, value: doc1, style: Plain, pos: '2:1-5'} - {event: SCALAR, value: value1, style: Plain, pos: '2:7-13'} - {event: SCALAR, value: doc2, style: Plain, pos: '3:1-5'} - {event: SCALAR, value: value2, style: Plain, pos: '3:7-13'} - {event: MAPPING-END, pos: '4:1'} - {event: DOCUMENT-END, pos: '4:1-4'} - name: Empty explicit documents text: | --- --- event: | - {event: DOCUMENT-START, explicit: true} - {event: SCALAR} - {event: DOCUMENT-END} - {event: DOCUMENT-START, explicit: true} - {event: SCALAR} - {event: DOCUMENT-END} EVENT: | - {event: DOCUMENT-START, pos: '1:1-4'} - {event: SCALAR, style: Plain, pos: '2:1'} - {event: DOCUMENT-END, implicit: true, pos: '2:1'} - {event: DOCUMENT-START, pos: '2:1-4'} - {event: SCALAR, style: Plain, pos: '3:1'} - {event: DOCUMENT-END, implicit: true, pos: '3:1'} - name: Implicit sequence document text: | - item1 - item2 event: | - {event: DOCUMENT-START} - {event: SEQUENCE-START} - {event: SCALAR, value: item1} - {event: SCALAR, value: item2} - {event: SEQUENCE-END} - {event: DOCUMENT-END} EVENT: | - {event: DOCUMENT-START, implicit: true, pos: '1:1'} - {event: SEQUENCE-START, pos: '1:1'} - {event: SCALAR, value: item1, style: Plain, pos: '1:3-8'} - {event: SCALAR, value: item2, style: Plain, pos: '2:3-8'} - {event: SEQUENCE-END, pos: '3:1'} - {event: DOCUMENT-END, implicit: true, pos: '3:1'} - name: Explicit sequence document text: | --- - item1 - item2 event: | - {event: DOCUMENT-START, explicit: true} - {event: SEQUENCE-START} - {event: SCALAR, value: item1} - {event: SCALAR, value: item2} - {event: SEQUENCE-END} - {event: DOCUMENT-END} EVENT: | - {event: DOCUMENT-START, pos: '1:1-4'} - {event: SEQUENCE-START, pos: '2:1'} - {event: SCALAR, value: item1, style: Plain, pos: '2:3-8'} - {event: SCALAR, value: item2, style: Plain, pos: '3:3-8'} - {event: SEQUENCE-END, pos: '4:1'} - {event: DOCUMENT-END, implicit: true, pos: '4:1'} - name: All explicit markers text: | --- first ... --- second ... event: | - {event: DOCUMENT-START, explicit: true} - {event: SCALAR, value: first} - {event: DOCUMENT-END, explicit: true} - {event: DOCUMENT-START, explicit: true} - {event: SCALAR, value: second} - {event: DOCUMENT-END, explicit: true} EVENT: | - {event: DOCUMENT-START, pos: '1:1-4'} - {event: SCALAR, value: first, style: Plain, pos: '2:1-6'} - {event: DOCUMENT-END, pos: '3:1-4'} - {event: DOCUMENT-START, pos: '4:1-4'} - {event: SCALAR, value: second, style: Plain, pos: '5:1-7'} - {event: DOCUMENT-END, pos: '6:1-4'} golang-go.yaml-yaml-v4-4.0.0~rc5/cmd/go-yaml/testdata/key-ordering.yaml000066400000000000000000000113521521353103300256270ustar00rootroot00000000000000# Key ordering tests for -n and -N flags # These tests verify that keys appear in the correct order: # For -n: anchor, tag, head, line, foot, then content (mapping/sequence/scalar) # For -N: kind, style, anchor, tag, head, line, foot, then text/content - name: Scalar with tag and anchor text: | &myanchor !!str value node: | anchor: myanchor plain: value NODE: | kind: Document content: - kind: Scalar style: Plain anchor: myanchor tag: '!!str' text: value - name: Scalar with anchor and comments text: | # head comment &myanchor value # line comment # foot comment node: | anchor: myanchor head: '# head comment' line: '# line comment' foot: '# foot comment' plain: value NODE: | kind: Document content: - kind: Scalar style: Plain anchor: myanchor tag: '!!str' head: '# head comment' line: '# line comment' foot: '# foot comment' text: value - name: Mapping with tag, anchor, and comments text: | # head comment &myanchor !!map key: value # line comment # foot comment node: | anchor: myanchor tag: '!!map' mapping: - head: '# head comment' foot: '# foot comment' plain: key - line: '# line comment' plain: value NODE: | kind: Document content: - kind: Mapping style: Plain anchor: myanchor tag: '!!map' content: - kind: Scalar style: Plain tag: '!!str' head: '# head comment' foot: '# foot comment' text: key - kind: Scalar style: Plain tag: '!!str' line: '# line comment' text: value - name: Sequence with tag and anchor text: | &myseq !!seq - item1 - item2 node: | anchor: myseq tag: '!!seq' sequence: - plain: item1 - plain: item2 NODE: | kind: Document content: - kind: Sequence style: Plain anchor: myseq tag: '!!seq' content: - kind: Scalar style: Plain tag: '!!str' text: item1 - kind: Scalar style: Plain tag: '!!str' text: item2 - name: Sequence with all metadata fields text: | # head comment &myseq !!seq - item1 - item2 # foot comment node: | anchor: myseq tag: '!!seq' sequence: - head: '# head comment' plain: item1 - foot: '# foot comment' plain: item2 NODE: | kind: Document content: - kind: Sequence style: Plain anchor: myseq tag: '!!seq' content: - kind: Scalar style: Plain tag: '!!str' head: '# head comment' text: item1 - kind: Scalar style: Plain tag: '!!str' foot: '# foot comment' text: item2 - name: Tag comes before anchor text: | &anchor !!str tagged node: | anchor: anchor plain: tagged NODE: | kind: Document content: - kind: Scalar style: Plain anchor: anchor tag: '!!str' text: tagged - name: Comments come before content text: | # head key: value # line node: | mapping: - head: '# head' plain: key - line: '# line' plain: value NODE: | kind: Document content: - kind: Mapping style: Plain tag: '!!map' content: - kind: Scalar style: Plain tag: '!!str' head: '# head' text: key - kind: Scalar style: Plain tag: '!!str' line: '# line' text: value - name: Double quoted scalar with tag and anchor text: | &myanchor !!str "quoted" node: | anchor: myanchor double: quoted NODE: | kind: Document content: - kind: Scalar style: Double anchor: myanchor tag: '!!str' text: quoted - name: Nested mapping with ordered keys text: | &outer !!map # outer head nested: &inner !!map # inner head key: value node: | anchor: outer tag: '!!map' mapping: - head: '# outer head' plain: nested - anchor: inner tag: '!!map' mapping: - head: '# inner head' plain: key - plain: value NODE: | kind: Document content: - kind: Mapping style: Plain anchor: outer tag: '!!map' content: - kind: Scalar style: Plain tag: '!!str' head: '# outer head' text: nested - kind: Mapping style: Plain anchor: inner tag: '!!map' content: - kind: Scalar style: Plain tag: '!!str' head: '# inner head' text: key - kind: Scalar style: Plain tag: '!!str' text: value golang-go.yaml-yaml-v4-4.0.0~rc5/cmd/go-yaml/testdata/multi-document.yaml000066400000000000000000000263221521353103300262010ustar00rootroot00000000000000# Multi-document tests for -n and -N flags # Multiple documents should be output as a YAML sequence - name: Two simple mappings text: | doc1: value1 --- doc2: value2 node: | - mapping: - plain: doc1 - plain: value1 - mapping: - plain: doc2 - plain: value2 NODE: | - kind: Document content: - kind: Mapping style: Plain tag: '!!map' content: - kind: Scalar style: Plain tag: '!!str' text: doc1 - kind: Scalar style: Plain tag: '!!str' text: value1 - kind: Document content: - kind: Mapping style: Plain tag: '!!map' content: - kind: Scalar style: Plain tag: '!!str' text: doc2 - kind: Scalar style: Plain tag: '!!str' text: value2 - name: Three documents with mixed types text: | scalar1 --- - item1 - item2 --- key: value node: | - plain: scalar1 - sequence: - plain: item1 - plain: item2 - mapping: - plain: key - plain: value NODE: | - kind: Document content: - kind: Scalar style: Plain tag: '!!str' text: scalar1 - kind: Document content: - kind: Sequence style: Plain tag: '!!seq' content: - kind: Scalar style: Plain tag: '!!str' text: item1 - kind: Scalar style: Plain tag: '!!str' text: item2 - kind: Document content: - kind: Mapping style: Plain tag: '!!map' content: - kind: Scalar style: Plain tag: '!!str' text: key - kind: Scalar style: Plain tag: '!!str' text: value - name: Documents with anchors and tags text: | &anchor1 !!str first --- &anchor2 !!map key: value node: | - anchor: anchor1 plain: first - anchor: anchor2 tag: '!!map' mapping: - plain: key - plain: value NODE: | - kind: Document content: - kind: Scalar style: Plain anchor: anchor1 tag: '!!str' text: first - kind: Document content: - kind: Mapping style: Plain anchor: anchor2 tag: '!!map' content: - kind: Scalar style: Plain tag: '!!str' text: key - kind: Scalar style: Plain tag: '!!str' text: value - name: Documents with comments text: | # comment 1 doc1: value1 --- # comment 2 doc2: value2 node: | - mapping: - head: '# comment 1' plain: doc1 - plain: value1 - mapping: - head: '# comment 2' plain: doc2 - plain: value2 NODE: | - kind: Document content: - kind: Mapping style: Plain tag: '!!map' content: - kind: Scalar style: Plain tag: '!!str' head: '# comment 1' text: doc1 - kind: Scalar style: Plain tag: '!!str' text: value1 - kind: Document content: - kind: Mapping style: Plain tag: '!!map' content: - kind: Scalar style: Plain tag: '!!str' head: '# comment 2' text: doc2 - kind: Scalar style: Plain tag: '!!str' text: value2 - name: Five documents text: | first --- second --- third --- fourth --- fifth node: | - plain: first - plain: second - plain: third - plain: fourth - plain: fifth NODE: | - kind: Document content: - kind: Scalar style: Plain tag: '!!str' text: first - kind: Document content: - kind: Scalar style: Plain tag: '!!str' text: second - kind: Document content: - kind: Scalar style: Plain tag: '!!str' text: third - kind: Document content: - kind: Scalar style: Plain tag: '!!str' text: fourth - kind: Document content: - kind: Scalar style: Plain tag: '!!str' text: fifth - name: Multiple sequences text: | - a - b --- - c - d --- - e - f node: | - sequence: - plain: a - plain: b - sequence: - plain: c - plain: d - sequence: - plain: e - plain: f NODE: | - kind: Document content: - kind: Sequence style: Plain tag: '!!seq' content: - kind: Scalar style: Plain tag: '!!str' text: a - kind: Scalar style: Plain tag: '!!str' text: b - kind: Document content: - kind: Sequence style: Plain tag: '!!seq' content: - kind: Scalar style: Plain tag: '!!str' text: c - kind: Scalar style: Plain tag: '!!str' text: d - kind: Document content: - kind: Sequence style: Plain tag: '!!seq' content: - kind: Scalar style: Plain tag: '!!str' text: e - kind: Scalar style: Plain tag: '!!str' text: f - name: Documents with different scalar styles text: | plain --- "double" --- 'single' node: | - plain: plain - double: double - single: single NODE: | - kind: Document content: - kind: Scalar style: Plain tag: '!!str' text: plain - kind: Document content: - kind: Scalar style: Double tag: '!!str' text: double - kind: Document content: - kind: Scalar style: Single tag: '!!str' text: single - name: Nested structures across documents text: | outer: inner: deep: value1 --- list: - nested: item: value2 node: | - mapping: - plain: outer - mapping: - plain: inner - mapping: - plain: deep - plain: value1 - mapping: - plain: list - sequence: - mapping: - plain: nested - mapping: - plain: item - plain: value2 NODE: | - kind: Document content: - kind: Mapping style: Plain tag: '!!map' content: - kind: Scalar style: Plain tag: '!!str' text: outer - kind: Mapping style: Plain tag: '!!map' content: - kind: Scalar style: Plain tag: '!!str' text: inner - kind: Mapping style: Plain tag: '!!map' content: - kind: Scalar style: Plain tag: '!!str' text: deep - kind: Scalar style: Plain tag: '!!str' text: value1 - kind: Document content: - kind: Mapping style: Plain tag: '!!map' content: - kind: Scalar style: Plain tag: '!!str' text: list - kind: Sequence style: Plain tag: '!!seq' content: - kind: Mapping style: Plain tag: '!!map' content: - kind: Scalar style: Plain tag: '!!str' text: nested - kind: Mapping style: Plain tag: '!!map' content: - kind: Scalar style: Plain tag: '!!str' text: item - kind: Scalar style: Plain tag: '!!str' text: value2 - name: Documents with full metadata text: | # head1 &anchor1 !!str value1 # line1 # foot1 --- # head2 &anchor2 !!seq - item # line2 # foot2 --- # head3 &anchor3 !!map key: val # line3 # foot3 node: | - foot: '# foot1' anchor: anchor1 head: '# head1' line: '# line1' plain: value1 - foot: '# foot2' anchor: anchor2 tag: '!!seq' sequence: - head: '# head2' line: '# line2' plain: item - anchor: anchor3 tag: '!!map' mapping: - head: '# head3' foot: '# foot3' plain: key - line: '# line3' plain: val NODE: | - kind: Document foot: '# foot1' content: - kind: Scalar style: Plain anchor: anchor1 tag: '!!str' head: '# head1' line: '# line1' text: value1 - kind: Document foot: '# foot2' content: - kind: Sequence style: Plain anchor: anchor2 tag: '!!seq' content: - kind: Scalar style: Plain tag: '!!str' head: '# head2' line: '# line2' text: item - kind: Document content: - kind: Mapping style: Plain anchor: anchor3 tag: '!!map' content: - kind: Scalar style: Plain tag: '!!str' head: '# head3' foot: '# foot3' text: key - kind: Scalar style: Plain tag: '!!str' line: '# line3' text: val - name: Documents with integers and special values text: | 42 --- 3.14 --- true --- null node: | - plain: '42' - plain: '3.14' - plain: 'true' - plain: 'null' NODE: | - kind: Document content: - kind: Scalar style: Plain tag: '!!int' text: '42' - kind: Document content: - kind: Scalar style: Plain tag: '!!float' text: '3.14' - kind: Document content: - kind: Scalar style: Plain tag: '!!bool' text: 'true' - kind: Document content: - kind: Scalar style: Plain tag: '!!null' text: 'null' - name: Documents with flow style text: | {key1: value1} --- [item1, item2] node: | - mapping: - plain: key1 - plain: value1 - sequence: - plain: item1 - plain: item2 NODE: | - kind: Document content: - kind: Mapping style: Flow tag: '!!map' content: - kind: Scalar style: Plain tag: '!!str' text: key1 - kind: Scalar style: Plain tag: '!!str' text: value1 - kind: Document content: - kind: Sequence style: Flow tag: '!!seq' content: - kind: Scalar style: Plain tag: '!!str' text: item1 - kind: Scalar style: Plain tag: '!!str' text: item2 - name: Multiple sequences with explicit markers (no duplicates) text: | - 1 --- - 2 explicit-yaml: | --- - 1 ... --- - 2 ... golang-go.yaml-yaml-v4-4.0.0~rc5/cmd/go-yaml/testdata/node-profuse.yaml000066400000000000000000000022671521353103300256430ustar00rootroot00000000000000# Node profuse mode tests - comparing -n and -N flags - name: Plain scalar text: | hello node: | plain: hello NODE: | kind: Document content: - kind: Scalar style: Plain tag: '!!str' text: hello - name: Double quoted scalar text: | "hello" node: | double: hello NODE: | kind: Document content: - kind: Scalar style: Double tag: '!!str' text: hello - name: Integer scalar text: | 42 node: | plain: '42' NODE: | kind: Document content: - kind: Scalar style: Plain tag: '!!int' text: '42' - name: Explicit tag text: | !!str 42 node: | plain: '42' NODE: | kind: Document content: - kind: Scalar style: Plain tag: '!!str' text: '42' - name: Mapping with plain scalars text: | key: value node: | mapping: - plain: key - plain: value NODE: | kind: Document content: - kind: Mapping style: Plain tag: '!!map' content: - kind: Scalar style: Plain tag: '!!str' text: key - kind: Scalar style: Plain tag: '!!str' text: value golang-go.yaml-yaml-v4-4.0.0~rc5/cmd/go-yaml/testdata/options.yaml000066400000000000000000000066201521353103300247250ustar00rootroot00000000000000- name: Version preset V2 text: | foo: - bar - baz v2-yaml: | foo: - bar - baz - name: Version preset V3 text: | foo: - bar - baz v3-yaml: | foo: - bar - baz - name: Custom indent level 3 text: | foo: - bar - baz indent3-yaml: | foo: - bar - baz - name: Line width wrapping text: | text: "This is a very long line of text that should wrap at a specific width" width40-yaml: | text: This is a very long line of text that should wrap at a specific width - name: Compact sequence indentation text: | foo: - bar - baz compact-yaml: | foo: - bar - baz - name: V3 with indent override text: | foo: - bar - baz v3-indent2-yaml: | foo: - bar - baz - name: Nested structure with V2 text: | root: level1: - item1 - item2 level2: key: value v2-yaml: | root: level1: - item1 - item2 level2: key: value - name: Nested structure with V3 text: | root: level1: - item1 - item2 level2: key: value v3-yaml: | root: level1: - item1 - item2 level2: key: value - name: Multiple items default (V4) text: | items: - one - two - three yaml: | items: - one - two - three - name: Multiple items V2 text: | items: - one - two - three v2-yaml: | items: - one - two - three - name: Multiple items V3 text: | items: - one - two - three v3-yaml: | items: - one - two - three - name: Canonical formatting text: | foo: bar num: 42 canonical-yaml: | --- { ? "foo" : "bar", ? "num" : "42", } - name: Explicit markers text: | foo: bar explicit-yaml: | --- foo: bar ... - name: Explicit start only text: | foo: bar explicit-start-yaml: | --- foo: bar - name: Unicode disabled text: | foo: bar no-unicode-yaml: | foo: bar - name: Comma-separated options text: | foo: bar canonical-explicit-yaml: | --- { ? "foo" : "bar", } ... - name: Multiple option overrides text: | foo: - bar - baz v2-indent4-yaml: | foo: - bar - baz - name: Stream option with mapping text: | a: b stream-node: | - stream: encoding: UTF-8 - mapping: - plain: a - plain: b - stream: encoding: UTF-8 stream-NODE: | - kind: Stream encoding: UTF-8 - kind: Document content: - kind: Mapping style: Plain tag: '!!map' content: - kind: Scalar style: Plain tag: '!!str' text: a - kind: Scalar style: Plain tag: '!!str' text: b - kind: Stream encoding: UTF-8 - name: Stream option with scalar text: | hello stream-node: | - stream: encoding: UTF-8 - plain: hello - stream: encoding: UTF-8 - name: Stream option with sequence text: | - item1 - item2 stream-node: | - stream: encoding: UTF-8 - sequence: - plain: item1 - plain: item2 - stream: encoding: UTF-8 golang-go.yaml-yaml-v4-4.0.0~rc5/cmd/go-yaml/testdata/output-modes.yaml000066400000000000000000000073531521353103300257030ustar00rootroot00000000000000# Tests for different output modes: yaml, YAML, json, JSON - name: Simple mapping - YAML output text: | foo: bar baz: qux yaml: | baz: qux foo: bar YAML: | foo: bar baz: qux json: | {"baz":"qux","foo":"bar"} JSON: | { "baz": "qux", "foo": "bar" } - name: Simple sequence - YAML output text: | - item1 - item2 - item3 yaml: | - item1 - item2 - item3 YAML: | - item1 - item2 - item3 json: | ["item1","item2","item3"] JSON: | [ "item1", "item2", "item3" ] - name: Nested structure - YAML output text: | users: - name: Alice age: 30 - name: Bob age: 25 yaml: | users: - age: 30 name: Alice - age: 25 name: Bob YAML: | users: - name: Alice age: 30 - name: Bob age: 25 json: | {"users":[{"age":30,"name":"Alice"},{"age":25,"name":"Bob"}]} JSON: | { "users": [ { "age": 30, "name": "Alice" }, { "age": 25, "name": "Bob" } ] } - name: YAML with comments - preserved text: | # This is a head comment key: value # This is a line comment # This is a foot comment yaml: | key: value YAML: | # This is a head comment key: value # This is a line comment # This is a foot comment json: | {"key":"value"} JSON: | { "key": "value" } - name: Flow style mapping - preserved text: "{a: 1, b: 2, c: 3}" yaml: | a: 1 b: 2 c: 3 YAML: "{a: 1, b: 2, c: 3}" json: | {"a":1,"b":2,"c":3} JSON: | { "a": 1, "b": 2, "c": 3 } - name: Quoted strings - preserved text: | single: 'quoted string' double: "quoted string" plain: plain string yaml: | double: quoted string plain: plain string single: quoted string YAML: | single: 'quoted string' double: "quoted string" plain: plain string json: | {"double":"quoted string","plain":"plain string","single":"quoted string"} JSON: | { "double": "quoted string", "plain": "plain string", "single": "quoted string" } - name: Literal block scalar - style preserved text: | description: | This is a literal block scalar that preserves newlines. yaml: | description: | This is a literal block scalar that preserves newlines. YAML: | description: | This is a literal block scalar that preserves newlines. json: | {"description":"This is a literal\nblock scalar that\npreserves newlines.\n"} JSON: | { "description": "This is a literal\nblock scalar that\npreserves newlines.\n" } - name: Folded block scalar - style preserved text: | description: > This is a folded block scalar that folds newlines. yaml: | description: | This is a folded block scalar that folds newlines. YAML: | description: > This is a folded block scalar that folds newlines. json: | {"description":"This is a folded block scalar that folds newlines.\n"} JSON: | { "description": "This is a folded block scalar that folds newlines.\n" } - name: Mixed types text: | string: hello number: 42 float: 3.14 bool: true null_value: null yaml: | bool: true float: 3.14 null_value: null number: 42 string: hello YAML: | string: hello number: 42 float: 3.14 bool: true null_value: null json: | {"bool":true,"float":3.14,"null_value":null,"number":42,"string":"hello"} JSON: | { "bool": true, "float": 3.14, "null_value": null, "number": 42, "string": "hello" } golang-go.yaml-yaml-v4-4.0.0~rc5/cmd/go-yaml/testdata/positions.yaml000066400000000000000000000027641521353103300252660ustar00rootroot00000000000000# Position format tests for go-yaml CLI - name: Single character position text: | a TOKEN: | - {token: STREAM-START, pos: '1:1'} - {token: SCALAR, value: a, pos: '1:1-2'} - {token: STREAM-END, pos: '2:1'} - name: | Same line range text: | key: value TOKEN: | - {token: STREAM-START, pos: '1:1'} - {token: BLOCK-MAPPING-START, pos: '1:1'} - {token: KEY, pos: '1:1'} - {token: SCALAR, value: key, pos: '1:1-4'} - {token: VALUE, pos: '1:4-5'} - {token: SCALAR, value: value, pos: '1:6-11'} - {token: BLOCK-END, pos: '2:1'} - {token: STREAM-END, pos: '2:1'} - name: | Multi-line literal scalar text: | key: | line1 line2 TOKEN: | - {token: STREAM-START, pos: '1:1'} - {token: BLOCK-MAPPING-START, pos: '1:1'} - {token: KEY, pos: '1:1'} - {token: SCALAR, value: key, pos: '1:1-4'} - {token: VALUE, pos: '1:4-5'} - {token: SCALAR, value: "line1\nline2\n", style: Literal, pos: '1:6-4:1'} - {token: BLOCK-END, pos: '4:1'} - {token: STREAM-END, pos: '4:1'} - name: | Comment position with line comment text: | a: b #c TOKEN: | - {token: STREAM-START, pos: '1:1'} - {token: BLOCK-MAPPING-START, pos: '1:1'} - {token: KEY, pos: '1:1'} - {token: SCALAR, value: a, pos: '1:1-2'} - {token: VALUE, pos: '1:2-3'} - {token: COMMENT, line: '#c', pos: '1:6-8'} - {token: SCALAR, value: b, pos: '1:4-5'} - {token: BLOCK-END, pos: '2:1'} - {token: STREAM-END, pos: '2:1'} golang-go.yaml-yaml-v4-4.0.0~rc5/cmd/go-yaml/token.go000066400000000000000000000433531521353103300222100ustar00rootroot00000000000000// Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Package main provides YAML token formatting utilities for the go-yaml tool. package main import ( "bytes" "fmt" "io" "os" "strings" "go.yaml.in/yaml/v4" ) // Token represents a YAML token with comment information type Token struct { Type string Value string Style string CommentType string // For COMMENT tokens: "head", "line", or "foot" StartLine int StartColumn int EndLine int EndColumn int HeadComment string LineComment string FootComment string } // TokenInfo represents the information about a YAML token for YAML encoding type TokenInfo struct { Token string `yaml:"token"` Value string `yaml:"value,omitempty"` Style string `yaml:"style,omitempty"` Head string `yaml:"head,omitempty"` Line string `yaml:"line,omitempty"` Foot string `yaml:"foot,omitempty"` Pos string `yaml:"pos,omitempty"` } // ProcessTokens reads YAML from reader and outputs token information using the internal scanner func ProcessTokens(reader io.Reader, profuse, compact, unmarshal bool) error { if unmarshal { return processTokensUnmarshal(reader, profuse, compact) } return processTokensWithParser(reader, profuse, compact) } // processTokensDecode uses Loader.Load for YAML processing func processTokensDecode(profuse, compact bool) error { loader, err := yaml.NewLoader(os.Stdin) if err != nil { return fmt.Errorf("failed to create loader: %w", err) } firstDoc := true for { var node yaml.Node err := loader.Load(&node) if err != nil { if err == io.EOF { break } return fmt.Errorf("failed to decode YAML: %w", err) } // Add document separator for all documents except the first if !firstDoc { fmt.Println("---") } firstDoc = false tokens := processNodeToTokens(&node, profuse) if compact { // For compact mode, output each token as a flow style mapping in a sequence for _, token := range tokens { info := formatTokenInfo(token, profuse) // Create a YAML node with flow style for the mapping compactNode := &yaml.Node{ Kind: yaml.MappingNode, Style: yaml.FlowStyle, } // Add the Token field compactNode.Content = append(compactNode.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: "token"}, &yaml.Node{Kind: yaml.ScalarNode, Value: info.Token}) // Add other fields if they exist if info.Value != "" { compactNode.Content = append(compactNode.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: "value"}, &yaml.Node{Kind: yaml.ScalarNode, Value: info.Value}) } if info.Style != "" { compactNode.Content = append(compactNode.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: "style"}, &yaml.Node{Kind: yaml.ScalarNode, Value: info.Style}) } if info.Head != "" { compactNode.Content = append(compactNode.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: "head"}, &yaml.Node{Kind: yaml.ScalarNode, Value: info.Head}) } if info.Line != "" { compactNode.Content = append(compactNode.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: "line"}, &yaml.Node{Kind: yaml.ScalarNode, Value: info.Line}) } if info.Foot != "" { compactNode.Content = append(compactNode.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: "foot"}, &yaml.Node{Kind: yaml.ScalarNode, Value: info.Foot}) } if info.Pos != "" { compactNode.Content = append(compactNode.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: "pos"}, &yaml.Node{Kind: yaml.ScalarNode, Value: info.Pos}) } var buf bytes.Buffer dumper, err := yaml.NewDumper(&buf) if err != nil { return fmt.Errorf("failed to create dumper: %w", err) } if err := dumper.Dump([]*yaml.Node{compactNode}); err != nil { dumper.Close() return fmt.Errorf("failed to dump compact token info: %w", err) } if err := dumper.Close(); err != nil { return fmt.Errorf("failed to close dumper: %w", err) } fmt.Print(buf.String()) } } else { // For non-compact mode, output each token as a separate mapping for _, token := range tokens { info := formatTokenInfo(token, profuse) var buf bytes.Buffer dumper, err := yaml.NewDumper(&buf) if err != nil { return fmt.Errorf("failed to create dumper: %w", err) } if err := dumper.Dump([]*TokenInfo{info}); err != nil { dumper.Close() return fmt.Errorf("failed to dump token info: %w", err) } if err := dumper.Close(); err != nil { return fmt.Errorf("failed to close dumper: %w", err) } fmt.Print(buf.String()) } } } return nil } // processTokensWithParser uses the internal parser for token processing func processTokensWithParser(reader io.Reader, profuse, compact bool) error { p, err := NewParser(reader) if err != nil { return fmt.Errorf("failed to create parser: %w", err) } defer p.Close() for { token, err := p.Next() if err != nil { return fmt.Errorf("failed to get next token: %w", err) } if token == nil { break } info := formatTokenInfo(token, profuse) if compact { // For compact mode, output each token as a flow style mapping in a sequence compactNode := &yaml.Node{ Kind: yaml.MappingNode, Style: yaml.FlowStyle, } // Add the Token field compactNode.Content = append(compactNode.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: "token"}, &yaml.Node{Kind: yaml.ScalarNode, Value: info.Token}) // Add other fields if they exist if info.Value != "" { compactNode.Content = append(compactNode.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: "value"}, &yaml.Node{Kind: yaml.ScalarNode, Value: info.Value}) } if info.Style != "" { compactNode.Content = append(compactNode.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: "style"}, &yaml.Node{Kind: yaml.ScalarNode, Value: info.Style}) } if info.Head != "" { compactNode.Content = append(compactNode.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: "head"}, &yaml.Node{Kind: yaml.ScalarNode, Value: info.Head}) } if info.Line != "" { compactNode.Content = append(compactNode.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: "line"}, &yaml.Node{Kind: yaml.ScalarNode, Value: info.Line}) } if info.Foot != "" { compactNode.Content = append(compactNode.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: "foot"}, &yaml.Node{Kind: yaml.ScalarNode, Value: info.Foot}) } if info.Pos != "" { compactNode.Content = append(compactNode.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: "pos"}, &yaml.Node{Kind: yaml.ScalarNode, Value: info.Pos}) } var buf bytes.Buffer dumper, err := yaml.NewDumper(&buf) if err != nil { return fmt.Errorf("failed to create dumper: %w", err) } if err := dumper.Dump([]*yaml.Node{compactNode}); err != nil { dumper.Close() return fmt.Errorf("failed to dump compact token info: %w", err) } if err := dumper.Close(); err != nil { return fmt.Errorf("failed to close dumper: %w", err) } fmt.Print(buf.String()) } else { // For non-compact mode, output each token as a separate mapping var buf bytes.Buffer dumper, err := yaml.NewDumper(&buf) if err != nil { return fmt.Errorf("failed to create dumper: %w", err) } if err := dumper.Dump([]*TokenInfo{info}); err != nil { dumper.Close() return fmt.Errorf("failed to dump token info: %w", err) } if err := dumper.Close(); err != nil { return fmt.Errorf("failed to close dumper: %w", err) } fmt.Print(buf.String()) } } return nil } // processTokensUnmarshal uses [yaml.Unmarshal] for YAML processing func processTokensUnmarshal(reader io.Reader, profuse, compact bool) error { // Read all input from reader input, err := io.ReadAll(reader) if err != nil { return fmt.Errorf("failed to read input: %w", err) } // Split input into documents documents := bytes.Split(input, []byte("---")) firstDoc := true for _, doc := range documents { // Skip empty documents if len(bytes.TrimSpace(doc)) == 0 { continue } // Add document separator for all documents except the first if !firstDoc { fmt.Println("---") } firstDoc = false // For unmarshal mode, use `any` first to avoid preserving comments var data any if err := yaml.Load(doc, &data); err != nil { return fmt.Errorf("failed to load YAML: %w", err) } // Convert to yaml.Node for token processing var node yaml.Node if err := yaml.Load(doc, &node); err != nil { return fmt.Errorf("failed to load YAML to node: %w", err) } tokens := processNodeToTokens(&node, profuse) if compact { // For compact mode, output each token as a flow style mapping in a sequence for _, token := range tokens { info := formatTokenInfo(token, profuse) // Create a YAML node with flow style for the mapping compactNode := &yaml.Node{ Kind: yaml.MappingNode, Style: yaml.FlowStyle, } // Add the Token field compactNode.Content = append(compactNode.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: "token"}, &yaml.Node{Kind: yaml.ScalarNode, Value: info.Token}) // Add other fields if they exist if info.Value != "" { compactNode.Content = append(compactNode.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: "value"}, &yaml.Node{Kind: yaml.ScalarNode, Value: info.Value}) } if info.Style != "" { compactNode.Content = append(compactNode.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: "style"}, &yaml.Node{Kind: yaml.ScalarNode, Value: info.Style}) } if info.Head != "" { compactNode.Content = append(compactNode.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: "head"}, &yaml.Node{Kind: yaml.ScalarNode, Value: info.Head}) } if info.Line != "" { compactNode.Content = append(compactNode.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: "line"}, &yaml.Node{Kind: yaml.ScalarNode, Value: info.Line}) } if info.Foot != "" { compactNode.Content = append(compactNode.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: "foot"}, &yaml.Node{Kind: yaml.ScalarNode, Value: info.Foot}) } if info.Pos != "" { compactNode.Content = append(compactNode.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: "pos"}, &yaml.Node{Kind: yaml.ScalarNode, Value: info.Pos}) } var buf bytes.Buffer dumper, err := yaml.NewDumper(&buf) if err != nil { return fmt.Errorf("failed to create dumper: %w", err) } if err := dumper.Dump([]*yaml.Node{compactNode}); err != nil { dumper.Close() return fmt.Errorf("failed to dump compact token info: %w", err) } if err := dumper.Close(); err != nil { return fmt.Errorf("failed to close dumper: %w", err) } fmt.Print(buf.String()) } } else { // For non-compact mode, output each token as a separate mapping for _, token := range tokens { info := formatTokenInfo(token, profuse) var buf bytes.Buffer dumper, err := yaml.NewDumper(&buf) if err != nil { return fmt.Errorf("failed to create dumper: %w", err) } if err := dumper.Dump([]*TokenInfo{info}); err != nil { dumper.Close() return fmt.Errorf("failed to dump token info: %w", err) } if err := dumper.Close(); err != nil { return fmt.Errorf("failed to close dumper: %w", err) } fmt.Print(buf.String()) } } } return nil } // formatTokenInfo converts a [Token] to a [TokenInfo] struct for YAML encoding func formatTokenInfo(token *Token, profuse bool) *TokenInfo { info := &TokenInfo{ Token: token.Type, } // For COMMENT tokens, use the CommentType to determine which field to populate if token.Type == "COMMENT" && token.CommentType != "" && token.Value != "" { switch token.CommentType { case "head": info.Head = token.Value case "line": info.Line = token.Value case "foot": info.Foot = token.Value } } else { // For non-COMMENT tokens if token.Value != "" { info.Value = token.Value } if token.Style != "" && token.Style != "Plain" { info.Style = token.Style } if token.HeadComment != "" { info.Head = token.HeadComment } if token.LineComment != "" { info.Line = token.LineComment } if token.FootComment != "" { info.Foot = token.FootComment } } if profuse { if token.StartLine == token.EndLine && token.StartColumn == token.EndColumn { // Single position info.Pos = fmt.Sprintf("%d:%d", token.StartLine, token.StartColumn) } else if token.StartLine == token.EndLine { // Range on same line info.Pos = fmt.Sprintf("%d:%d-%d", token.StartLine, token.StartColumn, token.EndColumn) } else { // Range across different lines info.Pos = fmt.Sprintf("%d:%d-%d:%d", token.StartLine, token.StartColumn, token.EndLine, token.EndColumn) } } return info } // processNodeToTokens converts a node to a slice of tokens func processNodeToTokens(node *yaml.Node, profuse bool) []*Token { var tokens []*Token // Add stream start token tokens = append(tokens, &Token{ Type: "STREAM-START", }) // Add document start token tokens = append(tokens, &Token{ Type: "DOCUMENT-START", }) // Process the node content tokens = append(tokens, processNodeToTokensRecursive(node, profuse)...) // Add document end token tokens = append(tokens, &Token{ Type: "DOCUMENT-END", }) // Add stream end token tokens = append(tokens, &Token{ Type: "STREAM-END", }) return tokens } // processNodeToTokensRecursive recursively converts a node to tokens func processNodeToTokensRecursive(node *yaml.Node, profuse bool) []*Token { var tokens []*Token switch node.Kind { case yaml.DocumentNode: for _, child := range node.Content { tokens = append(tokens, processNodeToTokensRecursive(child, profuse)...) } case yaml.MappingNode: tokens = append(tokens, &Token{ Type: "BLOCK-MAPPING-START", StartLine: node.Line, StartColumn: node.Column, EndLine: node.Line, EndColumn: node.Column, HeadComment: node.HeadComment, LineComment: node.LineComment, FootComment: node.FootComment, }) for i := 0; i < len(node.Content); i += 2 { if i+1 < len(node.Content) { // Key tokens = append(tokens, &Token{ Type: "KEY", StartLine: node.Content[i].Line, StartColumn: node.Content[i].Column, EndLine: node.Content[i].Line, EndColumn: node.Content[i].Column, }) keyTokens := processNodeToTokensRecursive(node.Content[i], profuse) tokens = append(tokens, keyTokens...) // Value tokens = append(tokens, &Token{ Type: "VALUE", StartLine: node.Content[i+1].Line, StartColumn: node.Content[i+1].Column, EndLine: node.Content[i+1].Line, EndColumn: node.Content[i+1].Column, }) valueTokens := processNodeToTokensRecursive(node.Content[i+1], profuse) tokens = append(tokens, valueTokens...) } } tokens = append(tokens, &Token{ Type: "BLOCK-END", StartLine: node.Line, StartColumn: node.Column, EndLine: node.Line, EndColumn: node.Column, }) case yaml.SequenceNode: tokens = append(tokens, &Token{ Type: "BLOCK-SEQUENCE-START", StartLine: node.Line, StartColumn: node.Column, EndLine: node.Line, EndColumn: node.Column, HeadComment: node.HeadComment, LineComment: node.LineComment, FootComment: node.FootComment, }) for _, child := range node.Content { tokens = append(tokens, &Token{ Type: "BLOCK-ENTRY", StartLine: child.Line, StartColumn: child.Column, EndLine: child.Line, EndColumn: child.Column, }) childTokens := processNodeToTokensRecursive(child, profuse) tokens = append(tokens, childTokens...) } tokens = append(tokens, &Token{ Type: "BLOCK-END", StartLine: node.Line, StartColumn: node.Column, EndLine: node.Line, EndColumn: node.Column, }) case yaml.ScalarNode: // Check for anchor before the scalar if node.Anchor != "" { tokens = append(tokens, &Token{ Type: "ANCHOR", Value: node.Anchor, StartLine: node.Line, StartColumn: node.Column, EndLine: node.Line, EndColumn: node.Column, }) } // Check for tag before the scalar // Check if the tag was explicit in the input tagWasExplicit := node.Style&yaml.TaggedStyle != 0 // Show tag if it exists and either: // - it's not !!str, or // - it's !!str and was explicit in the input if node.Tag != "" && (node.Tag != "!!str" || tagWasExplicit) { tokens = append(tokens, &Token{ Type: "TAG", Value: node.Tag, StartLine: node.Line, StartColumn: node.Column, EndLine: node.Line, EndColumn: node.Column, }) } // Calculate end position for scalars based on value length endLine := node.Line endColumn := node.Column if node.Value != "" { // For single-line values, add the length to the column if !strings.Contains(node.Value, "\n") { endColumn += len(node.Value) } else { // For multi-line values, we'd need more complex logic // For now, just use the start position endColumn = node.Column } } tokens = append(tokens, &Token{ Type: "SCALAR", Value: node.Value, StartLine: node.Line, StartColumn: node.Column, EndLine: endLine, EndColumn: endColumn, Style: formatStyle(node.Style, false), HeadComment: node.HeadComment, LineComment: node.LineComment, FootComment: node.FootComment, }) case yaml.AliasNode: // Generate ALIAS token for alias nodes tokens = append(tokens, &Token{ Type: "ALIAS", Value: node.Value, StartLine: node.Line, StartColumn: node.Column, EndLine: node.Line, EndColumn: node.Column, HeadComment: node.HeadComment, LineComment: node.LineComment, FootComment: node.FootComment, }) } return tokens } golang-go.yaml-yaml-v4-4.0.0~rc5/cmd/go-yaml/yaml.go000066400000000000000000000224141521353103300220250ustar00rootroot00000000000000// Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Package main provides YAML formatting utilities for the go-yaml tool. package main import ( "bytes" "fmt" "io" "os" "go.yaml.in/yaml/v4" ) // ProcessYAML reads YAML from reader and outputs formatted YAML func ProcessYAML(reader io.Reader, preserve, unmarshalMode, decodeMode, marshalMode, encodeMode bool, opts []yaml.Option) error { if unmarshalMode { return processYAMLUnmarshal(reader, preserve, marshalMode) } if decodeMode { return processYAMLDecode(reader, preserve, encodeMode, nil) // Decode API doesn't support options } // Default: use Load API with options return processYAMLLoad(reader, preserve, marshalMode, encodeMode, opts) } // processYAMLLoad uses Loader.Load for YAML processing with options func processYAMLLoad(reader io.Reader, preserve, marshal, encode bool, opts []yaml.Option) error { if preserve { // Preserve comments and styles by using yaml.Node loader, err := yaml.NewLoader(reader, opts...) if err != nil { return fmt.Errorf("failed to create loader: %w", err) } // For Dumper mode, create a single Dumper for all documents var dumper *yaml.Dumper if !marshal && !encode { dumper, err = yaml.NewDumper(os.Stdout, opts...) if err != nil { return fmt.Errorf("failed to create dumper: %w", err) } defer dumper.Close() } firstDoc := true for { var node yaml.Node err := loader.Load(&node) if err != nil { if err == io.EOF { break } return fmt.Errorf("failed to decode YAML: %w", err) } // If the node is not a DocumentNode, wrap it in one var outNode *yaml.Node if node.Kind == yaml.DocumentNode { outNode = &node } else { outNode = &yaml.Node{ Kind: yaml.DocumentNode, Content: []*yaml.Node{&node}, } } if marshal { // Add document separator for all documents except the first if !firstDoc { fmt.Println("---") } firstDoc = false // Use Marshal for output (no options) output, err := yaml.Marshal(outNode) if err != nil { return fmt.Errorf("failed to marshal YAML: %w", err) } fmt.Print(string(output)) } else if encode { // Add document separator for all documents except the first if !firstDoc { fmt.Println("---") } firstDoc = false // Use Encoder for output (no options) enc := yaml.NewEncoder(os.Stdout) if err := enc.Encode(outNode); err != nil { enc.Close() return fmt.Errorf("failed to encode YAML: %w", err) } if err := enc.Close(); err != nil { return fmt.Errorf("failed to close encoder: %w", err) } } else { // Use Dumper for output with options // Dumper handles document separators automatically if err := dumper.Dump(outNode); err != nil { return fmt.Errorf("failed to dump YAML: %w", err) } } } } else { // Don't preserve comments and styles - use `any` for clean output loader, err := yaml.NewLoader(reader, opts...) if err != nil { return fmt.Errorf("failed to create loader: %w", err) } // For Dumper mode, create a single Dumper for all documents var dumper *yaml.Dumper if !marshal && !encode { dumper, err = yaml.NewDumper(os.Stdout, opts...) if err != nil { return fmt.Errorf("failed to create dumper: %w", err) } defer dumper.Close() } firstDoc := true for { var data any err := loader.Load(&data) if err != nil { if err == io.EOF || err.Error() == "EOF" { break } return fmt.Errorf("failed to decode YAML: %w", err) } if marshal { // Add document separator for all documents except the first if !firstDoc { fmt.Println("---") } firstDoc = false // Use Marshal for output (no options) output, err := yaml.Marshal(data) if err != nil { return fmt.Errorf("failed to marshal YAML: %w", err) } fmt.Print(string(output)) } else if encode { // Add document separator for all documents except the first if !firstDoc { fmt.Println("---") } firstDoc = false // Use Encoder for output (no options) enc := yaml.NewEncoder(os.Stdout) if err := enc.Encode(data); err != nil { enc.Close() return fmt.Errorf("failed to encode YAML: %w", err) } if err := enc.Close(); err != nil { return fmt.Errorf("failed to close encoder: %w", err) } } else { // Use Dumper for output with options // Dumper handles document separators automatically if err := dumper.Dump(data); err != nil { return fmt.Errorf("failed to dump YAML: %w", err) } } } } return nil } // processYAMLDecode uses deprecated Decoder.Decode for YAML processing (no options support) func processYAMLDecode(reader io.Reader, preserve, encode bool, opts []yaml.Option) error { decoder := yaml.NewDecoder(reader) firstDoc := true for { if preserve { var node yaml.Node err := decoder.Decode(&node) if err != nil { if err == io.EOF { break } return fmt.Errorf("failed to decode YAML: %w", err) } // Add document separator for all documents except the first if !firstDoc { fmt.Println("---") } firstDoc = false // If the node is not a DocumentNode, wrap it in one var outNode *yaml.Node if node.Kind == yaml.DocumentNode { outNode = &node } else { outNode = &yaml.Node{ Kind: yaml.DocumentNode, Content: []*yaml.Node{&node}, } } if encode { // Use Encoder for output enc := yaml.NewEncoder(os.Stdout) if err := enc.Encode(outNode); err != nil { enc.Close() return fmt.Errorf("failed to encode YAML: %w", err) } if err := enc.Close(); err != nil { return fmt.Errorf("failed to close encoder: %w", err) } } else { // Default output (no options for deprecated Decode API) output, err := yaml.Marshal(outNode) if err != nil { return fmt.Errorf("failed to marshal YAML: %w", err) } fmt.Print(string(output)) } } else { var data any err := decoder.Decode(&data) if err != nil { if err == io.EOF || err.Error() == "EOF" { break } return fmt.Errorf("failed to decode YAML: %w", err) } // Add document separator for all documents except the first if !firstDoc { fmt.Println("---") } firstDoc = false if encode { // Use Encoder for output enc := yaml.NewEncoder(os.Stdout) if err := enc.Encode(data); err != nil { enc.Close() return fmt.Errorf("failed to encode YAML: %w", err) } if err := enc.Close(); err != nil { return fmt.Errorf("failed to close encoder: %w", err) } } else { // Default output (no options for deprecated Decode API) output, err := yaml.Marshal(data) if err != nil { return fmt.Errorf("failed to marshal YAML: %w", err) } fmt.Print(string(output)) } } } return nil } // processYAMLUnmarshal uses yaml.Unmarshal for YAML processing func processYAMLUnmarshal(reader io.Reader, preserve, marshal bool) error { // Read all input from reader input, err := io.ReadAll(reader) if err != nil { return fmt.Errorf("failed to read input: %w", err) } // Split input into documents documents := bytes.Split(input, []byte("---")) firstDoc := true for _, doc := range documents { // Skip empty documents if len(bytes.TrimSpace(doc)) == 0 { continue } // Add document separator for all documents except the first if !firstDoc { fmt.Println("---") } firstDoc = false if preserve { // Preserve comments and styles by using yaml.Node var node yaml.Node if err := yaml.Load(doc, &node); err != nil { return fmt.Errorf("failed to load YAML: %w", err) } // If the node is not a DocumentNode, wrap it in one var outNode *yaml.Node if node.Kind == yaml.DocumentNode { outNode = &node } else { outNode = &yaml.Node{ Kind: yaml.DocumentNode, Content: []*yaml.Node{&node}, } } if marshal { // Use Dump for output output, err := yaml.Dump(outNode) if err != nil { return fmt.Errorf("failed to dump YAML: %w", err) } fmt.Print(string(output)) } else { // Use Dumper for output dumper, err := yaml.NewDumper(os.Stdout) if err != nil { return fmt.Errorf("failed to create dumper: %w", err) } if err := dumper.Dump(outNode); err != nil { dumper.Close() return fmt.Errorf("failed to dump YAML: %w", err) } if err := dumper.Close(); err != nil { return fmt.Errorf("failed to close dumper: %w", err) } } } else { // For unmarshal mode with -y (not -Y), always use `any` to avoid preserving comments var data any if err := yaml.Load(doc, &data); err != nil { return fmt.Errorf("failed to load YAML: %w", err) } if marshal { // Use Dump for output output, err := yaml.Dump(data) if err != nil { return fmt.Errorf("failed to dump YAML: %w", err) } fmt.Print(string(output)) } else { // Use Dumper for output dumper, err := yaml.NewDumper(os.Stdout) if err != nil { return fmt.Errorf("failed to create dumper: %w", err) } if err := dumper.Dump(data); err != nil { dumper.Close() return fmt.Errorf("failed to dump YAML: %w", err) } if err := dumper.Close(); err != nil { return fmt.Errorf("failed to close dumper: %w", err) } } } } return nil } golang-go.yaml-yaml-v4-4.0.0~rc5/doc.go000066400000000000000000000070031521353103300175150ustar00rootroot00000000000000// Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Package yaml implements YAML 1.1/1.2 encoding and decoding for Go programs. // // # Quick Start // // For simple encoding and decoding, use [Unmarshal] and [Marshal]: // // type Config struct { // Name string `yaml:"name"` // Version string `yaml:"version"` // } // // // Decode YAML to Go struct // var config Config // err := yaml.Unmarshal(yamlData, &config) // // // Encode Go struct to YAML // data, err := yaml.Marshal(&config) // // For encoding/decoding with options, use [Load] and [Dump]: // // // Decode with strict field checking // err := yaml.Load(data, &config, yaml.WithKnownFields()) // // // Encode with custom indent // data, err := yaml.Dump(&config, yaml.WithIndent(2)) // // // Decode all documents from multi-document stream // var docs []Config // err := yaml.Load(multiDocYAML, &docs, yaml.WithAllDocuments()) // // // Encode multiple documents as multi-document stream // docs := []Config{config1, config2} // data, err := yaml.Dump(docs, yaml.WithAllDocuments()) // // # Streaming with Loader and Dumper // // For multi-document streams or when you need custom options, use [Loader] and [Dumper]: // // // Load multiple documents from a stream // loader, err := yaml.NewLoader(reader) // if err != nil { // log.Fatal(err) // } // for { // var doc any // if err := loader.Load(&doc); err == io.EOF { // break // } else if err != nil { // log.Fatal(err) // } // // Process document... // } // // // Dump multiple documents to a stream // dumper, err := yaml.NewDumper(writer, yaml.WithIndent(2)) // if err != nil { // log.Fatal(err) // } // dumper.Dump(&doc1) // dumper.Dump(&doc2) // dumper.Close() // // # Options System // // Configure YAML processing behavior with functional options: // // yaml.NewDumper(w, // yaml.WithIndent(2), // Indentation spacing // yaml.WithCompactSeqIndent(), // Compact sequences (defaults to true) // yaml.WithLineWidth(80), // Line wrapping width // yaml.WithUnicode(false), // Escape non-ASCII (override default true) // yaml.WithKnownFields(), // Strict field checking (defaults to true) // yaml.WithUniqueKeys(), // Prevent duplicate keys (defaults to true) // yaml.WithSingleDocument(), // Single document mode // ) // // Or use version-specific option presets for consistent formatting: // // yaml.NewDumper(w, yaml.WithV3Defaults()) // // Options can be combined and later options override earlier ones: // // // Start with v3 defaults, then override indent // yaml.NewDumper(w, // yaml.WithV3Defaults(), // yaml.WithIndent(2), // ) // // Load options from YAML configuration files: // // opts, err := yaml.OptsYAML(configYAML) // dumper, err := yaml.NewDumper(w, opts) // // # YAML Compatibility // // This package supports most of YAML 1.2, but preserves some YAML 1.1 // behavior for backward compatibility: // // - YAML 1.1 booleans (yes/no, on/off) are supported when decoding into // typed bool values, otherwise treated as strings // - Octals can use 0777 format (YAML 1.1) or 0o777 format (YAML 1.2) // - Base-60 floats are not supported (removed in YAML 1.2) // // # Version Defaults // // [NewLoader] and [NewDumper] use v4 defaults (2-space indentation, compact // sequences). The older [Marshal] and [Unmarshal] functions use v3 defaults // for backward compatibility. Use the options system to select different // version defaults if needed. package yaml golang-go.yaml-yaml-v4-4.0.0~rc5/docs/000077500000000000000000000000001521353103300173515ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc5/docs/README.md000066400000000000000000000021261521353103300206310ustar00rootroot00000000000000# go-yaml Documentation ## User Guides | Document | Description | |----------|-------------| | [v3 to v4 Migration](v3-to-v4-migration.md) | Complete guide for upgrading from v3 | | [Options Reference](options.md) | All configuration options explained | ## Developer Documentation For contributors and those interested in go-yaml internals: - **[dev/](dev/)** — Internal documentation directory - [go-yaml Internals](dev/go-yaml-internals.md) — Comprehensive analysis of load/dump stacks, stages, transforms, and architectural issues - [How go-yaml Works](dev/how-go-yaml-works.md) — High-level overview of YAML processing - [Dump and Load API](dev/dump-load-api.md) — Comprehensive API guide - [Mermaid Diagrams](dev/) — Pipeline overview, call hierarchy, comment flow, and resolver problem diagrams ## Quick Links - [Main README](../README.md) — Getting started - [CONTRIBUTING](../CONTRIBUTING.md) — How to contribute - [Examples](../example/README.md) — Runnable code examples - [API Reference](https://pkg.go.dev/go.yaml.in/yaml/v4) — Full API documentation golang-go.yaml-yaml-v4-4.0.0~rc5/docs/dev/000077500000000000000000000000001521353103300201275ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc5/docs/dev/README.md000066400000000000000000000024251521353103300214110ustar00rootroot00000000000000# Developer Documentation This directory contains internal documentation for go-yaml developers and contributors. ## Contents - **[go-yaml-internals.md](go-yaml-internals.md)** - Comprehensive analysis of the load and dump stack internals, including all stages, transforms, security features, and architectural issues - **[how-go-yaml-works.md](how-go-yaml-works.md)** - High-level overview of how go-yaml processes YAML - **[dump-load-api.md](dump-load-api.md)** - API documentation for dump and load operations ### Mermaid Diagrams - **[pipeline-overview.mmd](pipeline-overview.mmd)** - Complete processing pipeline from bytes to Go values and back, showing the symmetric 3-stage load/dump architecture with Resolver and Desolver stages - **[call-hierarchy.mmd](call-hierarchy.mmd)** - Function call hierarchy showing pull-based (load) vs push-based (dump) architecture - **[comment-flow.mmd](comment-flow.mmd)** - Comment handling flow through Scanner → Parser → Composer ## For Users User-facing documentation is in the parent [docs/](../) directory: - [README.md](../README.md) - Main documentation index - [options.md](../options.md) - Configuration options reference - [v3-to-v4-migration.md](../v3-to-v4-migration.md) - Migration guide for upgrading from v3 to v4 golang-go.yaml-yaml-v4-4.0.0~rc5/docs/dev/call-hierarchy.mmd000066400000000000000000000030301521353103300235110ustar00rootroot00000000000000--- title: go-yaml Call Hierarchy --- flowchart TB subgraph LOAD ["LOAD: Pull-Based"] direction TB L_entry["Entry Points
Loader.Load / Load"] L_loader["Loader.Load
loader.go"] L_constructor["Constructor.Construct
constructor.go"] L_resolver["Resolver.Resolve
resolver.go"] L_composer["Composer.Parse
composer.go"] L_parser["Parser.Parse
parser.go"] L_scanner["Scanner.Scan
scanner.go"] L_reader["Reader.updateBuffer
reader.go"] L_entry -->|calls| L_loader L_loader -->|1. compose| L_composer L_loader -->|2. resolve| L_resolver L_loader -->|3. construct| L_constructor L_composer -->|calls| L_parser L_parser -->|calls via peekToken| L_scanner L_scanner -->|calls| L_reader end subgraph DUMP ["DUMP: Push-Based"] direction TB D_entry["Entry Points
Dumper.Dump / Dump"] D_dumper["Dumper.Dump
dumper.go"] D_representer["Representer.Represent
representer.go"] D_desolver["Desolver.Desolve
desolver.go"] D_serializer["Serializer.Serialize
serializer.go"] D_emitter["Emitter.Emit
emitter.go"] D_writer["Writer.flush
writer.go"] D_entry -->|calls| D_dumper D_dumper -->|1. represent| D_representer D_dumper -->|2. desolve| D_desolver D_dumper -->|3. serialize| D_serializer D_serializer -->|calls emit| D_emitter D_emitter -->|calls| D_writer end golang-go.yaml-yaml-v4-4.0.0~rc5/docs/dev/comment-flow.mmd000066400000000000000000000015761521353103300232460ustar00rootroot00000000000000--- title: Comment Flow Through Load Stack --- flowchart TB subgraph SCANNER ["Scanner"] S1[scanLineComment
same-line comments] S2[scanComments
classify head/foot/line] S3[Comment struct
head, line, foot fields] S1 --> S3 S2 --> S3 end subgraph PARSER ["Parser"] P1[UnfoldComments
join to tokens by position] P2[Parser fields
HeadComment, LineComment, FootComment] P3[setEventComments
transfer to Event] P4[TAIL_COMMENT_EVENT
block-end foot comments] P1 --> P2 P2 --> P3 P2 --> P4 end subgraph COMPOSER ["Composer"] C1[node
basic transfer] C2[mapping
reassignment logic] C3[Node fields
HeadComment, LineComment, FootComment] C1 --> C3 C2 --> C3 end S3 --> P1 P3 --> C1 P4 --> C2 golang-go.yaml-yaml-v4-4.0.0~rc5/docs/dev/dump-load-api.md000066400000000000000000000334071521353103300231110ustar00rootroot00000000000000# YAML Dumping and Loading API This guide covers go-yaml's dumping and loading APIs, from simple one-liners to advanced streaming with full configuration control. ## Recommended API for v4+ **For new code in v4+, prefer `Dump`/`Load` over `Marshal`/`Unmarshal` and `Encode`/`Decode`.** The classic API (`Marshal`, `Unmarshal`, `Encoder`, `Decoder`) remains supported for compatibility, but `Dump`/`Load` and `Dumper`/`Loader` offer more flexibility with options support. ### Why Dump and Load? The `Dump` and `Load` functions provide the same simplicity as `Marshal`/`Unmarshal` while offering full control over YAML formatting and parsing behavior through options: ```go // Old way (v3 semantics, no options): data, _ := yaml.Marshal(&config) // New way (v4 semantics by default + full options): data, _ := yaml.Dump(&config) // v4 defaults: 2-space indent, compact data, _ := yaml.Dump(&config, yaml.WithIndent(4)) // v4 with custom indent ``` The names `Dump` and `Load` are from the YAML specification and also the most common names used for these actions in actual YAML implementations. **Note**: Marshal, Unmarshal, Encode and Decode are not part of the standard YAML vocabulary, but the are still fitting because go-yaml v2 and v3 did not provide a full Dump and Load stack. Those involve the Represent (for Dump) and Construct (for Load) YAML stack stages and v4 will offer those (optionally) for Dump and Load. ### Classic API `Marshal`/`Unmarshal` and `Encode`/`Decode` provide a simple, options-free interface: - They work **without options** and use **v3 semantics** (4-space indent, non-compact sequences) - They're perfect for simple use cases and upgrading existing v3 code - For new code, use `Dump`/`Load` instead—they can replicate the same behavior with added flexibility ### Flexibility With `Dump` and `Load`, you can choose your starting point: - **v4 semantics**: `yaml.Dump(&data)` (default: 2-space indent, compact sequences) - **v3 semantics**: `yaml.Dump(&data, yaml.WithV3Defaults())` (same indentation and sequence style as Marshal: 4-space indent, non-compact) - **v2 semantics**: `yaml.Dump(&data, yaml.WithV2Defaults())` (2-space indent, non-compact) - **Custom options**: `yaml.Dump(&data, yaml.WithIndent(4), yaml.WithExplicitStart())` This means `Dump` and `Load` default to modern v4 formatting, but can easily replicate what `Marshal`/`Unmarshal` do, while also giving you complete control when you need it. ## API Overview go-yaml provides five main API functions for dumping and loading YAML: | Reader | Writer | Configurable | Use Case | |--------|--------|--------------|----------| | `Load` | `Dump` | Yes | Single or multi-doc with options | | `NewLoader` | `NewDumper` | Yes | Large files, continuous streams | | `Unmarshal` | `Marshal` | No | Quick conversions, preset behavior | | `NewDecoder` | `NewEncoder` | No | Multi-doc streams, preset behavior | ## Configurable API: Dump and Load Like Marshal/Unmarshal but with options support. Perfect for single operations that need custom behavior. ### Dump Dump a single value with v4 defaults (2-space indent, compact): ```go data, err := yaml.Dump(&config) ``` With custom options: ```go data, err := yaml.Dump(&config, yaml.WithIndent(4)) ``` Dump multiple values as a multi-document stream using `WithAllDocuments()`: ```go docs := []Config{config1, config2, config3} data, err := yaml.Dump(docs, yaml.WithAllDocuments(), yaml.WithIndent(2)) // Output: // name: first // --- // name: second // --- // name: third ``` ### Load Load a single document with options: ```go var config Config err := yaml.Load(yamlData, &config, yaml.WithKnownFields()) ``` **Important:** `Load` requires exactly one document by default. Zero documents or multiple documents will return an error. Strict validation catches typos: ```go yamlData := []byte(` name: myapp prto: 8080 # typo! `) var config Config err := yaml.Load(yamlData, &config, yaml.WithKnownFields()) // Error: field prto not found in type Config ``` Load all documents from a multi-document stream using `WithAllDocuments()`: ```go multiDoc := []byte(` name: first --- name: second --- name: third `) var docs []map[string]any err := yaml.Load(multiDoc, &docs, yaml.WithAllDocuments()) for i, doc := range docs { fmt.Printf("Doc %d: %v\n", i, doc) } ``` With typed slices: ```go var configs []Config err := yaml.Load(multiDoc, &configs, yaml.WithAllDocuments()) // Each document decoded as Config ``` **When to use:** Need options but don't need streaming. Config files, API responses, test data. ## Configurable Streaming API: NewDumper and NewLoader Full control over dumping/loading with streaming support and configurable options. Use for large files, network streams, or when you need to process documents one at a time with custom configuration. ### NewDumper Create a dumper that writes to any `io.Writer`: ```go file, _ := os.Create("output.yaml") defer file.Close() dumper, err := yaml.NewDumper(file, yaml.WithIndent(2)) if err != nil { log.Fatal(err) } // Dump multiple documents dumper.Dump(&doc1) dumper.Dump(&doc2) dumper.Dump(&doc3) // Always close to flush remaining output dumper.Close() ``` Write to a buffer: ```go var buf bytes.Buffer dumper, _ := yaml.NewDumper(&buf, yaml.WithIndent(2)) dumper.Dump(&config) dumper.Close() fmt.Println(buf.String()) ``` **Note: `NewDumper` opens a write stream that must be closed with a call to `Close()`. This call can be deferred.** ### NewLoader Create a loader that reads from any `io.Reader`: ```go file, _ := os.Open("config.yaml") defer file.Close() loader, err := yaml.NewLoader(file, yaml.WithKnownFields()) if err != nil { log.Fatal(err) } var config Config if err := loader.Load(&config); err != nil { log.Fatal(err) } ``` Process multi-document streams: ```go loader, _ := yaml.NewLoader(reader) for { var doc map[string]any err := loader.Load(&doc) if err == io.EOF { break } if err != nil { log.Fatal(err) } processDocument(doc) } ``` **When to use:** Large files, network streams, processing documents incrementally, or when you need maximum control. ## Classic API: Marshal and Unmarshal Simple conversion with no options or streaming. **Note:** These functions use **v3 semantics** (4-space indent, non-compact sequences) and **do not accept options**. For new code in v4+, use `Dump` and `Load` instead, which offer the same simplicity with full options control. ### Marshal ```go import "go.yaml.in/yaml/v4" type Config struct { Name string `yaml:"name"` Port int `yaml:"port"` Enabled bool `yaml:"enabled"` } config := Config{Name: "myapp", Port: 8080, Enabled: true} data, err := yaml.Marshal(&config) // Output: // name: myapp // port: 8080 // enabled: true ``` **Equivalent using Dump with v3 semantics to match Marshal:** ```go config := Config{Name: "myapp", Port: 8080, Enabled: true} data, err := yaml.Dump(&config, yaml.WithV3Defaults()) // Same output as Marshal (4-space indent, non-compact) ``` ### Unmarshal ```go yamlData := []byte(` name: myapp port: 8080 enabled: true `) var config Config err := yaml.Unmarshal(yamlData, &config) fmt.Println(config.Name) // "myapp" ``` **Equivalent using Load with v3 semantics to match Unmarshal:** ```go var config Config err := yaml.Load(yamlData, &config, yaml.WithV3Defaults()) fmt.Println(config.Name) // "myapp" ``` **When to use:** Quick scripts, tests, simple config files where default formatting is fine. ## Classic Streaming API: Encoder and Decoder For multi-document streams without needing options. Simple functions for encoding/decoding streams. ### Encode Encode values to an `io.Writer`: ```go file, _ := os.Create("output.yaml") defer file.Close() encoder := yaml.NewEncoder(file) encoder.Encode(&doc1) encoder.Encode(&doc2) encoder.Encode(&doc3) encoder.Close() ``` Each call to `Encode` writes a separate YAML document: ```yaml name: first --- name: second --- name: third ``` **Equivalent using NewDumper with v3 semantics to match Encoder:** ```go file, _ := os.Create("output.yaml") defer file.Close() dumper, _ := yaml.NewDumper(file, yaml.WithV3Defaults()) dumper.Dump(&doc1) dumper.Dump(&doc2) dumper.Dump(&doc3) dumper.Close() // Same output as NewEncoder (4-space indent, non-compact) ``` ### Decode Decode values from an `io.Reader`: ```go file, _ := os.Open("config.yaml") defer file.Close() decoder := yaml.NewDecoder(file) for { var doc map[string]any err := decoder.Decode(&doc) if err == io.EOF { break } if err != nil { log.Fatal(err) } processDocument(doc) } ``` **Equivalent using NewLoader with v3 semantics to match Decoder:** ```go file, _ := os.Open("config.yaml") defer file.Close() loader, _ := yaml.NewLoader(file, yaml.WithV3Defaults()) for { var doc map[string]any err := loader.Load(&doc) if err == io.EOF { break } if err != nil { log.Fatal(err) } processDocument(doc) } ``` **When to use:** Multi-document streams where default formatting is fine. No need for custom indentation or validation options. ## Options System All flexible and streaming APIs accept options. Options are applied left-to-right, with later options overriding earlier ones. ### Individual Options ```go yaml.NewDumper(w, yaml.WithIndent(2), // 2-space indentation yaml.WithCompactSeqIndent(), // Compact list style (defaults to true) yaml.WithLineWidth(120), // Wider lines yaml.WithUnicode(false), // Escape Unicode (override default) ) yaml.NewLoader(r, yaml.WithKnownFields(), // Strict field checking (defaults to true) yaml.WithSingleDocument(), // Only load first doc yaml.WithUniqueKeys(), // Error on duplicate keys (defaults to true) ) ``` ### Version Presets Use preset options that match different go-yaml versions: ```go // v2: 2-space indent, non-compact sequences yaml.Dump(&config, yaml.WithV2Defaults()) // v3: 4-space indent, non-compact sequences yaml.Dump(&config, yaml.WithV3Defaults()) // v4: 2-space indent, compact sequences (default) yaml.Dump(&config, yaml.WithV4Defaults()) // or just yaml.Dump(&config) ``` ### Combining Options Mix presets with individual overrides: ```go // Start with v3, then override indent to 2 yaml.NewDumper(w, yaml.WithV3Defaults(), yaml.WithIndent(2), // This wins ) ``` ### Loading Options from YAML Configure formatting via YAML files: ```go configYAML := ` indent: 3 compact-seq-indent: true known-fields: true ` opts, err := yaml.OptsYAML(configYAML) if err != nil { log.Fatal(err) } data, err := yaml.Dump(&config, opts) ``` See [Options Guide](options.md) for complete option reference. ## Node-Level Load and Dump When working with the Node API directly, you can also use options through the `Load()` and `Dump()` methods. ### Node.Load() - Load with Options The `Node.Load()` method is particularly useful inside custom `UnmarshalYAML` implementations where you need to preserve options like `WithKnownFields()`. ```go type Config struct { Name string `yaml:"name"` Port int `yaml:"port"` } func (c *Config) UnmarshalYAML(node *yaml.Node) error { type plain Config // Use Load to preserve KnownFields option return node.Load((*plain)(c), yaml.WithKnownFields()) } ``` **Solves Issue #460:** Before `node.Load()`, calling `node.Decode()` in custom unmarshalers would create a new decoder without options, losing strict field validation. **Usage example:** ```go yamlData := []byte(` name: myapp port: 8080 unknown: field # This will be caught! `) var config Config err := yaml.Unmarshal(yamlData, &config) if err != nil { fmt.Println("Error:", err) // Error: field unknown not found in type Config } ``` ### Node.Dump() - Dump with Options The `Node.Dump()` method lets you apply dumping options when building Node trees programmatically: ```go config := Config{Name: "myapp", Port: 8080} var node yaml.Node err := node.Dump(&config, yaml.WithIndent(2), yaml.WithExplicitStart(), ) // Now marshal the node to YAML data, _ := yaml.Marshal(&node) fmt.Println(string(data)) // Output: // --- // name: myapp // port: 8080 ``` **When to use:** - Custom unmarshalers that need strict validation (`node.Load()`) - Building Node trees with specific formatting (`node.Dump()`) - Programmatic YAML manipulation with options - Preserving options through the full load/dump pipeline ## Examples ### Example 1: Config File with Validation ```go func LoadConfig(filename string) (*Config, error) { data, err := os.ReadFile(filename) if err != nil { return nil, err } var config Config err = yaml.Load(data, &config, yaml.WithKnownFields(), // Catch typos (defaults to true) ) return &config, err } ``` ### Example 2: Generate CI/CD Config ```go func GeneratePipeline(stages []Stage) ([]byte, error) { return yaml.Dump(&Pipeline{Stages: stages}, yaml.WithIndent(2), // CI systems prefer 2-space ) } ``` ### Example 3: Process Log Stream ```go func ProcessYAMLLogs(reader io.Reader) error { loader, _ := yaml.NewLoader(reader) for { var entry LogEntry err := loader.Load(&entry) if err == io.EOF { return nil } if err != nil { return err } handleLogEntry(entry) } } ``` ### Example 4: Merge Multiple Configs ```go func MergeConfigs(files []string) ([]byte, error) { var allDocs []any for _, f := range files { data, _ := os.ReadFile(f) var docs []any _ = yaml.Load(data, &docs, yaml.WithAllDocuments()) allDocs = append(allDocs, docs...) } return yaml.Dump(allDocs, yaml.WithAllDocuments(), yaml.WithIndent(2)) } ``` ## See Also - [Options Guide](options.md) - Complete option reference - [API Documentation](https://pkg.go.dev/go.yaml.in/yaml/v4) - Full API reference golang-go.yaml-yaml-v4-4.0.0~rc5/docs/dev/go-yaml-internals.md000066400000000000000000001277711521353103300240320ustar00rootroot00000000000000go-yaml Internals ================= This document analyzes the load and dump stacks in the go-yaml implementation, documenting each stage's input/output forms, the transforms performed, and identifying where processing happens in the "wrong places" that could be refactored for cleaner API hooks. ``` LOAD (YAML → Native) DUMP (Native → YAML) ──────────────────── ──────────────────── (Native Value) ←→ (Native Value) ↑ ↓ Constructor Representer ↑ ↓ (Nodes - tagged) ←→ (Nodes - tagged) ↑ ↓ Resolver Desolver ↑ ↓ (Nodes - untagged) ←→ (Nodes - untagged) ↑ ↓ Composer Serializer ↑ ↓ (Events) ←→ (Events) ↑ ↓ Parser Emitter ↑ ↓ (Tokens) ↓ ↑ ↓ Scanner ↓ ↑ ↓ (Code Points) ←→ (Code Points) ↑ ↓ Reader Writer ↑ ↓ (Raw Bytes) ←→ (Raw Bytes) ``` See also: - [Pipeline Overview Diagram](pipeline-overview.mmd) - [Call Hierarchy Diagram](call-hierarchy.mmd) - [Comment Flow Diagram](comment-flow.mmd) ## Load Stack The load stack transforms YAML text into Go values through a series of stages. **Control Flow:** The load stack uses a pull-based call hierarchy. Entry points (such as `Load()`, `Loader.Load()`, or `Node.Load()`) orchestrate the process by creating and coordinating the stages: - **Entry points** create a **Loader** which owns **Composer**, **Resolver**, and **Constructor** - **Entry points** call **Loader.Load()** which executes the 3-stage pipeline: 1. **Composer.Compose()** creates Node tree with unresolved tags (calls Parser for Events) 2. **Resolver.Resolve()** determines implicit types for untagged scalars 3. **Constructor.Construct()** converts Node tree to Go values - **Composer** calls **Parser**.Parse() to get Events (Parser and Scanner share the same struct) - **Parser** calls its own **Scanner** methods (Scan via peekToken) to get Tokens - **Scanner** calls **Reader** functions (updateBuffer) to get more bytes ### Reader Handles input buffering and character encoding detection/conversion. Info: - File: internal/libyaml/reader.go (170 lines) - Main Function: `func (parser *Parser) updateBuffer(length int) error` - Input: `[]byte` or `io.Reader` - Output: UTF-8 normalized bytes in `parser.buffer` - Called From: * Scanner ([`scanner.go`](../../internal/libyaml/scanner.go) / `fetchNextToken()`) - Important Processes: * `reader.go / determineEncoding - Detects UTF-8/UTF-16 from BOM` * `reader.go / updateRawBuffer - Reads more bytes from input source` Transforms: * **Encoding detection from BOM** (UTF-8, UTF-16LE, UTF-16BE) * **UTF-16 to UTF-8 conversion** with surrogate pair handling * **YAML 1.2 character set validation** (Tab, LF, CR, printable ASCII, BMP, supplementary planes) * **UTF-8 sequence validation** * **Input buffering** for lookahead Notes: * Encoding is stored in `parser.encoding` field * The reader is not a separate stage but integrated into the Parser struct * Character validation rules: allowed characters are Tab (0x09), LF (0x0A), CR (0x0D), printable ASCII (0x20-0x7E), BMP (U+0080-U+FFFD excluding surrogates), and supplementary planes (U+10000-U+10FFFF) ### Scanner Lexical analysis - converts raw bytes into tokens. Info: - File: internal/libyaml/scanner.go (2599 lines) - Main Function: `func (parser *Parser) Scan(token *Token) error` - Input: UTF-8 bytes from reader buffer - Output: `Token` struct - Called From: * Parser ([`parser.go`](../../internal/libyaml/parser.go) / `peekToken()`) - Important Processes: * `scanner.go / fetchMoreTokens - Ensures token queue has enough tokens` * `scanner.go / fetchNextToken - Dispatches to token fetchers by character` * `scanner.go / scanComments - Classifies comments as head/foot/line` * `scanner.go / scanLineComment - Captures same-line comments` * `reader.go / updateBuffer - Refills input buffer (calls Reader)` Transforms: * **Character classification and dispatch** ([`fetchNextToken()`](../../internal/libyaml/scanner.go)) * **Flow level tracking** (`flow_level` field) * **Indentation stack management** (`indent`, `indents` fields) * **Simple key candidate tracking** (`simple_keys` stack) * **Synthetic token generation** (`BLOCK_SEQUENCE_START/BLOCK_MAPPING_START/BLOCK_END`) * **Escape sequence processing** in quoted scalars * **Line folding** in folded block scalars * **Chomping behavior** for block scalars (`-` strip, `+` keep) * **URI decoding** in tags * **Comment tokenization** via `scanLineComment()` and `scanComments()` * **Comment classification** (head/foot/line) based on indentation and context Notes: * Scanner and Parser share the same `Parser` struct * Tag handle and suffix remain separate at this stage (`Value` + `suffix` fields) * Scalar style is recorded (Plain, SingleQuoted, DoubleQuoted, Literal, Folded) * 2-token lookahead requirement for comment association (see [Comment Handling](#comment-handling-in-the-load-stack)) * Comment processing details covered in [Comment Handling](#comment-handling-in-the-load-stack) section * Depth limits enforced: `max_flow_level` and `max_indents` both set to 10000 (see [Security Limits](#security-limits-and-protections)) * 23 token types defined: - `NO_TOKEN` - `STREAM_START_TOKEN` - `STREAM_END_TOKEN` - `VERSION_DIRECTIVE_TOKEN` - `TAG_DIRECTIVE_TOKEN` - `DOCUMENT_START_TOKEN` - `DOCUMENT_END_TOKEN` - `BLOCK_SEQUENCE_START_TOKEN` - `BLOCK_MAPPING_START_TOKEN` - `BLOCK_END_TOKEN` - `FLOW_SEQUENCE_START_TOKEN` - `FLOW_SEQUENCE_END_TOKEN` - `FLOW_MAPPING_START_TOKEN` - `FLOW_MAPPING_END_TOKEN` - `BLOCK_ENTRY_TOKEN` - `FLOW_ENTRY_TOKEN` - `KEY_TOKEN` - `VALUE_TOKEN` - `ALIAS_TOKEN` - `ANCHOR_TOKEN` - `TAG_TOKEN` - `SCALAR_TOKEN` - `COMMENT_TOKEN` ### Parser Syntactic analysis - converts token stream into event stream. Info: - File: internal/libyaml/parser.go (1174 lines) - Main Function: `func (parser *Parser) Parse(event *Event) error` - Input: `Token` stream from Scanner (via internal `peekToken()`/`skipToken()`) - Output: `Event` struct - Called From: * Composer ([`composer.go / Composer.peek()`](../../internal/libyaml/composer.go)) * Composer ([`composer.go / Composer.expect()`](../../internal/libyaml/composer.go)) - Important Processes: * `parser.go / stateMachine - Dispatches to parser state handlers` * `parser.go / peekToken - Looks at next token (calls Scanner)` * `parser.go / skipToken - Consumes current token` * `scanner.go / Scan - Gets next token from Scanner (same struct)` * `parser.go / parseStreamStart - Handles stream start event` * `parser.go / parseDocumentStart - Handles document boundaries` * `parser.go / parseNode - Parses scalar/sequence/mapping nodes` * `parser.go / UnfoldComments - Joins comment lines to tokens` * `parser.go / setEventComments - Transfers comments to Event` Transforms: * **LL(1) grammar production matching** (22 parser states): - `PARSE_STREAM_START_STATE` - `PARSE_IMPLICIT_DOCUMENT_START_STATE` - `PARSE_DOCUMENT_START_STATE` - `PARSE_DOCUMENT_CONTENT_STATE` - `PARSE_DOCUMENT_END_STATE` - `PARSE_BLOCK_NODE_STATE` - `PARSE_BLOCK_SEQUENCE_FIRST_ENTRY_STATE` - `PARSE_BLOCK_SEQUENCE_ENTRY_STATE` - `PARSE_INDENTLESS_SEQUENCE_ENTRY_STATE` - `PARSE_BLOCK_MAPPING_FIRST_KEY_STATE` - `PARSE_BLOCK_MAPPING_KEY_STATE` - `PARSE_BLOCK_MAPPING_VALUE_STATE` - `PARSE_FLOW_SEQUENCE_FIRST_ENTRY_STATE` - `PARSE_FLOW_SEQUENCE_ENTRY_STATE` - `PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_KEY_STATE` - `PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_VALUE_STATE` - `PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_END_STATE` - `PARSE_FLOW_MAPPING_FIRST_KEY_STATE` - `PARSE_FLOW_MAPPING_KEY_STATE` - `PARSE_FLOW_MAPPING_VALUE_STATE` - `PARSE_FLOW_MAPPING_EMPTY_VALUE_STATE` - `PARSE_END_STATE` * **Tag handle → full URI resolution** (`!!str` → `tag:yaml.org,2002:str`) * **Token grouping** (anchor + tag + scalar tokens → single `SCALAR_EVENT`) * **Comment attachment to events** via `UnfoldComments()` and `setEventComments()` * **`TAIL_COMMENT_EVENT` generation** for block-end foot comments * **`splitStemComment()` processing** for comments preceding nested structures * **Implicit/`quoted_implicit` flag calculation** * **Block structure tokens → hierarchical event pairs** Notes: * Tag handle/suffix split is lost here - only full URI remains * The `Implicit` flag indicates whether tag was omitted in source * Comment processing details covered in [Comment Handling](#comment-handling-in-the-load-stack) section ### Composer Builds the Node tree from the event stream. Info: - File: internal/libyaml/composer.go (320 lines) - Main Function: `func (c *Composer) Parse() *Node` - Input: `Event` stream from owned Parser (via internal `peek()`/`expect()`) - Output: `*Node` tree - Called From: * Entry point [`yaml.go / unmarshal()`](../../yaml.go) * Entry point [`yaml.go / Decoder.Decode()`](../../yaml.go) * Entry point [`constructor.go / Construct()`](../../internal/libyaml/constructor.go) - Important Processes: * `composer.go / peek - Peeks at next event type (calls Parser)` * `composer.go / expect - Consumes event of expected type (calls Parser)` * `parser.go / Parser.Parse - Gets next event from owned Parser` * `composer.go / node - Creates Node with tag/style` * `composer.go / scalar - Builds ScalarNode from event` * `composer.go / mapping - Builds MappingNode recursively` * `composer.go / sequence - Builds SequenceNode recursively` * `composer.go / document - Builds DocumentNode wrapper` Transforms: * **Event sequence → tree structure** * **Tag short-form normalization** (`tag:yaml.org,2002:str` → `!!str`) * **Anchor registration** in map (`c.anchors`) * **Alias name → pointer to target Node** * **Style flag conversion** (libyaml styles → Node Style flags) * **Comment transfer** from Event to Node * **Comment reassignment logic** in mappings (foot → key, tail → key) Notes: * Byte-level Index is lost here (only Line/Column preserved) * Implicit document distinction is lost * Tag resolution happens in the next stage (Resolver) * Comment reassignment rules detailed in [Comment Handling](#comment-handling-in-the-load-stack) section ### Resolver Resolves tags to determine the type of each scalar value in the node tree. Info: - File: internal/libyaml/resolver.go (300+ lines) - Main Function: `func (r *Resolver) Resolve(node *Node)` - Input: `*Node` tree with unresolved tags - Output: Modified Node tree with resolved tags (in-place) - Called From: * Loader ([`loader.go:267`](../../internal/libyaml/loader.go) / `Loader.Load()`) - Important Processes: * `resolver.go / Resolve - Walks node tree, resolves scalar tags` * `resolver.go / resolveScalar - Infers tag based on content` * `resolver.go / parseTimestamp - Parses ISO 8601 timestamps` * `strconv / ParseInt - Parses signed integers` * `strconv / ParseUint - Parses unsigned integers` * `strconv / ParseFloat - Parses floating point numbers` Transforms: * **Implicit tag resolution** based on scalar content (`true` → `!!bool`, `42` → `!!int`) * **YAML 1.1 compatibility handling** (sexagesimal, old bools) * **Timestamp parsing** * **Special value recognition** (`.nan`, `.inf`, `null`) * **Recursive tree walking** to resolve all scalars in sequences and mappings Notes: * Now a proper stage in the Load pipeline, called once between Composer and Constructor * Modifies the Node tree in-place (sets Tag field on ScalarNodes) * The YAML spec treats Resolver as a distinct stage producing the "Representation Graph" * This refactor eliminates duplicate resolution calls that existed in the old architecture ### Constructor Converts Node tree into Go values. Info: - File: internal/libyaml/constructor.go (1183 lines) - Main Function: `func (c *Constructor) Construct(n *Node, out reflect.Value) bool` - Input: `*Node` tree (received from Composer via entry point) - Output: `reflect.Value` (Go values modified in place) - Called From: * Entry point [`yaml.go / unmarshal()`](../../yaml.go) * Entry point [`yaml.go / Decoder.Decode()`](../../yaml.go) * Entry point [`node.go / Node.Decode()`](../../internal/libyaml/node.go) - Important Processes: * `constructor.go / prepare - Checks for Unmarshaler interface` * `constructor.go / scalar - Converts ScalarNode to Go value` * `constructor.go / mapping - Converts MappingNode to map/struct` * `constructor.go / sequence - Converts SequenceNode to slice` * `constructor.go / document - Unwraps DocumentNode` * `constructor.go / alias - Follows alias pointer, reconstructs` Transforms: * **Custom Unmarshaler interface detection and dispatch** * **Duplicate key detection** (when `UniqueKeys` option enabled) - checks all mapping keys * **Alias expansion ratio protection** (billion laughs defense) - limits constructs from alias expansion * **Self-referential alias detection** - prevents infinite loops from aliases containing themselves * **Tag-based type conversion** using already-resolved tags from Resolver stage * **`indicatedString()` check** for quoted scalars (quoted/literal scalars force string type) * **Merge key (`<<`) handling** - explicit keys take precedence, can merge single mapping or sequence of mappings * **Inline struct/map handling** (`,inline` tag) - one inline map per struct, string keys required, unknown keys go there * **Known fields enforcement** (`WithKnownFields` option) - rejects unknown struct fields when enabled * **Unhashable key error handling** - maps/slices as mapping keys trigger errors * **TextUnmarshaler support** - for scalar types only * **YAML 1.1 boolean compatibility** - `yes/no/on/off` recognized for typed bool targets * **Type coercion** (YAML types → Go types) * **Alias expansion** (full reconstruction each time) * **Binary base64 decoding** (`!!binary` tag) * **Struct field mapping** via `getStructInfo()` Notes: * Now receives fully-resolved tags from the Resolver stage (no duplicate resolution) * Aliases are fully reconstructed each time (not shared) * Duplicate key detection (in [`mapping()`](../../internal/libyaml/constructor.go) function) compares all keys in mappings when enabled * Merge key rules: explicit keys take precedence over merged keys, can merge a single mapping or a sequence of mappings * Inline rules: only one inline map allowed per struct, requires string keys, unknown keys are stored in the inline map field * Indicated strings (quoted or literal style) force string type regardless of content * Node tree, comments, style, anchors, positions are all discarded ## Comment Handling in the Load Stack Comments flow through Scanner → Parser → Composer with classification, attachment, and reassignment at each stage. The goal is to preserve comments and associate them with the appropriate YAML nodes so they can be round-tripped or presented in the Node tree. See also: [Comment Flow Diagram](comment-flow.mmd) ### Comment Types Comments are classified into several types based on their position relative to nodes: | Type | Purpose | Example | |------|---------|---------| | `HeadComment` | Lines preceding a node (no blank line separation) | `# This is a comment\nkey: value` | | `LineComment` | Same line as a node, after its value | `key: value # inline comment` | | `FootComment` | After a node, before any blank lines | `key: value\n# trailing comment` | | `TailComment` | Internal: foot comment at end of block mapping value | Used during parsing only | | `stem_comment` | Internal: comment on entry before nested structure | Used during parsing only | ### Scanner: Comment Tokenization The scanner identifies and classifies comments as it processes tokens: - **`scanLineComment()`** - Captures same-line comments (when no newlines have occurred since the last token) - **`scanComments()`** - Main classifier that determines head/foot/line based on: - Indentation relative to `next_indent` - Flow context (all remaining comments become foot comments in `[...]` or `{...}`) - Empty line boundaries (blank lines separate head from foot) - **2-token lookahead requirement** ([`scanLineComment()`](../../internal/libyaml/scanner.go)) - Scanner needs to peek ahead to properly associate comments - **Special case**: Sequence entry line comments are transformed to head comments Location: [`scanner.go`](../../internal/libyaml/scanner.go) / `scanComments()` and `scanLineComment()` ### Parser: Comment Attachment The parser accumulates comments from tokens and attaches them to events: - **`UnfoldComments()`** - Joins accumulated comment lines to tokens based on position - **Parser comment fields** - `HeadComment`, `LineComment`, `FootComment` accumulate during token processing - **`setEventComments()`** - Transfers parser comment fields to Event struct - **`TAIL_COMMENT_EVENT`** - Special event type for foot comments at block ends (e.g., end of mapping value) - **`splitStemComment()`** - Handles comments on entries preceding nested structures (splits into head comment for the nested item) - **Document header splitting** - HeadComment is split at empty lines; content after blank goes to FootComment Location: [`parser.go`](../../internal/libyaml/parser.go) / `UnfoldComments()`, `setEventComments()`, and `splitStemComment()` ### Composer: Comment Transfer and Reassignment The composer transfers comments from events to nodes and applies reassignment logic: - **Basic transfer**: Event.{Head,Line,Foot}Comment → Node.{Head,Line,Foot}Comment - **Mapping reassignment rules** (in `mapping()` function): 1. **Key FootComment reassignment for dedented comments** - If a comment is dedented (less indented than the key), it moves from key's FootComment to the mapping's FootComment 2. **Value FootComment transfers to Key** - When the value has a FootComment but the key doesn't, the value's FootComment becomes the key's FootComment 3. **TAIL_COMMENT_EVENT FootComment goes to Key** - Tail comments (from block-end events) are assigned to the key's FootComment 4. **Final mapping FootComment moves to last key** - At the end of a mapping, if the mapping has a FootComment, it's moved to the last key's FootComment Location: [`composer.go`](../../internal/libyaml/composer.go) / `mapping()` function ### Edge Cases Several edge cases require special handling: - **Sequence entry line-to-head transformation** - Line comments on sequence entries (`- item # comment`) become head comments of the item - **Block end token skip for head comments** - When `BLOCK_END` tokens have head comments, they're handled specially - **Flow context closure** - When closing `]` or `}` in flow style, all remaining comments become foot comments - **Document header splitting at empty lines** - Head comments with embedded blank lines are split (before blank stays head, after blank becomes foot) ## Security Limits and Protections go-yaml includes several security features to prevent denial-of-service attacks. ### Depth Limits (Scanner) The scanner enforces maximum nesting depth to prevent stack overflow: - `max_flow_level = 10000` - Maximum nesting in flow style `[[[...]]]` or `{{{...}}}` - `max_indents = 10000` - Maximum nesting via indentation (block style) Location: [`scanner.go`](../../internal/libyaml/scanner.go) / flow level and indent tracking ### Alias Expansion Ratio (Constructor) Prevents "billion laughs" style attacks via nested alias expansion: - Documents under 400,000 constructs: allows up to 99% from alias expansion - Documents over 4,000,000 constructs: allows only 10% from alias expansion - Scales smoothly between thresholds Error: `"document contains excessive aliasing"` Location: [`constructor.go`](../../internal/libyaml/constructor.go) / `allowedAliasRatio()` function and expansion check in `sequence()` ### Self-Referential Alias Detection (Constructor) Detects and prevents infinite loops from aliases referencing themselves: - Tracks nodes being expanded in `c.aliases` map - Error: `"anchor '%s' value contains itself"` Location: [`constructor.go`](../../internal/libyaml/constructor.go) / `alias()` function ## Dump Stack The dump stack transforms Go values into YAML text through a symmetric 3-stage pipeline. **Control Flow:** The dump stack uses a push-based call hierarchy. Entry points (such as `Dump()`, `Dumper.Dump()`, or `Node.Dump()`) orchestrate the process by creating and coordinating the stages: - **Entry points** create a **Dumper** which owns **Representer**, **Desolver**, and **Serializer** - **Entry points** call **Dumper.Dump()** which executes the 3-stage pipeline: 1. **Representer.Represent()** converts Go values to tagged Node tree 2. **Desolver.Desolve()** removes inferable tags to minimize output 3. **Serializer.Serialize()** converts Node tree to Events and pushes to Emitter - **Serializer** owns an **Emitter** and calls emit() to push Events - **Emitter** accumulates Events, formats output, and calls **Writer** to flush bytes ### Representer Converts Go values to a tagged Node tree (Stage 1 of Dump pipeline). Info: - File: internal/libyaml/representer.go (600+ lines) - Main Function: `func (r *Representer) Represent(tag string, in reflect.Value) *Node` - Input: `reflect.Value` + `Options` - Output: `*Node` tree with explicit tags - Called From: * Dumper ([`dumper.go:116`](../../internal/libyaml/dumper.go) / `Dumper.Dump()`) - Important Processes: * `representer.go / marshal - Dispatches by Go type` * `representer.go / mapv - Marshals map with sorted keys` * `representer.go / structv - Marshals struct fields` * `representer.go / slicev - Marshals slice/array` * `representer.go / stringv - Marshals string with style choice` * `representer.go / intv - Marshals integer values` * `representer.go / floatv - Marshals floating point values` Transforms: * **Go type dispatch** ([`marshal()`](../../internal/libyaml/representer.go) type switch) * **Custom Marshaler interface detection and dispatch** * **TextMarshaler support** - detects and calls TextMarshaler interface methods * **Struct field ordering and filtering** (exported, by tag, omitempty) * **Map key sorting** (natural sort with numeric awareness) - ensures deterministic output * **Explicit tag assignment** - all nodes get explicit tags (e.g., `!!str`, `!!int`, `!!map`) * **YAML 1.1 compatibility checks** (`isBase60Float()`, `isOldBool()`) * **Style selection** (literal for multiline strings, flow from struct tags) * **Binary data base64 encoding** - non-UTF-8 strings automatically tagged `!!binary` and base64 encoded * **Anchor assignment** for shared/circular references Notes: * Now returns `*Node` instead of emitting events directly * All output nodes have explicit tags - Desolver stage removes inferable ones * Map key sorting ensures deterministic output by using natural sort with numeric awareness * This stage is now symmetric with Constructor on the Load side ### Desolver Removes inferable tags from Node tree (Stage 2 of Dump pipeline, inverse of Resolver). Info: - File: internal/libyaml/desolver.go (150+ lines) - Main Function: `func (d *Desolver) Desolve(node *Node)` - Input: `*Node` tree with explicit tags - Output: Modified Node tree with minimal tags (in-place) - Called From: * Dumper ([`dumper.go:119`](../../internal/libyaml/dumper.go) / `Dumper.Dump()`) - Important Processes: * `desolver.go / Desolve - Walks node tree, removes inferable tags` * `desolver.go / desolveScalar - Checks if tag can be inferred` * `desolver.go / canInferTag - Tests if value would resolve to same tag` Transforms: * **Tag elision** for scalars where tag can be inferred (`!!str "hello"` → `hello`) * **Recursive tree walking** to process all scalars in sequences and mappings * **Preserve explicit tags** when content would be misresolved (e.g., `!!str "42"`) * **YAML 1.1 compatibility awareness** (checks for ambiguous old-style values) Notes: * NEW stage in v4, makes Dump symmetric with Load (mirrors Resolver) * Modifies the Node tree in-place (clears Tag field when inferable) * This is the inverse operation of Resolver - while Resolver adds tags based on content, Desolver removes tags that can be inferred * Results in cleaner YAML output without unnecessary type annotations * Called between Representer and Serializer in the dump pipeline ### Serializer Converts Node tree to events (Stage 3 of Dump pipeline). Info: - File: internal/libyaml/serializer.go (250+ lines) - Main Function: `func (s *Serializer) Serialize(node *Node)` - Input: `*Node` tree with minimal tags (from Desolver) - Output: Events pushed to owned Emitter - Called From: * Dumper ([`dumper.go:122`](../../internal/libyaml/dumper.go) / `Dumper.Dump()`) - Important Processes: * `serializer.go / emit - Sends event to owned Emitter` * `serializer.go / node (recursive) - Walks child nodes` * `serializer.go / isSimpleCollection - Checks if flow style appropriate` * `emitter.go / Emitter.Emit - Emits events (owned Emitter)` Transforms: * **Node tree → event stream** * **Recursive tree walking** to emit events for all nodes * **Flow style detection for simple collections** (`WithFlowSimpleCollections` option) - automatically uses flow style for eligible collections * **Comment placement/shifting** (foot → tail) * **Style flag interpretation** (converts Node.Style to Event.Style) * **Anchor emission** for nodes that need aliasing * **Invalid UTF-8 → base64** conversion for binary data Notes: * Now a proper stage that owns the Emitter (was part of Representer before) * Receives Node tree with tags already optimized by Desolver stage * Simple collection = all scalar children, fits within line width * This stage is now symmetric with Composer on the Load side ### Emitter Converts events to YAML text output. Info: - File: internal/libyaml/emitter.go (2075 lines) - Main Function: `func (emitter *Emitter) Emit(event *Event) error` - Input: `Event` stream (queued in `emitter.events`) - Output: UTF-8 bytes to `emitter.buffer` - Called From: * Representer ([`representer.go / Representer.must()`](../../internal/libyaml/representer.go)) * Representer ([`representer.go / Representer.emit()`](../../internal/libyaml/representer.go)) - Important Processes: * `emitter.go / needMoreEvents - Checks if more events needed for lookahead` * `emitter.go / analyzeEvent - Analyzes scalar/tag for style decisions` * `emitter.go / stateMachine - Dispatches to emitter state handlers` * `emitter.go / selectScalarStyle - Final style selection (can override)` * `emitter.go / writeScalar - Writes scalar with chosen style` * `writer.go / flush - Flushes buffer to output (calls Writer)` Transforms: * **Event accumulation** for lookahead decisions * **Final style selection** (`selectScalarStyle()` can override earlier choices) * **Simple key eligibility check** (length ≤128, no multiline) * **Block vs flow style** (context-dependent) * **Scalar analysis** for style validity (`analyzeScalar()`) * **Line wrapping** at `best_width` * **Indentation management** * **Escape sequence encoding** * **Tag directive shortening** Notes: * Emitter can override style decisions made by Representer/Serializer * Style decisions are split across multiple stages ### Writer Flushes output buffer to destination. Info: - File: internal/libyaml/writer.go (32 lines) - Main Function: `func (emitter *Emitter) flush() error` - Input: `emitter.buffer` - Output: bytes to `write_handler` callback - Called From: * Emitter ([`emitter.go / put()`](../../internal/libyaml/emitter.go)) - Important Processes: * `write_handler callback - Configured output destination` Transforms: * **Buffer flush** to output destination Notes: * Very simple - just calls the configured write handler ## Intermediate Forms The data representations passed between stages. ### Bytes Raw input/output bytes. At input: Raw bytes from file or string, potentially any encoding (UTF-8, UTF-16LE, UTF-16BE). At output: UTF-8 encoded YAML text. ### Token Scanner output - lexical tokens. ```go type Token struct { Type TokenType // SCALAR_TOKEN, ALIAS_TOKEN, TAG_TOKEN, etc. StartMark Mark // Position: Index, Line, Column EndMark Mark encoding Encoding // For STREAM_START_TOKEN only Value []byte // Scalar value, anchor name, or tag handle suffix []byte // Tag suffix (for TAG_TOKEN) prefix []byte // Tag directive prefix Style ScalarStyle // Plain, SingleQuoted, DoubleQuoted, Literal, Folded major, minor int8 // For VERSION_DIRECTIVE_TOKEN } ``` Location: [`yaml.go`](../../internal/libyaml/yaml.go) / `Token` struct definition Notes: * Tag handle and suffix are still separate here * Full byte-level position (Index) is available * For the complete list of 23 token types, see [Scanner Notes](#scanner) ### Event Parser output - syntactic events. ```go type Event struct { Type EventType // SCALAR_EVENT, MAPPING_START_EVENT, etc. StartMark Mark EndMark Mark encoding Encoding versionDirective *VersionDirective tagDirectives []TagDirective HeadComment []byte LineComment []byte FootComment []byte TailComment []byte Anchor []byte Tag []byte // FULL resolved URI (not handle+suffix) Value []byte Implicit bool // Was tag omitted? quoted_implicit bool // Was tag omitted for quoted style? Style Style } ``` Location: [`yaml.go`](../../internal/libyaml/yaml.go) / `Event` struct definition Notes: * Tag is now full URI - handle/suffix split is lost * Comments are attached * 11 event types defined: - `STREAM_START_EVENT` - `STREAM_END_EVENT` - `DOCUMENT_START_EVENT` - `DOCUMENT_END_EVENT` - `ALIAS_EVENT` - `SCALAR_EVENT` - `SEQUENCE_START_EVENT` - `SEQUENCE_END_EVENT` - `MAPPING_START_EVENT` - `MAPPING_END_EVENT` - `TAIL_COMMENT_EVENT` ### Node Composer output - tree structure. ```go type Node struct { Kind Kind // DocumentNode, SequenceNode, MappingNode, ScalarNode, AliasNode Style Style // TaggedStyle, DoubleQuotedStyle, LiteralStyle, FlowStyle, etc. Tag string // SHORT form tag (!!str, !!int, etc.) Value string // Scalar content (unescaped, unquoted) Anchor string Alias *Node // Points to target Node for AliasNode Content []*Node // Children for Document/Sequence/Mapping HeadComment string LineComment string FootComment string Line, Column int // NOTE: Byte Index is lost // StreamNode-specific: Encoding Encoding Version *StreamVersionDirective TagDirectives []StreamTagDirective } ``` Location: [`node.go`](../../internal/libyaml/node.go) / `Node` struct definition Notes: * Tag is short form - full URI is lost * Byte Index is lost - only Line/Column remain * 6 node kinds: - `DocumentNode` - `SequenceNode` - `MappingNode` - `ScalarNode` - `AliasNode` - `StreamNode` * 6 style flags: - `TaggedStyle` - `DoubleQuotedStyle` - `SingleQuotedStyle` - `LiteralStyle` - `FoldedStyle` - `FlowStyle` * `indicatedString()` method determines if a scalar skips tag resolution (quoted or literal style) * `shouldUseLiteralStyle()` heuristic for multi-line strings decides between literal block style and quoted style ### Repr (Representation Graph) The Node tree after tag resolution. In the YAML specification, the Representation Graph is a distinct stage where all tags have been fully resolved. In go-yaml's implementation, this is still represented using Node structures with resolved tags. The "Repr" is conceptual - there's no separate struct. The transformation from Nodes to Repr happens when `resolve()` is called to determine implicit tags. ### Native Value Go language values: structs, maps, slices, strings, ints, etc. This is the final output of the Load stack and the input to the Dump stack. Represented as `reflect.Value` internally. ## Potential Problems and Inconsistencies ### 1. ~~resolve() Called in Four Places~~ FIXED ✓ **Status:** This problem has been resolved in the current refactor. **Old Problem:** The `resolve()` function was called from 4 different places: - Composer (to set Node.Tag) - Constructor (to get typed value) - duplicate work - Representer (to check if quoting needed) - Serializer (to check if tag can be elided) **Solution Implemented:** - **Load Stack:** Resolver is now a proper stage with `Resolve()` method, called once in `Loader.Load()` between Composer and Constructor - **Dump Stack:** Desolver is a new stage with `Desolve()` method, called once in `Dumper.Dump()` between Representer and Serializer - **Result:** Both stacks now have clean 3-stage pipelines with symmetric tag resolution/desolving stages The pipelines are now: - **Load:** Composer → Resolver → Constructor - **Dump:** Representer → Desolver → Serializer ### 2. Style Decisions Split Across Stages **Load:** Scanner → Parser → Composer → Constructor (each touches style) **Dump:** Representer → Serializer → Emitter (each can override style) **Problem:** Hard to control or predict final output style. Emitter can override everything. **Better:** Make style decisions once and respect them, or clearly separate "hints" from "requirements". ### 3. Information Lost at Each Stage | Information | Lost At | Impact | |-------------|---------|--------| | Tag handle/suffix split | Parser | Can't reconstruct `!custom!type` | | Byte-level Index | Composer | Less precise error positions | | Implicit document flag | Composer | Can't round-trip explicit `---` | | Full tag URI | Composer | Can't round-trip verbatim tags | ### 4. YAML 1.1 Compatibility Scattered These checks appear in multiple files: - `isBase60Float()` - representer.go, resolver.go - `isOldBool()` - representer.go, resolver.go - YAML 1.1 bool values (y/Y/yes/Yes/YES) - resolver.go **Problem:** No single configuration point for YAML version semantics. ### 5. ~~Representer Has Parser Logic~~ FIXED ✓ **Status:** This problem has been resolved in the current refactor. **Old Problem:** The Representer called `resolve()` to check if strings would be misresolved - this was parser-era logic living in the dump stack. **Solution Implemented:** The Desolver stage now handles tag removal using the same resolution logic, but as a proper inverse operation of the Resolver. The Representer no longer needs to call resolution functions - it simply creates a fully-tagged Node tree, and the Desolver removes inferable tags in a separate stage. **Result:** Clean separation of concerns - Representer focuses on Go→Node conversion, Desolver handles tag optimization. ### 6. Scanner and Parser Share Struct Both stages use the `Parser` struct, making it hard to: - Expose tokens separately from events - Have clean API boundaries between stages ### 7. v4 vs Legacy Default Differences Different entry points use different default settings, which can cause inconsistent behavior: | Setting | v4 Defaults | Legacy Defaults | |---------|-------------|-----------------| | Indent | 2 | 4 | | CompactSeqIndent | true | false | | LineWidth | 80 | -1 (unlimited) | `Load/Dump` use v4 defaults; `Marshal/Unmarshal` use Legacy defaults. Location: [`options.go`](../../internal/libyaml/options.go) / `Options` struct v4 defaults and `LegacyOptions` variable **Problem:** Users may see different formatting depending on which API they use, even with identical data. ## Anything Else I Missed? Areas that may need deeper investigation: - Anchor/alias reconstruction behavior - Error propagation and position reporting - The relationship between root package types and internal/libyaml types ## Glossary ### A **Alias** - A YAML reference to a previously defined anchor, allowing reuse of content. Represented with `*name` syntax in YAML. In go-yaml, AliasNode points to the target Node. **Anchor** - A named marker (`&name`) that identifies a YAML node for later reference via aliases. Stored as a string in Node/Event/Token structs. ### B **Billion Laughs Attack** - A denial-of-service attack using nested aliases to cause exponential expansion. go-yaml protects against this with alias expansion ratio limits. **Block Style** - YAML's indentation-based syntax for collections and scalars. Examples: indented mappings/sequences, literal blocks (`|`), folded blocks (`>`). **BOM (Byte Order Mark)** - A special Unicode character at the start of a file indicating encoding. go-yaml detects UTF-8, UTF-16LE, and UTF-16BE from BOM. ### C **Chomping** - Controls how trailing newlines are handled in block scalars. Strip (`-`) removes them, keep (`+`) preserves them, clip (default) keeps one newline. **Composer** - Load stack stage that builds Node trees from Event streams. Handles tag normalization, anchor registration, and comment transfer. **Constructor** - Load stack stage that converts Node trees to native Go values. Performs type coercion, handles custom Unmarshalers, and enforces constraints. ### D **Document** - A single YAML data structure within a stream. Can be explicit (starts with `---`) or implicit. Represented as DocumentNode in go-yaml. **Dump Stack** - The pipeline that transforms Go values into YAML text: Representer → Serializer → Emitter → Writer. ### E **Emitter** - Dump stack stage that converts Events to UTF-8 YAML text. Handles final style selection, line wrapping, and indentation. **Event** - Parser output representing syntactic structure. 11 types including `SCALAR_EVENT`, `MAPPING_START_EVENT`, `SEQUENCE_END_EVENT`. Contains tag, value, comments, and position. ### F **Flow Style** - YAML's compact JSON-like syntax using brackets `[]` and braces `{}`. Example: `{key: value}` or `[a, b, c]`. **Folded Block Scalar** - Multi-line string (`>`) where newlines are converted to spaces, except for blank lines and more-indented lines. **FootComment** - A comment appearing after a node but before any blank lines. Can be reassigned to keys in mappings. ### H **HeadComment** - Comments appearing before a node with no blank line separation. ### I **Implicit Tag** - A tag inferred by the resolver based on scalar content rather than explicitly specified. Example: `42` → `!!int`. **Indicated String** - A scalar that has quoted or literal style, indicating it should be treated as a string regardless of content. Skips tag resolution. **Indentless Sequence** - A YAML sequence where entries are indicated by `-` but not further indented relative to the parent. Used in some mapping values. **Inline Struct** - A struct field tagged with `,inline` that merges its fields into the parent struct during marshaling/unmarshaling. ### L **Literal Block Scalar** - Multi-line string (`|`) where newlines are preserved exactly as written. **Load Stack** - The pipeline that transforms YAML text into Go values: Reader → Scanner → Parser → Composer → Resolver → Constructor. ### M **Mapping** - YAML's key-value structure (like a map or dictionary). Represented as MappingNode in go-yaml. **Marshaling** - The process of converting Go values to YAML text (Dump stack). **Merge Key** - Special YAML key (`<<`) that merges content from another mapping or sequence of mappings. Explicit keys take precedence. ### N **Node** - Tree-based representation of YAML structure. 6 kinds: DocumentNode, SequenceNode, MappingNode, ScalarNode, AliasNode, StreamNode. ### P **Parser** - Load stack stage that converts Tokens to Events using LL(1) grammar. Implements 22 parser states and handles comment attachment. **Pull-Based** - Architecture where higher stages request data from lower stages. Used in Load stack (Constructor pulls from Composer, which pulls from Parser, etc.). **Push-Based** - Architecture where lower stages push data to higher stages. Used in Dump stack (Representer pushes Events to Emitter). ### R **Reader** - Load stack component that handles encoding detection, UTF-8 conversion, and input buffering. Not a separate stage but integrated into Parser struct. **Representer** - Dump stack stage that converts Go values to Events. Handles type dispatch, field filtering, key sorting, and style selection. **Representation Graph** - In YAML spec, the data structure after tag resolution. In go-yaml, conceptually the Node tree with resolved tags. **Resolver** - Function (not a separate stage) that infers tags from scalar content. Called from multiple places in both Load and Dump stacks. ### S **Scalar** - YAML's atomic value type (string, number, boolean, null). Represented as ScalarNode with a style (plain, quoted, literal, folded). **Scanner** - Load stack stage performing lexical analysis. Converts UTF-8 bytes to Tokens, handling indentation tracking, flow level tracking, and comment tokenization. **Self-Referential Alias** - An alias that references itself directly or indirectly, causing infinite loops. go-yaml detects and prevents this. **Sequence** - YAML's ordered list structure (like an array). Represented as SequenceNode in go-yaml. **Serializer** - Dump stack stage that converts Node trees to Events. Handles tag elision checks and flow style detection. **Simple Collection** - A sequence or mapping containing only scalar children that fits within line width. Eligible for automatic flow style. **Simple Key** - A mapping key that is short enough (≤128 characters) and single-line. Flow mappings require simple keys. **Stream** - Top-level container for YAML documents. A stream can contain multiple documents separated by `---`. Represented as StreamNode in go-yaml. **Surrogate Pair** - UTF-16 encoding mechanism for characters outside the Basic Multilingual Plane. go-yaml handles these during UTF-16 to UTF-8 conversion. ### T **Tag** - Type indicator for YAML nodes. Short form (`!!str`, `!!int`) or full URI (`tag:yaml.org,2002:str`). Can be explicit or implicit. **Tag Directive** - YAML directive (`%TAG`) that defines a short handle for tag URIs. Example: `%TAG ! tag:yaml.org,2002:`. **Tag Elision** - Omitting an explicit tag when it can be inferred. Serializer checks if tags can be elided during dump. **Tag Handle** - Short prefix for tags (like `!!` or `!custom!`). Stored separately from suffix in Token but merged in Event. **TAIL_COMMENT_EVENT** - Special event type for foot comments at block structure ends. Used during parsing to properly assign comments to mapping keys. **TextMarshaler/TextUnmarshaler** - Go interfaces for custom text encoding/decoding. Supported by Representer (marshaling) and Constructor (unmarshaling, scalars only). **Token** - Scanner output representing lexical units. 23 types including `SCALAR_TOKEN`, `BLOCK_MAPPING_START_TOKEN`, `TAG_TOKEN`. Contains byte-level position (Index). ### U **Unmarshaling** - The process of converting YAML text to Go values (Load stack). **UniqueKeys** - Option that enables duplicate key detection in mappings. Enabled by default in v2, v3, and v4. ### V **Version Directive** - YAML directive (`%YAML 1.2`) specifying the YAML version. Stored in StreamNode. ### W **Writer** - Dump stack component that flushes output buffer to destination. Very simple - just calls configured write handler. golang-go.yaml-yaml-v4-4.0.0~rc5/docs/dev/how-go-yaml-works.md000066400000000000000000000270231521353103300237600ustar00rootroot00000000000000# How go-yaml Works This document explains the internal architecture of YAML processing in go-yaml. Understanding this helps you debug issues, contribute to the project, and appreciate why YAML loading isn't as simple as "parsing." ## Common Misconceptions ### "Loading" is not "Parsing" Many people incorrectly refer to YAML loading as "parsing," and some implementations even name their load function `parse()`. This is technically wrong and obscures what's really happening. **Parsing** is just one stage in a multi-stage pipeline. A parser applies grammar rules to a token stream. **Loading** encompasses the entire transformation from YAML bytes to native language values, which involves many more steps than just parsing. ### YAML Processing is a Pipeline YAML processing isn't a single monolithic operation. Both loading and dumping are **pipelines of transforms**, where each stage: - Has a single, well-defined responsibility - Consumes input in one representation - Produces output in a different representation - Can be inspected and debugged independently The two user-facing functions in go-yaml are `Load()` and `Dump()`, but these are just the entry points to much deeper pipelines. ## The Big Picture: Paired Pipelines Load and Dump are mirror-image stacks of transforms with mostly matching stages. Data flows through different representations at each stage: ``` LOAD (YAML → Native) DUMP (Native → YAML) ──────────────────── ──────────────────── (Native Value) ←→ (Native Value) ↑ ↓ Constructor Representer ↑ ↓ (Nodes) (Nodes - tagged) ↑ ↓ Resolver Desolver ↑ ↓ (Nodes - unresolved) (Nodes - minimal tags) ↑ ↓ Composer Serializer ↑ ↓ (Events) ←→ (Events) ↑ ↓ Parser ↓ ↑ ↓ (Tokens) ↓ ↑ ↓ Scanner Emitter ↑ ↓ (Code Points) ←→ (Code Points) ↑ ↓ Reader Writer ↑ ↓ (Raw Bytes) ←→ (Raw Bytes) ``` **Stack Symmetry Improvements** The v4 refactor has made the stacks more symmetric: 1. **3-Stage Pipelines**: Both Load and Dump now have clean 3-stage pipelines: - **Load:** Composer → Resolver → Constructor - **Dump:** Representer → Desolver → Serializer 2. **Symmetric Tag Handling**: - **Resolver** (Load) infers tags from content - **Desolver** (Dump) removes inferable tags - NEW in v4 3. **Node Trees on Both Sides**: Both stacks work with Node trees as intermediate representation 4. **Remaining Asymmetry**: Scanner+Parser on Load vs single Emitter on Dump (due to YAML's complex indentation-based syntax) ## Data Representations Each stage consumes and produces data in specific representations: ### Raw Bytes The actual file contents or byte stream. No interpretation has been done yet. ### Code Points Unicode characters after encoding detection and conversion. The Reader handles UTF-8, UTF-16LE, and UTF-16BE encoding. ### Tokens Lexical units produced by the Scanner. Examples: `BLOCK_MAPPING_START_TOKEN`, `SCALAR_TOKEN`, `KEY_TOKEN`, `VALUE_TOKEN`, `ANCHOR_TOKEN`. Tokens have no nested structure — they're a flat stream that describes YAML syntax at the character level (indentation, indicators, scalars). ### Events Structural units produced by the Parser. Examples: `MAPPING_START_EVENT`, `MAPPING_END_EVENT`, `SCALAR_EVENT`, `ALIAS_EVENT`. Events represent the grammar-level structure of YAML. The Parser validates that tokens conform to YAML grammar rules and produces a cleaner event stream. ### Nodes The tree structure built by the Composer. Each Node has a kind (Document, Mapping, Sequence, Scalar, Alias), value, tag, style, and position. At this stage, anchors are resolved to build the graph structure, but tags are still in their raw form (`!!str`, `!!int`, or implicit). ### Repr (Representation Graph) The node tree after the Resolver has processed tags. Tags are resolved according to YAML tag resolution rules (implicit typing, tag directives, etc.). In go-yaml's implementation, this is still represented using Node structures, but with all tags fully resolved. The "Repr" is a conceptual stage from the YAML specification. ### Native Value Go language values: structs, maps, slices, strings, ints, etc. This is what application code works with. ## Loading Pipeline Stages ### 1. Reader (Raw Bytes → Code Points) **File**: `internal/libyaml/reader.go` The Reader handles input encoding: - Detects encoding (UTF-8, UTF-16LE, UTF-16BE) via BOM or heuristics - Converts bytes to Unicode code points - Buffers input for efficient scanning This stage ensures the Scanner works with a consistent Unicode stream regardless of input encoding. ### 2. Scanner (Code Points → Tokens) **File**: `internal/libyaml/scanner.go` The Scanner performs lexical analysis: - Tracks indentation levels - Identifies block vs. flow context - Detects simple keys (for compact mappings like `key: value`) - Produces a stream of tokens This is the most complex stage because YAML's indentation-based syntax requires careful context tracking. **Example tokens** for `foo: bar`: - `STREAM_START_TOKEN` - `BLOCK_MAPPING_START_TOKEN` - `KEY_TOKEN` - `SCALAR_TOKEN` (value: "foo") - `VALUE_TOKEN` - `SCALAR_TOKEN` (value: "bar") - `BLOCK_END_TOKEN` - `STREAM_END_TOKEN` ### 3. Parser (Tokens → Events) **File**: `internal/libyaml/parser.go` The Parser applies YAML grammar: - Consumes the token stream - Validates structure according to YAML grammar rules - Produces a cleaner event stream **This is what "parsing" actually means** — applying grammar rules to a token stream. **Example events** for `foo: bar`: - `STREAM_START_EVENT` - `DOCUMENT_START_EVENT` - `MAPPING_START_EVENT` - `SCALAR_EVENT` (value: "foo") - `SCALAR_EVENT` (value: "bar") - `MAPPING_END_EVENT` - `DOCUMENT_END_EVENT` - `STREAM_END_EVENT` ### 4. Composer (Events → Nodes) **File**: `internal/libyaml/composer.go` The Composer builds the node tree: - Creates Document, Mapping, Sequence, and Scalar nodes - Registers anchors and resolves aliases to build the graph structure - Handles multi-document streams - Attaches comments to nodes The output is a tree (or graph, with aliases) of Node objects. ### 5. Resolver (Nodes → Repr) **File**: `internal/libyaml/resolver.go` The Resolver handles tag resolution: - Determines implicit tags based on scalar content (e.g., `true` → `!!bool`, `42` → `!!int`, `foo` → `!!str`) - Processes explicit tags (e.g., `!!str 42` forces string type) - Applies tag directives from document headers - Produces the Representation Graph In go-yaml's implementation, the Repr is the node tree with fully resolved tags. ### 6. Constructor (Repr → Native Values) **File**: `internal/libyaml/constructor.go` The Constructor converts YAML to Go: - Maps YAML types to Go types (`!!str` → string, `!!seq` → slice) - Handles struct field mapping via reflection - Calls custom `UnmarshalYAML` methods when defined - Supports `encoding.TextUnmarshaler` interface - Tracks alias depth for security This is where YAML becomes usable Go data structures. ## Dumping Pipeline Stages ### 1. Representer (Native Values → Tagged Nodes) **File**: `internal/libyaml/representer.go` The Representer converts Go values to a tagged Node tree: - Handles basic types (maps, structs, slices, strings, numbers, bools) - Calls custom `MarshalYAML` methods when defined - Supports `encoding.TextMarshaler` interface - Makes style decisions (literal vs. quoted scalars, flow vs. block collections) - Processes struct tags (`yaml:"name,omitempty,flow"`) - Assigns explicit tags to all nodes (`!!str`, `!!int`, `!!map`, etc.) - **Returns a Node tree** (no longer emits events directly) ### 2. Desolver (Tagged Nodes → Minimal Nodes) **File**: `internal/libyaml/desolver.go` The Desolver removes inferable tags (NEW in v4, inverse of Resolver): - Walks the tagged node tree from Representer - Removes tags that can be inferred during parsing - Preserves explicit tags when content would be misresolved - Produces cleaner YAML output without unnecessary type annotations - **Example:** `!!str "hello"` becomes `hello`, but `!!str "42"` keeps the tag ### 3. Serializer (Nodes → Events) **File**: `internal/libyaml/serializer.go` The Serializer linearizes the node tree: - Walks the node tree depth-first - Produces a stream of events - Handles anchor assignment for sharing/circular references - Determines whether collections should use flow style - Pushes events to the Emitter ### 4. Emitter (Events → Code Points) **File**: `internal/libyaml/emitter.go` The Emitter generates formatted YAML: - Converts events to YAML text - Handles indentation and line wrapping - Chooses between different scalar styles (plain, quoted, literal, folded) - Produces Unicode code points - Supports canonical output mode This stage combines the work that Scanner+Parser do on the Load side. ### 5. Writer (Code Points → Raw Bytes) **File**: `internal/libyaml/writer.go` The Writer handles output: - Converts Unicode code points to bytes - Handles output encoding - Buffers writes for efficiency - Writes to the output stream ## Why This Architecture Matters ### Single Responsibility Each stage has one job and does it well. This makes the code easier to understand, test, and maintain. ### Debuggability You can inspect intermediate representations at any stage. The `go-yaml` CLI tool leverages this to show tokens, events, and nodes. ### Extensibility A plugin system allows hooking into any stage to customize behavior. Want custom tag resolution? Hook the Resolver. Want custom output formatting? Hook the Emitter. ### Spec Compliance The architecture follows the YAML specification's terminology and processing model. This makes go-yaml easier to understand for anyone familiar with the YAML spec. ## Debugging with the go-yaml CLI Tool The `go-yaml` command-line tool can show you each stage of processing: ```bash # Show tokens go-yaml -t <<< 'foo: bar' # Show events go-yaml -e <<< 'foo: bar' # Show node tree go-yaml -n <<< 'foo: bar' ``` This is invaluable for understanding what's happening at each stage and debugging parsing issues. See the [main README](../README.md#the-go-yaml-cli-tool) for more details on using the CLI tool. ## Summary YAML processing is a pipeline, not a single operation: - **Loading** flows through: Reader → Scanner → Parser → Composer → Resolver → Constructor - **Dumping** flows through: Representer → Desolver → Serializer → Emitter → Writer - Each stage transforms data from one representation to another - The v4 refactor has made the stacks more symmetric with 3-stage pipelines: - **Load:** Composer → Resolver → Constructor - **Dump:** Representer → Desolver → Serializer - "Parsing" is just one stage (tokens → events), not the whole process Understanding these stages helps you work with YAML more effectively, debug issues faster, and appreciate the complexity hidden behind the simple `yaml.Load()` and `yaml.Dump()` functions. golang-go.yaml-yaml-v4-4.0.0~rc5/docs/dev/pipeline-overview.mmd000066400000000000000000000036041521353103300243020ustar00rootroot00000000000000--- title: go-yaml Processing Pipeline Overview --- flowchart TB subgraph LOAD ["LOAD STACK (YAML to Go)"] direction TB L_bytes[/Raw Bytes/] L_reader[Reader] L_codepoints[/Code Points UTF-8/] L_scanner[Scanner] L_tokens[/Tokens/] L_parser[Parser] L_events[/Events/] L_composer[Composer] L_nodes_raw[/Nodes - untagged/] L_resolver[Resolver] L_nodes_tagged[/Nodes - tagged/] L_constructor[Constructor] L_native[/Native Go Values/] L_bytes --> L_reader L_reader --> L_codepoints L_codepoints --> L_scanner L_scanner --> L_tokens L_tokens --> L_parser L_parser --> L_events L_events --> L_composer L_composer --> L_nodes_raw L_nodes_raw --> L_resolver L_resolver --> L_nodes_tagged L_nodes_tagged --> L_constructor L_constructor --> L_native end subgraph DUMP ["DUMP STACK (Go to YAML)"] direction TB D_native[/Native Go Values/] D_representer[Representer] D_nodes_tagged[/Nodes - tagged/] D_desolver[Desolver] D_nodes_minimal[/Nodes - minimal tags/] D_serializer[Serializer] D_events[/Events/] D_emitter[Emitter] D_codepoints[/Code Points UTF-8/] D_writer[Writer] D_bytes[/Raw Bytes/] D_native --> D_representer D_representer --> D_nodes_tagged D_nodes_tagged --> D_desolver D_desolver --> D_nodes_minimal D_nodes_minimal --> D_serializer D_serializer --> D_events D_events --> D_emitter D_emitter --> D_codepoints D_codepoints --> D_writer D_writer --> D_bytes end L_native <--> D_native L_nodes_tagged <-.-> D_nodes_tagged L_events <-.-> D_events L_codepoints <-.-> D_codepoints L_bytes <-.-> D_bytes golang-go.yaml-yaml-v4-4.0.0~rc5/docs/options.md000066400000000000000000000404221521353103300213700ustar00rootroot00000000000000# YAML Options Guide This guide explains how to use the go-yaml options system to control YAML formatting and behavior. ## Quick Start The simplest way to use go-yaml is with the default settings: ```go import "go.yaml.in/yaml/v4" // Uses v4 defaults (2-space indent, compact sequences) dumper, _ := yaml.NewDumper(writer) dumper.Dump(&myData) dumper.Close() ``` But what if you want 4-space indentation like v3? Or strict field checking? That's where options come in. ## Individual Options **Note:** All options work with both package-level functions (`Load`, `Dump`, `NewLoader`, `NewDumper`) and node-level methods (`node.Load()`, `node.Dump()`). You can tweak specific settings with individual option functions: ```go dumper, _ := yaml.NewDumper(writer, yaml.WithIndent(4), // 4-space indentation (v3 style) yaml.WithCompactSeqIndent(false), // Non-compact lists (v3 style) ) ``` ### Available Options #### Dumper (Encoding) Options **Boolean Options:** All boolean options support variadic arguments. Call without arguments to enable (defaults to `true`), or pass `false` to explicitly disable: ```go yaml.WithExplicitStart() // Enable (same as WithExplicitStart(true)) yaml.WithExplicitStart(false) // Explicitly disable ``` ##### `yaml.WithIndent(spaces int)` Controls how many spaces to use for indentation. ```go // 2-space indent (compact) yaml.NewDumper(w, yaml.WithIndent(2)) // 4-space indent (readable) yaml.NewDumper(w, yaml.WithIndent(4)) // 8-space indent (very spacious) yaml.NewDumper(w, yaml.WithIndent(8)) ``` **Example output with different indents:** ```yaml # WithIndent(2) servers: web: host: localhost port: 8080 # WithIndent(4) servers: web: host: localhost port: 8080 # WithIndent(8) servers: web: host: localhost port: 8080 ``` ##### `yaml.WithCompactSeqIndent(...bool)` Controls whether the list indicator `- ` counts as part of the indentation. ```go // Enable compact sequences (short form) yaml.NewDumper(w, yaml.WithIndent(4), yaml.WithCompactSeqIndent(), ) ``` **Example:** ```yaml # compact=true (v4 default) items: - name: first value: 1 - name: second value: 2 # compact=false (v3 style) items: - name: first value: 1 - name: second value: 2 ``` ##### `yaml.WithLineWidth(width int)` Sets the preferred line width for wrapping long strings. Use -1 or 0 for unlimited width. ```go // Wrap at 40 characters yaml.NewDumper(w, yaml.WithLineWidth(40)) // Unlimited width (no wrapping) yaml.NewDumper(w, yaml.WithLineWidth(-1)) ``` **Example:** ```yaml # LineWidth=40 description: | This is a long description that wraps to multiple lines. # LineWidth=-1 description: This is a long description that stays on one line ``` **Default:** 80 characters ##### `yaml.WithUnicode(...bool)` Controls whether non-ASCII characters appear as-is or are escaped. ```go // Allow unicode (default) yaml.NewDumper(w, yaml.WithUnicode(true)) // Escape non-ASCII characters yaml.NewDumper(w, yaml.WithUnicode(false)) ``` **Example:** ```yaml # WithUnicode(true) - default name: café # WithUnicode(false) name: "caf\u00e9" ``` **Use case:** Set to false for ASCII-only output required by legacy systems. **Default:** true ##### `yaml.WithCanonical(...bool)` Forces strictly canonical YAML output with explicit tags. Primarily for debugging and spec testing. ```go yaml.NewDumper(w, yaml.WithCanonical(true)) ``` **Example:** ```yaml # Normal output name: John age: 30 # Canonical output !!map { ? !!str "name" : !!str "John", ? !!str "age" : !!int "30", } ``` **Default:** false ##### `yaml.WithLineBreak(lineBreak yaml.LineBreak)` Sets the line ending style. Available options: `yaml.LineBreakLN` (Unix), `yaml.LineBreakCR` (old Mac), `yaml.LineBreakCRLN` (Windows). ```go // Unix line endings (default) yaml.NewDumper(w, yaml.WithLineBreak(yaml.LineBreakLN)) // Windows line endings yaml.NewDumper(w, yaml.WithLineBreak(yaml.LineBreakCRLN)) ``` **Default:** `yaml.LineBreakLN` (Unix `\n`) ##### `yaml.WithExplicitStart(...bool)` Controls whether document start markers (`---`) are always emitted. ```go // Always emit --- at document start (short form) yaml.NewDumper(w, yaml.WithExplicitStart()) ``` **Example:** ```yaml # WithExplicitStart(true) --- name: test # WithExplicitStart(false) - default name: test ``` **Use case:** Multi-document streams, explicit document boundaries. **Default:** false ##### `yaml.WithExplicitEnd(...bool)` Controls whether document end markers (`...`) are always emitted. ```go // Always emit ... at document end (short form) yaml.NewDumper(w, yaml.WithExplicitEnd()) ``` **Example:** ```yaml # WithExplicitEnd(true) name: test ... # WithExplicitEnd(false) - default name: test ``` **Use case:** Streaming scenarios where document end must be explicit. **Default:** false ##### `yaml.WithFlowSimpleCollections(...bool)` Controls whether simple collections use flow style. Simple collections are sequences and mappings that: - Contain only scalar values (no nested collections) - Fit within the line width when rendered in flow style ```go // Use flow style for simple collections (short form) yaml.NewDumper(w, yaml.WithFlowSimpleCollections()) ``` **Example:** ```yaml # WithFlowSimpleCollections(true) config: tags: [web, api, prod] metadata: {version: 1.0, author: admin} nested: # Not simple - has nested collections items: - name: foo # WithFlowSimpleCollections(false) - default config: tags: - web - api - prod metadata: version: 1.0 author: admin ``` **Use case:** Compact output for simple data structures, JSON-like formatting. **Default:** false ##### `yaml.WithQuotePreference(style yaml.QuoteStyle)` Controls which type of quotes to use when quoting is required by the YAML spec. **Important:** This option only affects strings that *require* quoting. Plain strings that don't need quoting remain unquoted regardless of this setting. Quoting is required for: - Strings that look like other YAML types (true, false, null, 123, etc.) - Strings with leading/trailing whitespace - Strings containing special YAML syntax characters - Empty strings in certain contexts ```go // Use single quotes when quoting is required (v4 default) yaml.NewDumper(w, yaml.WithQuotePreference(yaml.QuoteSingle)) // Use double quotes when quoting is required yaml.NewDumper(w, yaml.WithQuotePreference(yaml.QuoteDouble)) // Use legacy v2/v3 behavior (mixed quoting) yaml.NewDumper(w, yaml.WithQuotePreference(yaml.QuoteLegacy)) ``` **Example:** ```yaml # WithQuotePreference(yaml.QuoteSingle) - v4 default plain: hello # Plain string stays plain bool: 'true' # Looks like bool, gets single quotes number: '123' # Looks like number, gets single quotes spaces: ' hello' # Leading space, gets single quotes # WithQuotePreference(yaml.QuoteDouble) plain: hello # Plain string stays plain bool: "true" # Looks like bool, gets double quotes number: "123" # Looks like number, gets double quotes spaces: " hello" # Leading space, gets double quotes # WithQuotePreference(yaml.QuoteLegacy) - v2/v3 behavior plain: hello # Plain string stays plain bool: "true" # Looks like bool, gets double quotes number: "123" # Looks like number, gets double quotes spaces: ' hello' # Leading space, gets single quotes ``` **Use case:** - `QuoteSingle`: Modern YAML style with single quotes (cleaner, less escaping) - `QuoteDouble`: When you prefer double quotes or need consistency with JSON-style - `QuoteLegacy`: Backward compatibility with go-yaml v2/v3 output **Default:** - v4: `QuoteSingle` - v2/v3: `QuoteLegacy` #### Loader (Decoding) Options **Boolean Options:** All boolean options support variadic arguments. Call without arguments to enable (defaults to `true`), or pass `false` to explicitly disable: ```go yaml.WithKnownFields() // Enable (same as WithKnownFields(true)) yaml.WithKnownFields(false) // Explicitly disable ``` ##### `yaml.WithKnownFields(...bool)` When enabled, loading will fail if the YAML contains fields that don't exist in your struct. ```go type Config struct { Name string `yaml:"name"` Port int `yaml:"port"` } // Enable strict field checking (short form) loader, _ := yaml.NewLoader(reader, yaml.WithKnownFields(), ) // This will fail if YAML contains "unknown_field" var config Config err := loader.Load(&config) ``` **Great for:** Catching typos in config files, enforcing strict schemas. ##### `yaml.WithSingleDocument(...bool)` Only load the first YAML document, then return EOF. ```go // Enable single document mode (short form) loader, _ := yaml.NewLoader(reader, yaml.WithSingleDocument(), ) var doc1 Doc loader.Load(&doc1) // Loads first document var doc2 Doc loader.Load(&doc2) // Returns io.EOF // Dynamic based on environment/config singleDoc := os.Getenv("SINGLE_DOC") == "true" loader, _ := yaml.NewLoader(reader, yaml.WithSingleDocument(singleDoc), ) ``` **Use case:** When you expect exactly one document and want to catch extras. **Default:** false (multi-document mode) ##### `yaml.WithUniqueKeys(...bool)` Controls duplicate key detection in mappings. When enabled, loading fails if duplicate keys are found. ```go // Enforce unique keys (default - short form) loader, _ := yaml.NewLoader(reader, yaml.WithUniqueKeys(), ) // Allow duplicates (not recommended) loader, _ := yaml.NewLoader(reader, yaml.WithUniqueKeys(false), ) ``` **Example YAML that would fail:** ```yaml admin: false admin: true # Error: duplicate key ``` **Security note:** This prevents key override attacks where an attacker could override security-critical keys. **Default:** true (enabled) ## Version-Specific Option Presets Instead of setting options one by one, you can use version presets that match go-yaml v2, v3, or v4 behavior. ### What's the difference? | Option | v2 | v3 | v4 | |-----------------------|---------|---------|--------| | Indent | 2 | 4 | 2 | | Compact Sequences | No | No | Yes | | Line Width | 80 | 80 | 80 | | Unicode | Yes | Yes | Yes | | Unique Keys | Yes | Yes | Yes | | Quote Preference | Legacy | Legacy | Single | **Note:** v4 uses compact sequences (modern YAML standard) while v2/v3 preserve the historical non-compact behavior for backward compatibility. ### Using v2 Options ```go dumper, _ := yaml.NewDumper(writer, yaml.WithV2Defaults()) ``` **When to use:** Matching go-yaml v2 output format (2-space indent). ### Using v3 Options ```go dumper, _ := yaml.NewDumper(writer, yaml.WithV3Defaults()) ``` **When to use:** Explicitly requesting v3 behavior (4-space indent, non-compact sequences). Use this for compatibility with older go-yaml v3 output or when working with code that expects v3 formatting. ### Using v4 Options (Default) ```go dumper, _ := yaml.NewDumper(writer, yaml.WithV4Defaults()) ``` **When to use:** Modern YAML output with 2-space indent and compact sequences. This is the default, so you usually don't need to specify `yaml.WithV4Defaults()` unless you want to be explicit. ## Mixing Presets with Individual Options You can start with a preset and override specific options. **Options apply left-to-right**, so later options override earlier ones. ```go // Start with v3 defaults (4-space), then override to 2-space dumper, _ := yaml.NewDumper(writer, yaml.WithV3Defaults(), yaml.WithIndent(2), // This wins ) ``` **More examples:** ```go // Default (v4) + override to 4-space indent dumper, _ := yaml.NewDumper(writer, yaml.WithIndent(4), // Overrides default 2-space ) // Start with 2-space, then apply v3 (4-space wins) dumper, _ := yaml.NewDumper(writer, yaml.WithIndent(2), yaml.WithV3Defaults(), // This overrides to 4 ) ``` ## Loading Options from YAML Config You can load your YAML processing options from a YAML configuration file! This is great for making formatting configurable. ```go configYAML := ` indent: 3 compact-seq-indent: true known-fields: true ` opts, err := yaml.OptsYAML(configYAML) if err != nil { log.Fatal(err) } dumper, _ := yaml.NewDumper(writer, opts) ``` **The YAML field names are:** - `indent` - Number of spaces - `compact-seq-indent` - Boolean - `line-width` - Integer - `unicode` - Boolean - `canonical` - Boolean - `line-break` - String (ln, cr, crln) - `explicit-start` - Boolean - `explicit-end` - Boolean - `flow-simple-coll` - Boolean - `quote-preference` - String (single, double, legacy) - `known-fields` - Boolean - `single-document` - Boolean - `unique-keys` - Boolean **Any field not in your config uses the version's defaults.** ## Real-World Examples ### Example 1: Strict Config File Loader ```go type AppConfig struct { Database DatabaseConfig `yaml:"database"` Server ServerConfig `yaml:"server"` } func LoadConfig(filename string) (*AppConfig, error) { f, err := os.Open(filename) if err != nil { return nil, err } defer f.Close() loader, err := yaml.NewLoader(f, yaml.WithV4Defaults(), yaml.WithKnownFields(), // Catch typos (defaults to true) yaml.WithSingleDocument(), // Expect exactly one doc ) if err != nil { return nil, err } var config AppConfig if err := loader.Load(&config); err != nil { return nil, err } return &config, nil } ``` ### Example 2: Matching External Tool Format Your CI system expects 2-space indented YAML: ```go func GenerateCI(config *CIConfig) ([]byte, error) { var buf bytes.Buffer dumper, err := yaml.NewDumper(&buf, yaml.WithV2Defaults(), ) if err != nil { return nil, err } if err := dumper.Dump(config); err != nil { return nil, err } dumper.Close() return buf.Bytes(), nil } ``` ### Example 3: User-Configurable Formatter ```go func FormatYAML(input []byte, userPrefs string) ([]byte, error) { // Load user's formatting preferences opts, err := yaml.OptsYAML(userPrefs) if err != nil { return nil, err } // Parse input var node yaml.Node if err := yaml.Load(input, &node); err != nil { return nil, err } // Reformat with user preferences var buf bytes.Buffer dumper, _ := yaml.NewDumper(&buf, opts) dumper.Dump(&node) dumper.Close() return buf.Bytes(), nil } ``` ### Example 4: Multi-Document Stream with Options ```go func ProcessMultiDocs(reader io.Reader) error { loader, err := yaml.NewLoader(reader, yaml.WithKnownFields(false), // Allow unknown fields (override default) ) if err != nil { return err } for { var doc Document err := loader.Load(&doc) if err == io.EOF { break } if err != nil { return err } // Process document... processDocument(&doc) } return nil } ``` ## Common Patterns ### Pattern: Application Config Loader ```go loader, _ := yaml.NewLoader(configFile, yaml.WithSingleDocument(), // Only one config file yaml.WithKnownFields(), // Strict validation (defaults to true) ) ``` ### Pattern: Default YAML Output ```go dumper, _ := yaml.NewDumper(output) // Uses v4 defaults (2-space, compact) ``` ### Pattern: Match Legacy Format ```go dumper, _ := yaml.NewDumper(output, yaml.WithV3Defaults()) // 4-space like old go-yaml ``` ## Tips & Tricks **Default is v4:** `yaml.NewDumper(w)` uses v4 defaults (2-space indent, compact sequences). **Order matters:** Options are applied left-to-right, with later options overriding earlier ones. For example, `yaml.WithIndent(2), yaml.WithV3Defaults()` gives you 4 spaces (V3 wins), while `yaml.WithV3Defaults(), yaml.WithIndent(2)` gives you 2 spaces. **Load from files:** Use `yaml.OptsYAML()` to make formatting configurable by users. **Validation:** Use `WithKnownFields()` to catch config file typos early. **One document:** Use `WithSingleDocument()` when you expect exactly one YAML document. **Test your options:** Run examples with different options to see the output format. ## See Also - [Dumping and Loading API Guide](dump-load-api.md) - Complete guide to Dump/Load APIs - [API Documentation](https://pkg.go.dev/go.yaml.in/yaml/v4) - Full API reference - [Examples](../example/README.md) - Runnable code examples golang-go.yaml-yaml-v4-4.0.0~rc5/docs/plugins.md000066400000000000000000000062741521353103300213650ustar00rootroot00000000000000# Plugin System The go-yaml v4 plugin system extends YAML processing with custom logic while maintaining performance, safety and backward compatibility. ## Overview Plugins allow you to customize certain internal processing during loading and dumping. Plugin interfaces use public types and can be implemented by external packages. ## Available Plugins ### Limit Plugin The limit plugin controls the maximum nesting depth and alias expansion allowed during parsing. By default, go-yaml enforces conservative limits to prevent DoS attacks. Use the limit plugin to relax or tighten those limits. ```go import "go.yaml.in/yaml/v4/plugin/limit" // Default limits (same as library defaults) loader := yaml.NewLoader(data, yaml.WithPlugin(limit.New())) // Disable alias checking (e.g. for documents with many programmatic aliases) loader := yaml.NewLoader(data, yaml.WithPlugin(limit.New(limit.AliasNone()))) // Custom depth limit loader := yaml.NewLoader(data, yaml.WithPlugin(limit.New(limit.DepthValue(50)))) ``` #### Limit Options | Option | Effect | |---|---| | `DepthValue(n)` | Max nesting depth (both flow and block) | | `DepthNone()` | Disable depth checking | | `DepthFunc(fn)` | Custom `func(depth int, ctx *yaml.DepthContext) error` | | `AliasValue(n)` | Max alias expansion count (simple threshold) | | `AliasNone()` | Disable alias ratio checking | | `AliasFunc(fn)` | Custom `func(aliasCount, constructCount int) error` | ## Using Plugins ### Basic Usage Register plugins with `WithPlugin()`: ```go import ( "go.yaml.in/yaml/v4" "go.yaml.in/yaml/v4/plugin/limit" ) loader := yaml.NewLoader(data, yaml.WithPlugin(limit.New(limit.AliasNone()))) var result any loader.Load(&result) ``` ## Default Behavior Both bare `NewLoader(data)` and version presets (`WithV4Defaults()`, etc.) include default limits equivalent to `limit.New()`. ## YAML Configuration Plugins can be configured from YAML strings using `OptsYAML`: ```go opts, err := yaml.OptsYAML(` plugin: limit: depth: 50 alias: 1000 `) ``` Each plugin key maps to a configuration object. For the limit plugin: - `depth` (int) — max nesting depth; `null` disables depth checking - `alias` (int) — max alias count; `null` disables alias checking - Omitted keys keep defaults - Bare `limit:` (null value) uses all defaults ```yaml # Disable depth checking, keep default alias limits plugin: limit: depth: null ``` ## Third-Party Plugins To write a third-party plugin, implement the `yaml.LimitPlugin` interface: ```go type LimitPlugin interface { CheckDepth(depth int, ctx *DepthContext) error CheckAlias(aliasCount, constructCount int) error } ``` Pass an instance to `yaml.WithPlugin()` — no import of `plugin/limit` is needed. Example: ```go type StrictLimit struct{} func (s *StrictLimit) CheckDepth(depth int, ctx *yaml.DepthContext) error { if depth > 100 { return fmt.Errorf("depth %d exceeds policy limit of 100", depth) } return nil } func (s *StrictLimit) CheckAlias(aliasCount, constructCount int) error { if aliasCount > 1000 { return fmt.Errorf("alias count %d exceeds policy limit", aliasCount) } return nil } yaml.NewLoader(data, yaml.WithPlugin(&StrictLimit{})) ``` golang-go.yaml-yaml-v4-4.0.0~rc5/docs/v3-to-v4-migration.md000066400000000000000000000160471521353103300231710ustar00rootroot00000000000000# Migrating from v3 to v4 This guide will help you migrate your code from `go.yaml.in/yaml/v3` (or `gopkg.in/yaml.v3`) to `go.yaml.in/yaml/v4`. ## Quick Migration Checklist - [ ] Update import path - [ ] Optionally migrate to new API (Load/Dump, Loader/Dumper) - [ ] Adjust formatting expectations or use yaml.WithV3Defaults() preset - [ ] Update tests ## Import Path Change **v3:** ```go import "gopkg.in/yaml.v3" // or import "go.yaml.in/yaml/v3" ``` **v4:** ```go import "go.yaml.in/yaml/v4" ``` Update all import statements throughout your codebase. ## API Changes ### Recommended: Use New API v4 introduces a cleaner API with better naming. #### Loading YAML **v3:** ```go err := yaml.Unmarshal(data, &config) ``` **v4:** ```go err := yaml.Load(data, &config) ``` #### Dumping YAML **v3:** ```go data, err := yaml.Marshal(&config) ``` **v4:** ```go data, err := yaml.Dump(&config) ``` #### Streaming Decoding **v3:** ```go decoder := yaml.NewDecoder(reader) err := decoder.Decode(&config) ``` **v4:** ```go loader := yaml.NewLoader(reader) err := loader.Load(&config) ``` #### Streaming Encoding **v3:** ```go encoder := yaml.NewEncoder(writer) err := encoder.Encode(&config) encoder.Close() ``` **v4:** ```go dumper := yaml.NewDumper(writer) err := dumper.Dump(&config) dumper.Close() ``` ## New Features in v4 ### Stream Nodes v4 adds `StreamNode`, a new node kind that exposes stream-level metadata not available in v3: encoding, `%YAML` version directives, and `%TAG` directives. This is accessed via `Node.Stream`, which is non-nil only on `StreamNode` nodes. Enable stream nodes with `WithStreamNodes()`: ```go loader := yaml.NewLoader(reader, yaml.WithStreamNodes()) for { var node yaml.Node err := loader.Load(&node) if errors.Is(err, io.EOF) { break } if node.Kind == yaml.StreamNode && node.Stream != nil { enc := node.Stream.Encoding ver := node.Stream.Version // *yaml.VersionDirective tags := node.Stream.TagDirectives // []yaml.TagDirective } } ``` With stream nodes enabled, the loader emits nodes in the pattern `[Stream, Doc, Stream, Doc, ..., Stream]` — one opening stream node per document boundary, plus a final closing stream node. ### Functional Options v4 introduces a functional options pattern for configuration: ```go // Version presets yaml.Dump(&data, yaml.WithV2Defaults()) // Use v2 defaults yaml.Dump(&data, yaml.WithV3Defaults()) // Use v3 defaults yaml.Dump(&data, yaml.WithV4Defaults()) // Use v4 defaults (2-space, compact) // Custom options yaml.Dump(&data, yaml.WithIndent(4), yaml.WithCompactSeqIndent(false), yaml.WithLineWidth(100), ) // Combine presets with overrides yaml.Dump(&data, yaml.WithV3Defaults(), yaml.WithIndent(2)) // Loading options yaml.Load(data, &config, yaml.WithKnownFields(), // Strict field checking yaml.WithUniqueKeys(), // Enforce unique keys ) ``` Available dump options: - `WithIndent(n)` - Set indentation spaces - `WithCompactSeqIndent(bool)` - Compact sequence indentation - `WithLineWidth(n)` - Maximum line width - `WithUnicode(bool)` - Use Unicode characters - `WithCanonical(bool)` - Canonical output format - `WithLineBreak(lb)` - Line break style (LN, CR, CRLN) - `WithExplicitStart(bool)` - Add `---` document start - `WithExplicitEnd(bool)` - Add `...` document end - `WithFlowSimpleCollections(bool)` - Use flow style for simple collections Available load options: - `WithKnownFields()` - Reject unknown struct fields - `WithUniqueKeys()` - Enforce unique mapping keys - `WithSingleDocument()` - Expect only one document ### Options from YAML You can configure options using YAML: ```go optsYAML := ` indent: 4 compact-seq-indent: false line-width: 100 ` opts, err := yaml.OptsYAML(optsYAML) if err != nil { log.Fatal(err) } data, err := yaml.Dump(&config, opts) ``` ## Formatting Differences v4 has different default formatting than v3: | Aspect | v3 Default | v4 Default | |--------|-----------|-----------| | Indentation | 4 spaces | 2 spaces | | Sequence style | Normal | Compact | **Example:** ```yaml # v3 default output items: - name: foo value: 1 - name: bar value: 2 # v4 default output items: - name: foo value: 1 - name: bar value: 2 ``` ### Preserving v3 Behavior If you need v3's formatting, use the `yaml.WithV3Defaults()` preset: ```go // Get v3-style formatting in v4 data, err := yaml.Dump(&config, yaml.WithV3Defaults()) ``` Or customize individual options: ```go data, err := yaml.Dump(&config, yaml.WithIndent(4), yaml.WithCompactSeqIndent(false), ) ``` ## Backward Compatibility All v3 APIs continue to work in v4. The classic API remains supported for simple use cases: - `Unmarshal()` - Classic API (or use `Load()` for more flexibility) - `Marshal()` - Classic API (or use `Dump()` for more flexibility) - `NewDecoder()` - Classic API (or use `NewLoader()` for more flexibility) - `NewEncoder()` - Classic API (or use `NewDumper()` for more flexibility) You can migrate incrementally: 1. Update import path to v4 2. Verify tests pass 3. Optionally migrate to new API for additional features 4. Update TypeError.Errors handling if applicable ## Migration Strategies ### Strategy 1: Minimal Change (Fastest) 1. Update import path 2. If using TypeError.Errors directly, update that code 3. Add `yaml.WithV3Defaults()` preset to maintain v3 formatting 4. Done! ```go // Only change needed for basic migration data, err := yaml.Dump(&config, yaml.WithV3Defaults()) ``` ### Strategy 2: Adopt New API (Recommended) 1. Update import path 2. Replace `Unmarshal` → `Load`, `Marshal` → `Dump` 3. Replace `NewDecoder` → `NewLoader`, `NewEncoder` → `NewDumper` 4. Test with v4 defaults or choose formatting explicitly ### Strategy 3: Feature Adoption (Maximum Value) 1. Follow Strategy 2 2. Explore new functional options 3. Leverage structured error information 4. Adopt version presets for different use cases ## Testing Your Migration ```bash # Run your existing tests go test ./... # Verify YAML output formatting # Use the go-yaml CLI tool to compare go install go.yaml.in/yaml/v4/cmd/go-yaml@latest ./go-yaml -n < testfile.yaml ``` ## Common Issues ### Issue: Output formatting changed **Solution:** Use `yaml.WithV3Defaults()` preset to maintain v3 formatting: ```go yaml.Dump(&data, yaml.WithV3Defaults()) ``` ### Issue: Want more flexibility from classic API **Solution:** Migrate to the new API for options support: - `Unmarshal` → `Load` - `Marshal` → `Dump` - `NewDecoder` → `NewLoader` - `NewEncoder` → `NewDumper` ## Getting Help If you encounter issues during migration: - Check the [API documentation](https://pkg.go.dev/go.yaml.in/yaml/v4) - Browse [examples](example/) - Open an [issue](https://github.com/yaml/go-yaml/issues) - Ask in [Slack](https://cloud-native.slack.com/archives/C08PPAT8PS7) ## Next Steps - Explore the new [functional options](#functional-options) - Review the [examples](example/) directory - Read the [full API documentation]( https://pkg.go.dev/go.yaml.in/yaml/v4) - Try the [go-yaml CLI tool](README.md#the-go-yaml-cli-tool) for debugging golang-go.yaml-yaml-v4-4.0.0~rc5/example/000077500000000000000000000000001521353103300200545ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc5/example/README.md000066400000000000000000000075151521353103300213430ustar00rootroot00000000000000# go-yaml Examples This directory contains examples demonstrating various features of the go-yaml library. ## Running Examples Each example can be run directly: ```bash go run example/basic_loader.go ``` Or from within the example directory: ```bash cd example go run basic_loader.go ``` ## Basic Examples ### Loading YAML **`basic_loader.go`** - Simple YAML loading with Loader - Creates a Loader from a reader - Loads YAML into Go structs - Basic unmarshaling example **`load_into_node.go`** - Load YAML into Node representation - Uses Node type for low-level YAML access - Demonstrates working with the YAML AST **`multi_document_loader.go`** - Load multiple YAML documents - Processes multi-document streams - Loops through documents until EOF **`single_document_loader.go`** - Load only first document - Uses WithSingleDocument option - Subsequent Load() calls return EOF ### Dumping YAML **`basic_dumper.go`** - Simple YAML dumping with Dumper - Creates a Dumper to a writer - Dumps Go structs to YAML **`multi_document_dumper.go`** - Dump multiple YAML documents - Creates a multi-document stream - Multiple Dump() calls separated by `---` **`dumper_with_indent.go`** - Custom indentation - Uses WithIndent option - Demonstrates formatting control **`dumper_indent_comparison.go`** - Compare different indent levels - Shows output with 2, 4, and 8-space indentation ## Options System Examples **`with_v4_option.go`** - Using v4 option presets - Demonstrates yaml.WithV4Defaults() - Shows v4 defaults (2-space indent) - Compares with default (v3) output **`with_v4_override.go`** - Overriding option presets - Combines version presets with individual options - Demonstrates left-to-right option application - Shows how later options override earlier ones **`multiple_options_loader.go`** - Combining multiple options - Uses WithSingleDocument and WithKnownFields together - Demonstrates strict field checking - Shows error handling for unknown fields ## Node-Level API Examples **`node_load_strict_unmarshaler/main.go`** - Strict field checking in custom unmarshalers - Solves Issue #460 - preserving options in custom UnmarshalYAML - Uses node.Load() with WithKnownFields() - Demonstrates proper error handling for unknown fields **`node_dump_with_options/main.go`** - Encoding nodes with different options - Shows node.Dump() with various formatting options - Compares default, v3, and custom indent styles - Demonstrates explicit start markers **`node_load_decode_comparison/main.go`** - Compare Decode() vs Load() - Side-by-side comparison of old and new approaches - Shows how node.Decode() loses options - Demonstrates why node.Load() solves Issue #460 **`node_programmatic_build/main.go`** - Build YAML programmatically - Build complex Node structures with options - Shows round-trip (Dump then Load) with options - Demonstrates node manipulation workflow ## Complete Demo **`loader_dumper_demo.go`** - Comprehensive feature demonstration - Covers Loader, Dumper, and options - Multiple examples in one file - Good overview of library capabilities ## Common Patterns ### Basic Load and Dump ```go import "go.yaml.in/yaml/v4" // Load var config Config yaml.Load(yamlData, &config) // Dump data, _ := yaml.Dump(&config) ``` ### Streaming with Options ```go // Load with options loader, _ := yaml.NewLoader(reader, yaml.WithKnownFields(), yaml.WithSingleDocument(), ) loader.Load(&config) // Dump with options dumper, _ := yaml.NewDumper(writer, yaml.WithIndent(2), ) dumper.Dump(&config) dumper.Close() ``` ### Using Version Presets ```go dumper, _ := yaml.NewDumper(writer, yaml.WithV4Defaults(), ) ``` ## Learn More - See the [main package documentation](https://pkg.go.dev/go.yaml.in/yaml/v4) for API reference - Run `make doc-serve` from the project root to view local documentation - Check individual example source code for detailed comments golang-go.yaml-yaml-v4-4.0.0~rc5/example/basic_dumper/000077500000000000000000000000001521353103300225115ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc5/example/basic_dumper/main.go000066400000000000000000000014141521353103300237640ustar00rootroot00000000000000// Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Example: Basic Dumper demonstrates simple YAML dumping from structs. package main import ( "bytes" "fmt" "go.yaml.in/yaml/v4" ) type Config struct { Name string `yaml:"name"` Version string `yaml:"version"` Tags []string `yaml:"tags,omitempty"` } func main() { fmt.Println("Example 4: Basic Dumper - Single Document") var buf bytes.Buffer dumper, err := yaml.NewDumper(&buf) if err != nil { panic(err) } cfg := Config{ Name: "service1", Version: "1.0.0", Tags: []string{"prod"}, } if err := dumper.Dump(&cfg); err != nil { panic(err) } if err := dumper.Close(); err != nil { panic(err) } fmt.Printf("Output:\n%s", buf.String()) } golang-go.yaml-yaml-v4-4.0.0~rc5/example/basic_loader/000077500000000000000000000000001521353103300224635ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc5/example/basic_loader/main.go000066400000000000000000000013051521353103300237350ustar00rootroot00000000000000// Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Example: Basic Loader demonstrates simple YAML loading into structs. package main import ( "fmt" "strings" "go.yaml.in/yaml/v4" ) type Config struct { Name string `yaml:"name"` Version string `yaml:"version"` Tags []string `yaml:"tags,omitempty"` } func main() { fmt.Println("Example 1: Basic Loader - Single Document") yamlData := `name: myapp version: 1.0.0 tags: - web - api ` var cfg Config loader, err := yaml.NewLoader(strings.NewReader(yamlData)) if err != nil { panic(err) } if err := loader.Load(&cfg); err != nil { panic(err) } fmt.Printf("Loaded: %+v\n", cfg) } golang-go.yaml-yaml-v4-4.0.0~rc5/example/dumper_indent_comparison/000077500000000000000000000000001521353103300251435ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc5/example/dumper_indent_comparison/main.go000066400000000000000000000053331521353103300264220ustar00rootroot00000000000000// Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Example: Dumper Indent Comparison shows different indentation options. package main import ( "bytes" "fmt" "os" "strconv" "go.yaml.in/yaml/v4" ) type Config struct { Name string `yaml:"name"` Version string `yaml:"version"` Server ServerConfig `yaml:"server"` Tags []string `yaml:"tags,omitempty"` Metadata map[string]string `yaml:"metadata,omitempty"` } type ServerConfig struct { Host string `yaml:"host"` Port int `yaml:"port"` Debug bool `yaml:"debug"` } func main() { cfg := Config{ Name: "myapp", Version: "1.0.0", Server: ServerConfig{ Host: "localhost", Port: 8080, Debug: true, }, Tags: []string{"web", "api", "production"}, Metadata: map[string]string{ "owner": "platform-team", "env": "prod", }, } // Check if indent level was provided as command-line argument if len(os.Args) > 1 { indent, err := strconv.Atoi(os.Args[1]) if err != nil { fmt.Fprintf(os.Stderr, "Error: invalid indent value %q (must be a number)\n", os.Args[1]) os.Exit(1) } if indent < 2 || indent > 9 { fmt.Fprintf(os.Stderr, "Error: indent value must be between 2 and 9 (got %d)\n", indent) os.Exit(1) } fmt.Printf("Example: Dumper with %d-space indent\n\n", indent) var buf bytes.Buffer dumper, err := yaml.NewDumper(&buf, yaml.WithIndent(indent)) if err != nil { panic(err) } if err := dumper.Dump(&cfg); err != nil { panic(err) } if err := dumper.Close(); err != nil { panic(err) } fmt.Print(buf.String()) return } // No argument provided - show comparison of different indent levels fmt.Println("Example: Dumper with Different Indent Levels") fmt.Println("(Run with argument to test specific indent: go run dumper_indent_comparison.go 3)") indentLevels := []int{2, 4, 8} for _, indent := range indentLevels { fmt.Printf("--- Indent: %d spaces ---\n", indent) var buf bytes.Buffer dumper, err := yaml.NewDumper(&buf, yaml.WithIndent(indent)) if err != nil { panic(err) } if err := dumper.Dump(&cfg); err != nil { panic(err) } if err := dumper.Close(); err != nil { panic(err) } fmt.Print(buf.String()) fmt.Println() } // Also show default indent (no option) fmt.Println("--- Default indent (no option) ---") var buf bytes.Buffer dumper, err := yaml.NewDumper(&buf) if err != nil { panic(err) } if err := dumper.Dump(&cfg); err != nil { panic(err) } if err := dumper.Close(); err != nil { panic(err) } fmt.Print(buf.String()) fmt.Println("\nTip: Run with a specific indent value as an argument:") fmt.Println(" go run example/dumper_indent_comparison.go 3") } golang-go.yaml-yaml-v4-4.0.0~rc5/example/dumper_with_indent/000077500000000000000000000000001521353103300237445ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc5/example/dumper_with_indent/main.go000066400000000000000000000014671521353103300252270ustar00rootroot00000000000000// Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Example: Dumper With Indent demonstrates custom indentation settings. package main import ( "bytes" "fmt" "go.yaml.in/yaml/v4" ) type Config struct { Name string `yaml:"name"` Version string `yaml:"version"` Tags []string `yaml:"tags,omitempty"` } func main() { fmt.Println("Example 6: Dumper with WithIndent Option") var buf bytes.Buffer dumper, err := yaml.NewDumper(&buf, yaml.WithIndent(4)) if err != nil { panic(err) } cfg := Config{ Name: "service", Version: "1.0.0", Tags: []string{"a", "b", "c"}, } if err := dumper.Dump(&cfg); err != nil { panic(err) } if err := dumper.Close(); err != nil { panic(err) } fmt.Printf("Output (4-space indent):\n%s", buf.String()) } golang-go.yaml-yaml-v4-4.0.0~rc5/example/load_into_node/000077500000000000000000000000001521353103300230315ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc5/example/load_into_node/main.go000066400000000000000000000054131521353103300243070ustar00rootroot00000000000000// Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Example: Load Into Node demonstrates loading YAML into Node structures. package main import ( "fmt" "strings" "go.yaml.in/yaml/v4" ) func main() { fmt.Println("Example: Load into yaml.Node") yamlData := `# Application configuration name: myapp # The app name version: 1.0.0 # Server settings server: host: localhost port: 8080 # Enable debug mode debug: true # List of enabled features features: - auth - logging - metrics ` loader, err := yaml.NewLoader(strings.NewReader(yamlData)) if err != nil { panic(err) } var node yaml.Node if err := loader.Load(&node); err != nil { panic(err) } fmt.Println("Successfully loaded YAML into Node") fmt.Printf("Root node kind: %v\n", node.Kind) fmt.Printf("Root has %d child nodes (the document)\n\n", len(node.Content)) // The first content is the document node doc := node.Content[0] fmt.Printf("Document node kind: %v\n", doc.Kind) fmt.Printf("Document has %d children (key-value pairs)\n\n", len(doc.Content)) // Walk through the top-level keys fmt.Println("Top-level keys with their values:") for i := 0; i < len(doc.Content); i += 2 { key := doc.Content[i] value := doc.Content[i+1] fmt.Printf("\nKey: %q\n", key.Value) if key.HeadComment != "" { fmt.Printf(" Head comment: %q\n", key.HeadComment) } if key.LineComment != "" { fmt.Printf(" Line comment: %q\n", key.LineComment) } switch value.Kind { case yaml.ScalarNode: fmt.Printf(" Value (scalar): %q\n", value.Value) if value.LineComment != "" { fmt.Printf(" Value line comment: %q\n", value.LineComment) } case yaml.MappingNode: fmt.Printf(" Value (mapping): %d key-value pairs\n", len(value.Content)/2) // Show nested keys for j := 0; j < len(value.Content); j += 2 { nestedKey := value.Content[j] nestedValue := value.Content[j+1] fmt.Printf(" %s: %s", nestedKey.Value, nestedValue.Value) if nestedValue.LineComment != "" { fmt.Printf(" %s", nestedValue.LineComment) } fmt.Println() } case yaml.SequenceNode: fmt.Printf(" Value (sequence): %d items\n", len(value.Content)) for j, item := range value.Content { fmt.Printf(" [%d]: %s\n", j, item.Value) } } } // Demonstrate modifying the node fmt.Println("\n--- Modifying Node ---") // Add a new field programmatically newKey := &yaml.Node{ Kind: yaml.ScalarNode, Value: "environment", } newValue := &yaml.Node{ Kind: yaml.ScalarNode, Value: "production", } doc.Content = append(doc.Content, newKey, newValue) // Re-dump to see the modified YAML out, err := yaml.Dump(&node) if err != nil { panic(err) } fmt.Println("\nModified YAML (added 'environment' field):") fmt.Print(string(out)) } golang-go.yaml-yaml-v4-4.0.0~rc5/example/loader_dumper_demo/000077500000000000000000000000001521353103300237025ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc5/example/loader_dumper_demo/main.go000066400000000000000000000065271521353103300251670ustar00rootroot00000000000000// Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Example: Loader Dumper Demo combines loading and dumping operations. package main import ( "bytes" "fmt" "io" "strings" "go.yaml.in/yaml/v4" ) type Config struct { Name string `yaml:"name"` Version string `yaml:"version"` Tags []string `yaml:"tags,omitempty"` } func main() { fmt.Println("=== YAML Loader/Dumper Demo ===") // Example 1: Basic Loader usage with single document fmt.Println("1. Basic Loader - Single Document:") singleDoc := `name: myapp version: 1.0.0 tags: - web - api ` var cfg Config loader, err := yaml.NewLoader(strings.NewReader(singleDoc)) if err != nil { panic(err) } if err := loader.Load(&cfg); err != nil { panic(err) } fmt.Printf(" Loaded: %+v\n\n", cfg) // Example 2: Multi-document stream fmt.Println("2. Multi-Document Loader:") multiDoc := `--- name: app1 version: 1.0.0 --- name: app2 version: 2.0.0 tags: - experimental --- name: app3 version: 3.0.0 ` loader2, err := yaml.NewLoader(strings.NewReader(multiDoc)) if err != nil { panic(err) } docNum := 1 for { var doc Config err := loader2.Load(&doc) if err == io.EOF { break } if err != nil { panic(err) } fmt.Printf(" Document %d: %+v\n", docNum, doc) docNum++ } fmt.Println() // Example 3: WithSingleDocument option fmt.Println("3. Loader with WithSingleDocument (stops after first doc):") loader3, err := yaml.NewLoader(strings.NewReader(multiDoc), yaml.WithSingleDocument()) if err != nil { panic(err) } var firstDoc Config if err := loader3.Load(&firstDoc); err != nil { panic(err) } fmt.Printf(" First document: %+v\n", firstDoc) var secondDoc Config err = loader3.Load(&secondDoc) if err == io.EOF { fmt.Println(" Second Load() returned io.EOF (as expected)") } // Example 4: Dumper - single document fmt.Println("4. Basic Dumper - Single Document:") var buf bytes.Buffer dumper, err := yaml.NewDumper(&buf) if err != nil { panic(err) } cfg1 := Config{Name: "service1", Version: "1.0.0", Tags: []string{"prod"}} if err := dumper.Dump(&cfg1); err != nil { panic(err) } if err := dumper.Close(); err != nil { panic(err) } fmt.Printf(" Output:\n%s\n", buf.String()) // Example 5: Dumper - multiple documents fmt.Println("5. Dumper - Multiple Documents:") buf.Reset() dumper2, err := yaml.NewDumper(&buf) if err != nil { panic(err) } cfg2 := Config{Name: "service1", Version: "1.0.0"} cfg3 := Config{Name: "service2", Version: "2.0.0", Tags: []string{"dev"}} cfg4 := Config{Name: "service3", Version: "3.0.0"} if err := dumper2.Dump(&cfg2); err != nil { panic(err) } if err := dumper2.Dump(&cfg3); err != nil { panic(err) } if err := dumper2.Dump(&cfg4); err != nil { panic(err) } if err := dumper2.Close(); err != nil { panic(err) } fmt.Printf(" Output:\n%s\n", buf.String()) // Example 6: Dumper with options fmt.Println("6. Dumper with WithIndent(4):") buf.Reset() dumper3, err := yaml.NewDumper(&buf, yaml.WithIndent(4)) if err != nil { panic(err) } cfg5 := Config{Name: "service", Version: "1.0.0", Tags: []string{"a", "b", "c"}} if err := dumper3.Dump(&cfg5); err != nil { panic(err) } if err := dumper3.Close(); err != nil { panic(err) } fmt.Printf(" Output (4-space indent):\n%s\n", buf.String()) fmt.Println("=== Demo Complete ===") } golang-go.yaml-yaml-v4-4.0.0~rc5/example/multi_document_dumper/000077500000000000000000000000001521353103300244605ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc5/example/multi_document_dumper/main.go000066400000000000000000000016031521353103300257330ustar00rootroot00000000000000// Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Example: Multi-Document Dumper demonstrates dumping multiple YAML documents. package main import ( "bytes" "fmt" "go.yaml.in/yaml/v4" ) type Config struct { Name string `yaml:"name"` Version string `yaml:"version"` Tags []string `yaml:"tags,omitempty"` } func main() { fmt.Println("Example 5: Dumper - Multiple Documents") var buf bytes.Buffer dumper, err := yaml.NewDumper(&buf) if err != nil { panic(err) } configs := []Config{ {Name: "service1", Version: "1.0.0"}, {Name: "service2", Version: "2.0.0", Tags: []string{"dev"}}, {Name: "service3", Version: "3.0.0"}, } for _, cfg := range configs { if err := dumper.Dump(&cfg); err != nil { panic(err) } } if err := dumper.Close(); err != nil { panic(err) } fmt.Printf("Output:\n%s", buf.String()) } golang-go.yaml-yaml-v4-4.0.0~rc5/example/multi_document_loader/000077500000000000000000000000001521353103300244325ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc5/example/multi_document_loader/main.go000066400000000000000000000015401521353103300257050ustar00rootroot00000000000000// Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Example: Multi-Document Loader demonstrates loading multiple YAML documents. package main import ( "fmt" "io" "strings" "go.yaml.in/yaml/v4" ) type Config struct { Name string `yaml:"name"` Version string `yaml:"version"` Tags []string `yaml:"tags,omitempty"` } func main() { fmt.Println("Example 2: Multi-Document Loader") multiDoc := `--- name: app1 version: 1.0.0 --- name: app2 version: 2.0.0 tags: - experimental --- name: app3 version: 3.0.0 ` loader, err := yaml.NewLoader(strings.NewReader(multiDoc)) if err != nil { panic(err) } docNum := 1 for { var doc Config err := loader.Load(&doc) if err == io.EOF { break } if err != nil { panic(err) } fmt.Printf("Document %d: %+v\n", docNum, doc) docNum++ } } golang-go.yaml-yaml-v4-4.0.0~rc5/example/multiple_options_loader/000077500000000000000000000000001521353103300250105ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc5/example/multiple_options_loader/main.go000066400000000000000000000033341521353103300262660ustar00rootroot00000000000000// Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Multiple Options Loader demonstrates using multiple options together. package main import ( "fmt" "io" "strings" "go.yaml.in/yaml/v4" ) type Config struct { Name string `yaml:"name"` Version string `yaml:"version"` Tags []string `yaml:"tags,omitempty"` } func main() { fmt.Println("Example 9: Loader with Multiple Options") fmt.Println("(WithSingleDocument + WithKnownFields)") // First test: unknown field should fail multiDocWithUnknown := `--- name: app1 version: 1.0.0 unknownField: this-will-be-rejected --- name: app2 version: 2.0.0 ` loader1, err := yaml.NewLoader( strings.NewReader(multiDocWithUnknown), yaml.WithSingleDocument(), yaml.WithKnownFields(), ) if err != nil { panic(err) } var strictCfg Config err = loader1.Load(&strictCfg) if err != nil { fmt.Printf("✓ Got expected error (unknown field): %v\n\n", err) } else { fmt.Printf("Unexpected: no error for unknown field\n\n") } // Second test: valid document with all options validDoc := `--- name: validapp version: 1.0.0 tags: - valid --- name: app2 version: 2.0.0 ` loader2, err := yaml.NewLoader( strings.NewReader(validDoc), yaml.WithSingleDocument(), yaml.WithKnownFields(), ) if err != nil { panic(err) } var validCfg Config if err := loader2.Load(&validCfg); err != nil { panic(err) } fmt.Printf("✓ First doc loaded: %+v\n", validCfg) // Second call should return EOF due to WithSingleDocument var secondCfg Config err = loader2.Load(&secondCfg) if err == io.EOF { fmt.Println("✓ Second Load() returned io.EOF (WithSingleDocument enforced)") } else { fmt.Printf("Unexpected: got %v\n", err) } } golang-go.yaml-yaml-v4-4.0.0~rc5/example/node_dump_with_options/000077500000000000000000000000001521353103300246345ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc5/example/node_dump_with_options/main.go000066400000000000000000000026671521353103300261220ustar00rootroot00000000000000// Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Node Dump With Options demonstrates dumping Nodes with custom options. package main import ( "fmt" "log" "go.yaml.in/yaml/v4" ) type ServerConfig struct { Host string `yaml:"host"` Port int `yaml:"port"` TLS bool `yaml:"tls"` } func main() { config := ServerConfig{ Host: "localhost", Port: 8080, TLS: true, } fmt.Println("=== Node.Dump() with Different Options ===") // Example 1: Default (v4 style - 2-space indent, compact) var node1 yaml.Node if err := node1.Dump(&config); err != nil { log.Fatal(err) } fmt.Println("1. Default (v4):") printNode(&node1) // Example 2: With 4-space indent var node2 yaml.Node if err := node2.Dump(&config, yaml.WithIndent(4)); err != nil { log.Fatal(err) } fmt.Println("2. With 4-space indent:") printNode(&node2) // Example 3: With v3 preset var node3 yaml.Node if err := node3.Dump(&config, yaml.WithV3Defaults()); err != nil { log.Fatal(err) } fmt.Println("3. With V3 preset:") printNode(&node3) // Example 4: Multiple options combined var node4 yaml.Node if err := node4.Dump(&config, yaml.WithIndent(2), yaml.WithExplicitStart(), ); err != nil { log.Fatal(err) } fmt.Println("4. With explicit start marker:") printNode(&node4) } func printNode(node *yaml.Node) { data, err := yaml.Dump(node) if err != nil { log.Fatal(err) } fmt.Printf("%s\n", data) } golang-go.yaml-yaml-v4-4.0.0~rc5/example/node_load_decode_comparison/000077500000000000000000000000001521353103300255355ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc5/example/node_load_decode_comparison/main.go000066400000000000000000000032701521353103300270120ustar00rootroot00000000000000// Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Example: Node Load Decode Comparison compares loading vs decoding with Nodes. package main import ( "fmt" "go.yaml.in/yaml/v4" ) type Config struct { Name string `yaml:"name"` Port int `yaml:"port"` } // DecodeUnmarshaler uses node.Decode() (loses options) type DecodeUnmarshaler struct { Config } func (d *DecodeUnmarshaler) UnmarshalYAML(node *yaml.Node) error { type plain DecodeUnmarshaler return node.Load((*plain)(d)) // Options lost! } // LoadUnmarshaler uses node.Load() (preserves options) type LoadUnmarshaler struct { Config } func (l *LoadUnmarshaler) UnmarshalYAML(node *yaml.Node) error { type plain LoadUnmarshaler return node.Load((*plain)(l), yaml.WithKnownFields()) // Options preserved! } func main() { // YAML with an unknown field yamlData := ` name: myapp port: 8080 unknown: field ` fmt.Println("=== Comparing node.Decode() vs node.Load() ===") // Test 1: Using node.Decode() - unknown field ignored fmt.Println("1. Using node.Decode() (old way):") var config1 DecodeUnmarshaler err := yaml.Load([]byte(yamlData), &config1) if err != nil { fmt.Printf(" Error: %v\n", err) } else { fmt.Printf(" ✗ No error - unknown field was ignored\n") fmt.Printf(" Loaded: %+v\n", config1) } fmt.Println() // Test 2: Using node.Load() with WithKnownFields() - unknown field rejected fmt.Println("2. Using node.Load() with WithKnownFields() (new way):") var config2 LoadUnmarshaler err = yaml.Load([]byte(yamlData), &config2) if err != nil { fmt.Printf(" ✓ Error caught: %v\n", err) } else { fmt.Printf(" ✗ No error - this shouldn't happen!\n") } } golang-go.yaml-yaml-v4-4.0.0~rc5/example/node_load_strict_unmarshaler/000077500000000000000000000000001521353103300257715ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc5/example/node_load_strict_unmarshaler/main.go000066400000000000000000000026271521353103300272530ustar00rootroot00000000000000// Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Node Load Strict Unmarshaler demonstrates strict unmarshaling with Nodes. package main import ( "fmt" "log" "go.yaml.in/yaml/v4" ) // Config with strict field checking in custom unmarshaler type Config struct { Name string `yaml:"name"` Port int `yaml:"port"` } // UnmarshalYAML implements custom unmarshaling with strict field checking func (c *Config) UnmarshalYAML(node *yaml.Node) error { type plain Config // Use node.Load() to preserve WithKnownFields option // This solves Issue #460 - node.Decode() would lose this option return node.Load((*plain)(c), yaml.WithKnownFields()) } func main() { fmt.Println("=== Node.Load() with WithKnownFields() ===") // Valid YAML - should succeed validYAML := ` name: myapp port: 8080 ` var config1 Config err := yaml.Load([]byte(validYAML), &config1) if err != nil { log.Fatal(err) } fmt.Printf("✓ Valid config loaded: %+v\n\n", config1) // Invalid YAML with unknown field - should fail // Note: "prto" is intentionally misspelled (should be "port") to demonstrate error detection invalidYAML := ` name: myapp prto: 8080 unknown: field ` var config2 Config err = yaml.Load([]byte(invalidYAML), &config2) if err != nil { fmt.Printf("✓ Expected error caught: %v\n", err) } else { fmt.Println("✗ ERROR: Should have failed on unknown fields!") } } golang-go.yaml-yaml-v4-4.0.0~rc5/example/node_programmatic_build/000077500000000000000000000000001521353103300247255ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc5/example/node_programmatic_build/main.go000066400000000000000000000021631521353103300262020ustar00rootroot00000000000000// Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Node Programmatic Build demonstrates building YAML Nodes programmatically. package main import ( "fmt" "log" "go.yaml.in/yaml/v4" ) func main() { fmt.Println("=== Building YAML Nodes Programmatically ===") // Build a complex structure environments := map[string]any{ "development": map[string]any{ "database": "dev.db", "debug": true, }, "production": map[string]any{ "database": "prod.db", "debug": false, }, } // Convert to node with specific formatting var node yaml.Node err := node.Dump(environments, yaml.WithIndent(2), yaml.WithCompactSeqIndent(), yaml.WithExplicitStart(), ) if err != nil { log.Fatal(err) } // Now we can manipulate the node or output it fmt.Println("Generated YAML with custom formatting:") data, err := yaml.Dump(&node) if err != nil { log.Fatal(err) } fmt.Printf("%s\n", data) // We can also load it back with options var result map[string]any if err := node.Load(&result); err != nil { log.Fatal(err) } fmt.Printf("Loaded back: %+v\n", result) } golang-go.yaml-yaml-v4-4.0.0~rc5/example/single_document_loader/000077500000000000000000000000001521353103300245615ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc5/example/single_document_loader/main.go000066400000000000000000000021631521353103300260360ustar00rootroot00000000000000// Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Example: Single Document Loader demonstrates loading a single YAML document. package main import ( "fmt" "io" "strings" "go.yaml.in/yaml/v4" ) type Config struct { Name string `yaml:"name"` Version string `yaml:"version"` Tags []string `yaml:"tags,omitempty"` } func main() { fmt.Println("Example 3: Loader with WithSingleDocument Option") multiDoc := `--- name: app1 version: 1.0.0 --- name: app2 version: 2.0.0 tags: - experimental --- name: app3 version: 3.0.0 ` // Create loader that stops after first document loader, err := yaml.NewLoader(strings.NewReader(multiDoc), yaml.WithSingleDocument()) if err != nil { panic(err) } var firstDoc Config if err := loader.Load(&firstDoc); err != nil { panic(err) } fmt.Printf("First document: %+v\n", firstDoc) // Try to load second document var secondDoc Config err = loader.Load(&secondDoc) if err == io.EOF { fmt.Println("Second Load() returned io.EOF (as expected with WithSingleDocument)") } else { fmt.Printf("Unexpected: got %v\n", err) } } golang-go.yaml-yaml-v4-4.0.0~rc5/example/version_options/000077500000000000000000000000001521353103300233145ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc5/example/version_options/main.go000066400000000000000000000030771521353103300245760ustar00rootroot00000000000000// Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Example: Version Options demonstrates using YAML version presets. package main import ( "fmt" "go.yaml.in/yaml/v4" ) type Config struct { Name string `yaml:"name"` Items []string `yaml:"items"` } func main() { cfg := Config{ Name: "test", Items: []string{"apple", "banana", "cherry"}, } fmt.Println("Example: Comparing v2, v3, and v4 option presets") // v2 options - 2-space indent, non-compact sequences fmt.Println("=== yaml.WithV2Defaults() - 2-space indent, non-compact sequences ===") out, _ := yaml.Dump(&cfg, yaml.WithV2Defaults()) fmt.Print(string(out)) // v3 options - 4-space indent (default), non-compact sequences fmt.Println("=== yaml.WithV3Defaults() - 4-space indent, non-compact sequences ===") out, _ = yaml.Dump(&cfg, yaml.WithV3Defaults()) fmt.Print(string(out)) // v4 options - 2-space indent, compact sequences fmt.Println("=== yaml.WithV4Defaults() - 2-space indent, compact sequences ===") out, _ = yaml.Dump(&cfg, yaml.WithV4Defaults()) fmt.Print(string(out)) // Override v4 options fmt.Println("=== yaml.WithV4Defaults() with WithIndent(3) override ===") out, _ = yaml.Dump(&cfg, yaml.WithV4Defaults(), yaml.WithIndent(3)) fmt.Print(string(out)) fmt.Println("\nNotice how:") fmt.Println("- v2 and v4 both use 2-space indentation") fmt.Println("- v3 uses 4-space indentation (classic go-yaml v3 style)") fmt.Println("- v4 uses compact sequences (items: flows from dash)") fmt.Println("- Options can be combined and later ones win") } golang-go.yaml-yaml-v4-4.0.0~rc5/example/with_v4_option/000077500000000000000000000000001521353103300230305ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc5/example/with_v4_option/main.go000066400000000000000000000042641521353103300243110ustar00rootroot00000000000000// Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Example: With V4 Option demonstrates using the V4 version preset. package main import ( "bytes" "fmt" "strings" "go.yaml.in/yaml/v4" ) type Config struct { Name string `yaml:"name"` Version string `yaml:"version"` Server ServerConfig `yaml:"server"` Tags []string `yaml:"tags,omitempty"` Metadata map[string]string `yaml:"metadata,omitempty"` } type ServerConfig struct { Host string `yaml:"host"` Port int `yaml:"port"` Debug bool `yaml:"debug"` } func main() { fmt.Println("Example: v4 Options (Now the Default)") fmt.Println("The new API (Dump, Load, NewDumper, NewLoader) defaults to v4:") fmt.Println(" - 2-space indentation") fmt.Println(" - Compact sequence indentation") yamlData := `# Application configuration name: myapp version: 1.0.0 # Server settings server: host: localhost port: 8080 debug: true tags: - web - api metadata: owner: platform-team env: prod ` // Load with default options (now v4) loader, err := yaml.NewLoader(strings.NewReader(yamlData)) if err != nil { panic(err) } var node yaml.Node if err := loader.Load(&node); err != nil { panic(err) } fmt.Println("--- Loaded with Default Options (v4) ---") fmt.Printf("Comments preserved: %v\n", node.Content[0].Content[0].HeadComment != "") fmt.Println() // Now dump it back with default options (v4: 2-space indent, compact) var buf bytes.Buffer dumper, err := yaml.NewDumper(&buf) if err != nil { panic(err) } if err := dumper.Dump(&node); err != nil { panic(err) } if err := dumper.Close(); err != nil { panic(err) } fmt.Println("--- Default Output (v4: 2-space indent, compact, comments preserved) ---") fmt.Print(buf.String()) // Compare with v3 defaults for backward compatibility fmt.Println("\n--- For comparison: v3 defaults (4-space indent, non-compact) ---") buf.Reset() dumper2, err := yaml.NewDumper(&buf, yaml.WithV3Defaults()) if err != nil { panic(err) } if err := dumper2.Dump(&node); err != nil { panic(err) } if err := dumper2.Close(); err != nil { panic(err) } fmt.Print(buf.String()) } golang-go.yaml-yaml-v4-4.0.0~rc5/example/with_v4_override/000077500000000000000000000000001521353103300233375ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc5/example/with_v4_override/main.go000066400000000000000000000036151521353103300246170ustar00rootroot00000000000000// Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Example: With V4 Override demonstrates overriding V4 preset options. package main import ( "bytes" "fmt" "go.yaml.in/yaml/v4" ) type Config struct { Name string `yaml:"name"` Server map[string]string `yaml:"server"` Tags []string `yaml:"tags"` } func main() { fmt.Println("Example: Overriding yaml.WithV4Defaults() options") cfg := Config{ Name: "myapp", Server: map[string]string{ "host": "localhost", "port": "8080", }, Tags: []string{"web", "api"}, } // Test 1: v4 options - should use 2-space indent fmt.Println("1. yaml.WithV4Defaults() - 2-space indent:") var buf bytes.Buffer dumper, err := yaml.NewDumper(&buf, yaml.WithV4Defaults()) if err != nil { panic(err) } if err := dumper.Dump(&cfg); err != nil { panic(err) } if err := dumper.Close(); err != nil { panic(err) } fmt.Print(buf.String()) // Test 2: v4 options then WithIndent(3) - WithIndent(3) overrides fmt.Println("\n2. WithV4Defaults(), then WithIndent(3) - should use 3-space indent:") buf.Reset() dumper2, err := yaml.NewDumper(&buf, yaml.WithV4Defaults(), yaml.WithIndent(3)) if err != nil { panic(err) } if err := dumper2.Dump(&cfg); err != nil { panic(err) } if err := dumper2.Close(); err != nil { panic(err) } fmt.Print(buf.String()) // Test 3: WithIndent(5) then v4 options - v4 options override to 2 fmt.Println("\n3. WithIndent(5), then WithV4Defaults() - should use 2-space indent (v4 wins):") buf.Reset() dumper3, err := yaml.NewDumper(&buf, yaml.WithIndent(5), yaml.WithV4Defaults()) if err != nil { panic(err) } if err := dumper3.Dump(&cfg); err != nil { panic(err) } if err := dumper3.Close(); err != nil { panic(err) } fmt.Print(buf.String()) fmt.Println("\nConclusion: Options are applied left-to-right, later options override earlier ones.") } golang-go.yaml-yaml-v4-4.0.0~rc5/go.mod000066400000000000000000000000431521353103300175240ustar00rootroot00000000000000module go.yaml.in/yaml/v4 go 1.18 golang-go.yaml-yaml-v4-4.0.0~rc5/internal/000077500000000000000000000000001521353103300202355ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc5/internal/libyaml/000077500000000000000000000000001521353103300216665ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc5/internal/libyaml/README.md000066400000000000000000000343001521353103300231450ustar00rootroot00000000000000# internal/libyaml This package provides low-level YAML processing functionality through a 3-stage pipeline: Scanner → Parser → Emitter. It implements the libyaml C library functionality in Go. ## Directory Overview The `internal/libyaml` package implements the core YAML processing stages: 1. **Scanner** - Tokenizes YAML text into tokens 2. **Parser** - Converts tokens into events following YAML grammar rules 3. **Emitter** - Serializes events back into YAML text ## File Organization ### Main Source Files - **scanner.go** - YAML scanner/tokenizer implementation - **parser.go** - YAML parser (tokens → events) - **emitter.go** - YAML emitter (events → YAML output) - **api.go** - Public API for Parser and Emitter types - **yaml.go** - Core types and constants (Event, Token, enums) - **reader.go** - Input handling and encoding detection - **writer.go** - Output handling - **util.go** - Internal types and helper functions ### Test Files - **scanner_test.go** - Scanner tests - **parser_test.go** - Parser tests - **emitter_test.go** - Emitter tests - **api_test.go** - API tests - **yaml_test.go** - Utility function tests - **reader_test.go** - Reader tests - **writer_test.go** - Writer tests - **util_test.go** - Character classification tests - **loader_test.go** - Data loader scalar resolution tests - **yamldatatest_test.go** - YAML test data loading framework - **yamldatatest_loader.go** - YAML test data loader with scalar type resolution (exported for reuse) ### Test Data Files (in `testdata/`) - **scanner.yaml** - Scanner test cases - **parser.yaml** - Parser test cases - **emitter.yaml** - Emitter test cases - **api.yaml** - API test cases - **yaml.yaml** - Utility function test cases - **reader.yaml** - Reader test cases - **writer.yaml** - Writer test cases - **util.yaml** - Character classification test cases - **loader.yaml** - Data loader scalar resolution test cases ## Processing Pipeline ### 1. Scanner (scanner.go) The scanner converts YAML text into tokens. **Input**: Raw YAML text (string or []byte) **Output**: Stream of tokens **Token types include**: - `SCALAR_TOKEN` - Plain, quoted, or block scalar values - `KEY_TOKEN`, `VALUE_TOKEN` - Mapping key/value indicators - `BLOCK_MAPPING_START_TOKEN`, `FLOW_MAPPING_START_TOKEN` - Mapping delimiters - `BLOCK_SEQUENCE_START_TOKEN`, `FLOW_SEQUENCE_START_TOKEN` - Sequence delimiters - `ANCHOR_TOKEN`, `ALIAS_TOKEN` - Anchor definitions and references - `TAG_TOKEN` - Type tags - `DOCUMENT_START_TOKEN`, `DOCUMENT_END_TOKEN` - Document boundaries **Responsibilities**: - Character encoding detection (UTF-8, UTF-16LE, UTF-16BE) - Line break normalization - Indentation tracking - Quote and escape sequence handling ### 2. Parser (parser.go) The parser converts tokens into events following YAML grammar rules. **Input**: Stream of tokens from Scanner **Output**: Stream of events **Event types include**: - `STREAM_START_EVENT`, `STREAM_END_EVENT` - Stream boundaries - `DOCUMENT_START_EVENT`, `DOCUMENT_END_EVENT` - Document boundaries - `SCALAR_EVENT` - Scalar values - `MAPPING_START_EVENT`, `MAPPING_END_EVENT` - Mapping boundaries - `SEQUENCE_START_EVENT`, `SEQUENCE_END_EVENT` - Sequence boundaries - `ALIAS_EVENT` - Anchor references **Responsibilities**: - Implementing YAML grammar and validation - Managing document directives (%YAML, %TAG) - Resolving anchors and aliases - Tracking implicit vs explicit markers - Style preservation (plain, single-quoted, double-quoted, literal, folded) ### 3. Emitter (emitter.go) The emitter converts events back into YAML text. **Input**: Stream of events **Output**: YAML text **Responsibilities**: - Style selection (plain/quoted scalars, block/flow collections) - Formatting control (canonical mode, indentation, line width) - Character encoding - Anchor and tag serialization - Document marker generation (---, ...) **Configuration options**: - `Canonical` - Emit in canonical YAML form - `Indent` - Indentation width (2-9 spaces) - `Width` - Line width (-1 for unlimited) - `Unicode` - Enable Unicode character output - `LineBreak` - Line break style (LN, CR, CRLN) ## Testing Framework ### Test Architecture The testing framework uses a data-driven approach: 1. **Test data** is stored in YAML files in the `testdata/` directory 2. **Test logic** is implemented in Go files (`*_test.go`) 3. **One-to-one pairing**: Each `testdata/foo.yaml` has a corresponding `foo_test.go` **Benefits**: - Easy to add new test cases without writing Go code - Test data is human-readable and self-documenting - Test logic is reusable across many test cases - Test data is separated from test code for clarity - Tests can become a common suite for multiple YAML frameworks ### Test Data Files Each YAML file contains test cases for a specific component: - **scanner.yaml** - Scanner/tokenization tests - Token sequence verification - Token property validation (value, style) - Error detection - **parser.yaml** - Parser/event generation tests - Event sequence verification - Event property validation (anchor, tag, value, directives) - Error detection - **emitter.yaml** - Emitter/serialization tests - Event-to-YAML conversion - Configuration options testing - Roundtrip testing (parse → emit) - Writer integration - **api.yaml** - API constructor and method tests - Constructor validation - Method behavior and state changes - Panic conditions - Cleanup verification - **yaml.yaml** - Utility function tests - Enum String() methods - Style accessor methods - **reader.yaml** - Reader/input handling tests - Encoding detection (UTF-8, UTF-16LE, UTF-16BE) - Buffer management - Error handling - **writer.yaml** - Writer/output handling tests - Buffer flushing - Output handlers (string, io.Writer) - Error conditions - **util.yaml** - Character classification tests - Character type predicates (isAlpha, isDigit, isHex, etc.) - Character conversion functions (asDigit, asHex, width) - Unicode handling - **loader.yaml** - Data loader scalar resolution tests - Numeric type resolution (integers, floats) - Boolean and null value handling - String vs numeric type disambiguation - Mixed-type collections ### Test Framework Implementation The test framework is implemented in `testdata_test.go`: **Core functions**: - `LoadAny(data []byte) (interface{}, error)` - Parses YAML using production loader with scalar type resolution (exported from loader.go) - `UnmarshalStruct(target interface{}, data map[string]interface{}) error` - Populates structs (exported) - `LoadTestCases(filename string) ([]TestCase, error)` - Loads and parses test YAML files **Core types**: - `TestCase` struct - Umbrella structure containing fields for all test types - Uses `interface{}` for flexible field types - Post-processing converts generic fields to specific types **Post-processing**: After loading, the framework processes test data: - Converts `Want` (interface{}) to `WantEvents`, `WantTokens`, or `WantSpecs` based on test type - Converts `Want` (interface{}) to `WantContains` (handles both scalar and sequence) - Converts `Checks` to field validation specifications ### Test Types #### Scanner Tests **scan-tokens** - Verify token sequence ```yaml - scan-tokens: name: Simple scalar yaml: |- hello want: - STREAM_START_TOKEN - SCALAR_TOKEN - STREAM_END_TOKEN ``` **scan-tokens-detailed** - Verify token properties ```yaml - scan-tokens-detailed: name: Single quoted scalar yaml: |- 'hello world' want: - STREAM_START_TOKEN - SCALAR_TOKEN: style: SINGLE_QUOTED_SCALAR_STYLE value: hello world - STREAM_END_TOKEN ``` **scan-error** - Verify error detection ```yaml - scan-error: name: Invalid character yaml: "\x01" ``` #### Parser Tests **parse-events** - Verify event sequence ```yaml - parse-events: name: Simple mapping yaml: | key: value want: - STREAM_START_EVENT - DOCUMENT_START_EVENT - MAPPING_START_EVENT - SCALAR_EVENT - SCALAR_EVENT - MAPPING_END_EVENT - DOCUMENT_END_EVENT - STREAM_END_EVENT ``` **parse-events-detailed** - Verify event properties ```yaml - parse-events-detailed: name: Anchor and alias yaml: | - &anchor value - *anchor want: - STREAM_START_EVENT - DOCUMENT_START_EVENT - SEQUENCE_START_EVENT - SCALAR_EVENT: anchor: anchor value: value - ALIAS_EVENT: anchor: anchor - SEQUENCE_END_EVENT - DOCUMENT_END_EVENT - STREAM_END_EVENT ``` **parse-error** - Verify error detection ```yaml - parse-error: name: Error state yaml: | key: : invalid ``` #### Emitter Tests **emit** - Emit events and verify output contains expected strings ```yaml - emit: name: Simple scalar data: - STREAM_START_EVENT: encoding: UTF8_ENCODING - DOCUMENT_START_EVENT: implicit: true - SCALAR_EVENT: value: hello implicit: true style: PLAIN_SCALAR_STYLE - DOCUMENT_END_EVENT: implicit: true - STREAM_END_EVENT want: hello ``` **emit-config** - Emit with configuration ```yaml - emit-config: name: Custom indent conf: indent: 4 data: - STREAM_START_EVENT: encoding: UTF8_ENCODING - DOCUMENT_START_EVENT: implicit: true - MAPPING_START_EVENT: implicit: true style: BLOCK_MAPPING_STYLE # ... more events want: key ``` **roundtrip** - Parse → emit, verify output ```yaml - roundtrip: name: Roundtrip yaml: | key: value list: - item1 - item2 want: - key - value - item1 ``` **emit-writer** - Emit to io.Writer ```yaml - emit-writer: name: Writer data: - STREAM_START_EVENT: encoding: UTF8_ENCODING # ... more events want: test ``` #### API Tests **api-new** - Test constructors ```yaml - api-new: name: New parser with: NewParser test: - nil: [raw-buffer, false] - cap: [raw-buffer, 512] - nil: [buffer, false] - cap: [buffer, 1536] ``` **api-method** - Test methods and field state ```yaml - api-method: name: Parser set input string with: NewParser byte: true call: [SetInputString, 'key: value'] test: - eq: [input, 'key: value'] - eq: [input-pos, 0] - nil: [read-handler, false] ``` **api-panic** - Test methods that should panic ```yaml - api-panic: name: Parser set input string twice with: NewParser byte: true init: [SetInputString, first] call: [SetInputString, second] want: must set the input source only once ``` **api-delete** - Test cleanup ```yaml - api-delete: name: Parser delete with: NewParser byte: true init: [SetInputString, test] test: - len: [input, 0] - len: [buffer, 0] ``` **api-new-event** - Test event constructors ```yaml - api-new-event: name: New stream start event call: [NewStreamStartEvent, UTF8_ENCODING] test: - eq: [Type, STREAM_START_EVENT] - eq: [encoding, UTF8_ENCODING] ``` #### Utility Tests **enum-string** - Test String() methods of enums ```yaml - enum-string: name: Scalar style plain enum: [ScalarStyle, PLAIN_SCALAR_STYLE] want: Plain ``` **style-accessor** - Test style accessor methods ```yaml - style-accessor: name: Event scalar style test: [ScalarStyle, DOUBLE_QUOTED_SCALAR_STYLE] ``` #### Loader Tests **scalar-resolution** - Test scalar type resolution ```yaml - scalar-resolution: name: Positive integer yaml: "42" want: 42 - scalar-resolution: name: Negative float yaml: "-2.5" want: -2.5 ``` **Resolution order**: 1. Boolean (true, false) 2. Null (null keyword only) 3. Hexadecimal integer (0x prefix) 4. Float (contains .) 5. Decimal integer 6. String (fallback) ### Common Keys in Test YAML Files Test cases use a **type-as-key** format where the test type is the map key: ```yaml - test-type: name: Test case name # ... other fields ``` **Common fields**: - **name** - Test case name (title case convention) - **yaml** - Input YAML string to test - **want** - Expected result (format varies by test type) - For api-panic: string containing expected panic message substring - For scan-error/parse-error: boolean (defaults to true if omitted; set to false if no error expected) - For enum-string: string representing expected String() output - For other types: varies (may be sequence or scalar) - **data** - For emitter tests: list of event specifications to emit - **conf** - For emitter config tests: emitter configuration options - **with** - For API tests: constructor name (NewParser, NewEmitter) - **call** - For API tests: method call [MethodName, arg1, arg2, ...] - **init** - For API panic tests: setup method call before main method - **byte** - For API tests: boolean flag to convert string args to []byte - **test** - For API tests: list of field validation checks in format `operator: [field, value]` where operator is one of: nil, cap, len, eq, gte, len-gt. - **test** - For style-accessor tests: array of [Method, STYLE] where Method is the accessor method (e.g., ScalarStyle) and STYLE is the style constant (e.g., DOUBLE_QUOTED_SCALAR_STYLE). - **enum** - For enum tests: array of [Type, Value] where Type is the enum type (e.g., ScalarStyle) and Value is the constant (e.g., PLAIN_SCALAR_STYLE) **Note on scalar type resolution**: Unquoted scalar values in test data are automatically resolved to appropriate Go types (int, float64, bool, nil) by the `LoadAny` function. Quoted scalars remain as strings. ### Running Tests ```bash # Run all tests in the package go test ./internal/libyaml # Run specific test file go test ./internal/libyaml -run TestScanner go test ./internal/libyaml -run TestParser go test ./internal/libyaml -run TestEmitter go test ./internal/libyaml -run TestAPI go test ./internal/libyaml -run TestYAML go test ./internal/libyaml -run TestLoader # Run specific test case (using subtest name) go test ./internal/libyaml -run TestScanner/Block_sequence go test ./internal/libyaml -run TestParser/Anchor_and_alias go test ./internal/libyaml -run TestEmitter/Flow_mapping go test ./internal/libyaml -run TestLoader/Scientific_notation_lowercase_e # Run with verbose output go test -v ./internal/libyaml # Run with coverage go test -cover ./internal/libyaml ``` golang-go.yaml-yaml-v4-4.0.0~rc5/internal/libyaml/composer.go000066400000000000000000000260621521353103300240520ustar00rootroot00000000000000// Copyright 2011-2019 Canonical Ltd // Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Composer stage: Builds a node tree from a libyaml event stream. // Handles document structure, anchors, and comment attachment. package libyaml import ( "fmt" "io" ) // Composer produces a node tree out of a libyaml event stream. type Composer struct { Parser Parser event Event doc *Node anchors map[string]*Node doneInit bool Textless bool streamNodes bool // enable stream node emission returnStream bool // flag to return stream node next atStreamEnd bool // at stream end encoding Encoding // stream encoding from STREAM_START opts *Options // options for loading } // NewComposer creates a new composer from a byte slice. func NewComposer(b []byte, opts *Options) *Composer { p := Composer{ Parser: NewParser(), opts: opts, } if len(b) == 0 { b = []byte{'\n'} } p.Parser.SetInputString(b) if opts != nil { p.Parser.depthCheck = opts.DepthCheck } return &p } // NewComposerFromReader creates a new composer from an [io.Reader]. func NewComposerFromReader(r io.Reader, opts *Options) *Composer { p := Composer{ Parser: NewParser(), opts: opts, } p.Parser.SetInputReader(r) if opts != nil { p.Parser.depthCheck = opts.DepthCheck } return &p } // Compose composes the next YAML node from the event stream. func (c *Composer) Compose() *Node { c.init() // Handle stream nodes if enabled if c.streamNodes { // Check for stream end first if c.peek() == STREAM_END_EVENT { // If we haven't returned the final stream node yet, // return it now if !c.atStreamEnd { c.atStreamEnd = true return c.createStreamNode() } // Already returned final stream node return nil } // Check if we should return a stream node before the next // document if c.returnStream { c.returnStream = false n := c.createStreamNode() // Capture directives from upcoming document c.captureDirectives(n) return n } } switch c.peek() { case SCALAR_EVENT: return c.scalar() case ALIAS_EVENT: return c.alias() case MAPPING_START_EVENT: return c.mapping() case SEQUENCE_START_EVENT: return c.sequence() case DOCUMENT_START_EVENT: return c.document() case STREAM_END_EVENT: // Happens when attempting to decode an empty buffer (when not // using stream nodes). return nil case TAIL_COMMENT_EVENT: panic("internal error: unexpected tail comment event (please report)") default: panic("internal error: attempted to parse unknown event (please report): " + c.event.Type.String()) } } // node creates a new node with the given kind, tag, and value, and attaches // position and comment information from the current event. func (c *Composer) node(kind Kind, tag, value string) *Node { var style Style if tag != "" && tag != "!" { // Normalize tag to short form (e.g., tag:yaml.org,2002:str -> !!str) tag = shortTag(tag) style = TaggedStyle } // Note: Nodes without explicit tags are left with empty tags. // Tag defaulting happens in a separate stage via Resolver. n := &Node{ Kind: kind, Tag: tag, Value: value, Style: style, } if !c.Textless { n.Line = c.event.StartMark.Line n.Column = c.event.StartMark.Column n.HeadComment = string(c.event.HeadComment) n.LineComment = string(c.event.LineComment) n.FootComment = string(c.event.FootComment) } return n } // document composes a document node by parsing its content between // DOCUMENT_START and DOCUMENT_END events. func (c *Composer) document() *Node { n := c.node(DocumentNode, "", "") c.doc = n c.expect(DOCUMENT_START_EVENT) c.parseChild(n) if c.peek() == DOCUMENT_END_EVENT { n.FootComment = string(c.event.FootComment) } c.expect(DOCUMENT_END_EVENT) // If stream nodes enabled, prepare to return a stream node next if c.streamNodes { c.returnStream = true } return n } // createStreamNode creates a stream node with encoding information. func (c *Composer) createStreamNode() *Node { n := &Node{ Kind: StreamNode, Stream: &Stream{Encoding: c.encoding}, } if !c.Textless && c.event.Type != NO_EVENT { n.Line = c.event.StartMark.Line n.Column = c.event.StartMark.Column } return n } // alias composes an alias node by resolving the referenced anchor. func (c *Composer) alias() *Node { n := c.node(AliasNode, "", string(c.event.Anchor)) n.Alias = c.anchors[n.Value] if n.Alias == nil { msg := fmt.Sprintf("unknown anchor '%s' referenced", n.Value) Fail(formatComposerError(msg, Mark{ Line: n.Line, Column: n.Column, })) } c.expect(ALIAS_EVENT) return n } // scalar composes a scalar node with value, tag, and style information. func (c *Composer) scalar() *Node { parsedStyle := c.event.ScalarStyle() var nodeStyle Style switch { case parsedStyle&DOUBLE_QUOTED_SCALAR_STYLE != 0: nodeStyle = DoubleQuotedStyle case parsedStyle&SINGLE_QUOTED_SCALAR_STYLE != 0: nodeStyle = SingleQuotedStyle case parsedStyle&LITERAL_SCALAR_STYLE != 0: nodeStyle = LiteralStyle case parsedStyle&FOLDED_SCALAR_STYLE != 0: nodeStyle = FoldedStyle } nodeValue := string(c.event.Value) nodeTag := string(c.event.Tag) n := c.node(ScalarNode, nodeTag, nodeValue) n.Style |= nodeStyle c.anchor(n, c.event.Anchor) c.expect(SCALAR_EVENT) return n } // sequence composes a sequence node by parsing elements between // SEQUENCE_START and SEQUENCE_END events. func (c *Composer) sequence() *Node { n := c.node(SequenceNode, string(c.event.Tag), "") if c.event.SequenceStyle()&FLOW_SEQUENCE_STYLE != 0 { n.Style |= FlowStyle } c.anchor(n, c.event.Anchor) c.expect(SEQUENCE_START_EVENT) for c.peek() != SEQUENCE_END_EVENT { c.parseChild(n) } n.LineComment = string(c.event.LineComment) n.FootComment = string(c.event.FootComment) c.expect(SEQUENCE_END_EVENT) return n } // mapping composes a mapping node by parsing key-value pairs between // MAPPING_START and MAPPING_END events, handling foot comments appropriately. func (c *Composer) mapping() *Node { n := c.node(MappingNode, string(c.event.Tag), "") block := true if c.event.MappingStyle()&FLOW_MAPPING_STYLE != 0 { block = false n.Style |= FlowStyle } c.anchor(n, c.event.Anchor) c.expect(MAPPING_START_EVENT) for c.peek() != MAPPING_END_EVENT { k := c.parseChild(n) if block && k.FootComment != "" { // Must be a foot comment for the prior value when being dedented. if len(n.Content) > 2 { n.Content[len(n.Content)-3].FootComment = k.FootComment k.FootComment = "" } } v := c.parseChild(n) if k.FootComment == "" && v.FootComment != "" { k.FootComment = v.FootComment v.FootComment = "" } if c.peek() == TAIL_COMMENT_EVENT { if k.FootComment == "" { k.FootComment = string(c.event.FootComment) } c.expect(TAIL_COMMENT_EVENT) } } n.LineComment = string(c.event.LineComment) n.FootComment = string(c.event.FootComment) if n.Style&FlowStyle == 0 && n.FootComment != "" && len(n.Content) > 1 { n.Content[len(n.Content)-2].FootComment = n.FootComment n.FootComment = "" } c.expect(MAPPING_END_EVENT) return n } // init initializes the composer by setting up the anchor map and consuming // the STREAM_START event. func (c *Composer) init() { if c.doneInit { return } c.anchors = make(map[string]*Node) // Peek to get the encoding from STREAM_START_EVENT if c.peek() == STREAM_START_EVENT { c.encoding = c.event.GetEncoding() } c.expect(STREAM_START_EVENT) c.doneInit = true // If stream nodes are enabled, prepare to return the first stream node if c.streamNodes { c.returnStream = true } } // Destroy cleans up the composer by deleting any pending event and the // underlying parser. func (c *Composer) Destroy() { if c.event.Type != NO_EVENT { c.event.Delete() } c.Parser.Delete() } // SetStreamNodes enables or disables stream node emission. func (c *Composer) SetStreamNodes(enable bool) { c.streamNodes = enable } // expect consumes an event from the event stream and // checks that it's of the expected type. func (c *Composer) expect(e EventType) { if c.event.Type == NO_EVENT { if err := c.Parser.Parse(&c.event); err != nil { c.fail(err) } } if c.event.Type == STREAM_END_EVENT { Fail(formatComposerError( "attempted to go past the end of stream; corrupted value?", Mark{Line: c.event.StartMark.Line, Column: c.event.StartMark.Column}, )) } if c.event.Type != e { Fail(formatComposerError( fmt.Sprintf("expected %s event but got %s", e, c.event.Type), Mark{Line: c.event.StartMark.Line, Column: c.event.StartMark.Column}, )) } c.event.Delete() c.event.Type = NO_EVENT } // peek peeks at the next event in the event stream, // puts the results into c.event and returns the event type. func (c *Composer) peek() EventType { if c.event.Type != NO_EVENT { return c.event.Type } // It's curious choice from the underlying API to generally return a // positive result on success, but on this case return true in an error // scenario. This was the source of bugs in the past (issue #666). if err := c.Parser.Parse(&c.event); err != nil { c.fail(err) } return c.event.Type } // fail panics with the given error. func (c *Composer) fail(err error) { Fail(err) } // anchor sets the anchor name on a node and records it in the anchor map. func (c *Composer) anchor(n *Node, anchor []byte) { if anchor != nil { n.Anchor = string(anchor) c.anchors[n.Anchor] = n } } // parseChild composes the next node and adds it as a child to the parent. func (c *Composer) parseChild(parent *Node) *Node { child := c.Compose() parent.Content = append(parent.Content, child) return child } // captureDirectives captures version and tag directives from upcoming // DOCUMENT_START. // The node n must have Stream initialized (as created by createStreamNode). func (c *Composer) captureDirectives(n *Node) { if c.peek() == DOCUMENT_START_EVENT { if vd := c.event.GetVersionDirective(); vd != nil { n.Stream.Version = &StreamVersionDirective{ Major: vd.Major(), Minor: vd.Minor(), } } if tds := c.event.GetTagDirectives(); len(tds) > 0 { n.Stream.TagDirectives = make([]StreamTagDirective, len(tds)) for i, td := range tds { n.Stream.TagDirectives[i] = StreamTagDirective{ Handle: td.GetHandle(), Prefix: td.GetPrefix(), } } } } } // Fail panics with a YAMLError wrapping the given error. func Fail(err error) { panic(&YAMLError{err}) } // failf panics with a YAMLError containing a formatted error message. func failf(format string, args ...any) { panic(&YAMLError{fmt.Errorf("yaml: "+format, args...)}) } // formatComposerError creates a LoadError for composer-stage errors. func formatComposerError(message string, mark Mark) *LoadError { return &LoadError{ Stage: ComposerStage, Mark: mark, Message: message, } } // formatComposerErrorContext creates a LoadError with both context and // problem information for composer-stage errors. func formatComposerErrorContext(context string, contextMark Mark, message string, mark Mark) *LoadError { return &LoadError{ Stage: ComposerStage, ContextMark: contextMark, ContextMsg: context, Mark: mark, Message: message, } } golang-go.yaml-yaml-v4-4.0.0~rc5/internal/libyaml/composer_test.go000066400000000000000000000074551521353103300251160ustar00rootroot00000000000000// Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Tests for the Composer stage package libyaml import ( "fmt" "testing" ) // checkComposedNode recursively validates a composed node against expected structure func checkComposedNode(t *testing.T, node *Node, wantMap map[string]any, path string) { t.Helper() // Check kind if kindStr, ok := wantMap["kind"].(string); ok { var expectedKind Kind switch kindStr { case "Scalar": expectedKind = ScalarNode case "Sequence": expectedKind = SequenceNode case "Mapping": expectedKind = MappingNode } if node.Kind != expectedKind { t.Fatalf("%s: got kind %v; want %v", path, node.Kind, expectedKind) } } // Check tag if wantTag, ok := wantMap["tag"].(string); ok { if node.Tag != wantTag { t.Fatalf("%s: got tag %q; want %q", path, node.Tag, wantTag) } } // Check value (for scalars) if wantValue, ok := wantMap["value"].(string); ok { if node.Value != wantValue { t.Fatalf("%s: got value %q; want %q", path, node.Value, wantValue) } } // Check style if wantStyle, ok := wantMap["style"].(string); ok { var expectedStyle Style switch wantStyle { case "Single": expectedStyle = SingleQuotedStyle case "Double": expectedStyle = DoubleQuotedStyle case "Literal": expectedStyle = LiteralStyle case "Folded": expectedStyle = FoldedStyle case "Flow": expectedStyle = FlowStyle } if expectedStyle != 0 && node.Style&expectedStyle == 0 { t.Fatalf("%s: expected style %v but got %v", path, expectedStyle, node.Style) } } // Check content (for collections) if wantContent, ok := wantMap["content"].([]any); ok { if len(node.Content) != len(wantContent) { t.Fatalf("%s: got %d children; want %d", path, len(node.Content), len(wantContent)) } for i, wantChild := range wantContent { if wantChildMap, ok := wantChild.(map[string]any); ok { childPath := fmt.Sprintf("%s[%d]", path, i) checkComposedNode(t, node.Content[i], wantChildMap, childPath) } } } } func TestComposer(t *testing.T) { RunTestCases(t, "composer.yaml", map[string]TestHandler{ "compose-scalar": func(t *testing.T, tc TestCase) { t.Helper() // Parse YAML from tc.From (YAML input string) yaml := tc.From.(string) c := NewComposer([]byte(yaml), nil) defer c.Destroy() // Get document node doc := c.Compose() if doc == nil || doc.Kind != DocumentNode { t.Fatal("expected DocumentNode") } if len(doc.Content) == 0 { t.Fatal("expected content in document") } node := doc.Content[0] // Check node against want spec wantMap := tc.Want.(map[string]any) checkComposedNode(t, node, wantMap, "root") }, "compose-collection": func(t *testing.T, tc TestCase) { t.Helper() // Parse YAML from tc.From (YAML input string) yaml := tc.From.(string) c := NewComposer([]byte(yaml), nil) defer c.Destroy() // Get document node doc := c.Compose() if doc == nil || doc.Kind != DocumentNode { t.Fatal("expected DocumentNode") } if len(doc.Content) == 0 { t.Fatal("expected content in document") } node := doc.Content[0] // Check node against want spec wantMap := tc.Want.(map[string]any) checkComposedNode(t, node, wantMap, "root") }, "compose-style": func(t *testing.T, tc TestCase) { t.Helper() // Parse YAML from tc.From (YAML input string) yaml := tc.From.(string) c := NewComposer([]byte(yaml), nil) defer c.Destroy() // Get document node doc := c.Compose() if doc == nil || doc.Kind != DocumentNode { t.Fatal("expected DocumentNode") } if len(doc.Content) == 0 { t.Fatal("expected content in document") } node := doc.Content[0] // Check node against want spec wantMap := tc.Want.(map[string]any) checkComposedNode(t, node, wantMap, "root") }, }) } golang-go.yaml-yaml-v4-4.0.0~rc5/internal/libyaml/constructor.go000066400000000000000000000761461521353103300246200ustar00rootroot00000000000000// Copyright 2011-2019 Canonical Ltd // Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Constructor stage: Converts YAML nodes to Go values. // Handles type resolution, custom unmarshalers, and struct field mapping. package libyaml import ( "encoding" "encoding/base64" "fmt" "math" "reflect" "time" ) // -------------------------------------------------------------------------- // Types and Interfaces // legacyConstructor is the old-style unmarshaler interface. // It's kept for backwards compatibility. type legacyConstructor interface { UnmarshalYAML(construct func(any) error) error } // constructorAdapter is an interface that wraps the root package's Unmarshaler // interface. // This allows the constructor to call constructors that expect *yaml.Node // instead of *libyaml.Node. type constructorAdapter interface { CallRootConstructor(n *Node) error } // ScalarConstructFunc is the signature for tag-specific scalar constructor // functions. // Each function handles construction of a specific YAML tag to various Go // types. type ScalarConstructFunc func(c *Constructor, n *Node, resolved any, out reflect.Value) bool // Constructor state type Constructor struct { doc *Node aliases map[*Node]bool TypeErrors []*LoadError stringMapType reflect.Type generalMapType reflect.Type KnownFields bool UniqueKeys bool constructCount int aliasCount int aliasDepth int aliasCheck func(aliasCount, constructCount int) error mergedFields map[any]bool } // NewConstructor creates a new Constructor initialized with the provided // options. func NewConstructor(opts *Options) *Constructor { return &Constructor{ stringMapType: stringMapType, generalMapType: generalMapType, KnownFields: opts.KnownFields, UniqueKeys: opts.UniqueKeys, aliases: make(map[*Node]bool), aliasCheck: opts.AliasCheck, } } // -------------------------------------------------------------------------- // Main Entry Point // Construct converts a YAML node into the Go value represented by out. // It dispatches to the appropriate handler based on the node kind and // handles alias expansion, custom unmarshalers, and type resolution. // Returns true if the construction was successful. func (c *Constructor) Construct(n *Node, out reflect.Value) (good bool) { c.constructCount++ if c.aliasDepth > 0 { c.aliasCount++ } if c.aliasCheck != nil { if err := c.aliasCheck(c.aliasCount, c.constructCount); err != nil { Fail(formatConstructorError(err, Mark{Line: n.Line, Column: n.Column})) } } if out.Type() == nodeType { out.Set(reflect.ValueOf(n).Elem()) return true } switch n.Kind { case DocumentNode: return c.document(n, out) case AliasNode: return c.alias(n, out) } out, constructed, good := c.prepare(n, out) if constructed { return good } // When out type implements [encoding.TextUnmarshaler], ensure the node // is a scalar. Otherwise, for example, constructing a YAML mapping // into a struct having no exported fields, but implementing // TextUnmarshaler would silently succeed, but do nothing. // // Note that this matches the behavior of both encoding/json and // encoding/json/v2. if n.Kind != ScalarNode && isTextUnmarshaler(out) { err := fmt.Errorf("cannot construct %s into %s (TextUnmarshaler)", shortTag(n.Tag), out.Type()) c.TypeErrors = append(c.TypeErrors, formatConstructorError(err, Mark{Line: n.Line, Column: n.Column})) return false } switch n.Kind { case ScalarNode: good = c.scalar(n, out) case MappingNode: good = c.mapping(n, out) case SequenceNode: good = c.sequence(n, out) case 0: if n.IsZero() { return c.null(out) } fallthrough default: Fail(formatConstructorError( fmt.Errorf("cannot construct node with unknown kind: '%d'", n.Kind), Mark{Line: n.Line, Column: n.Column}, )) } return good } // -------------------------------------------------------------------------- // Package-level Variables and Constants var ( nodeType = reflect.TypeOf(Node{}) durationType = reflect.TypeOf(time.Duration(0)) stringMapType = reflect.TypeOf(map[string]any{}) generalMapType = reflect.TypeOf(map[any]any{}) ifaceType = generalMapType.Elem() ) // scalarConstructors maps YAML scalar tags to their constructor functions. var scalarConstructors = map[string]ScalarConstructFunc{ strTag: (*Constructor).constructStr, intTag: (*Constructor).constructInt, boolTag: (*Constructor).constructBool, floatTag: (*Constructor).constructFloat, nullTag: (*Constructor).constructNull, timestampTag: (*Constructor).constructTimestamp, binaryTag: (*Constructor).constructBinary, mergeTag: (*Constructor).constructMerge, } // -------------------------------------------------------------------------- // Scalar tag constructors // constructStr constructs a !!str tagged value into various Go types. func (c *Constructor) constructStr(n *Node, resolved any, out reflect.Value) bool { switch out.Kind() { case reflect.String: out.SetString(n.Value) return true case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: // Handle time.Duration parsing from strings like "3s", "1m", // etc. if out.Type() == durationType { d, err := time.ParseDuration(n.Value) if err == nil { out.SetInt(int64(d)) return true } } case reflect.Bool: // YAML 1.1 compatibility: allow string values like "y", "on", // "Off" as bools switch n.Value { case "y", "Y", "yes", "Yes", "YES", "on", "On", "ON": out.SetBool(true) return true case "n", "N", "no", "No", "NO", "off", "Off", "OFF": out.SetBool(false) return true } case reflect.Interface: out.Set(reflect.ValueOf(resolved)) return true } c.tagError(n, strTag, out) return false } // constructInt constructs a !!int tagged value into various Go types. func (c *Constructor) constructInt(n *Node, resolved any, out reflect.Value) bool { switch out.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: isDuration := out.Type() == durationType switch resolved := resolved.(type) { case int: if !isDuration && !out.OverflowInt(int64(resolved)) { out.SetInt(int64(resolved)) return true } else if isDuration && resolved == 0 { out.SetInt(0) return true } case int64: if !isDuration && !out.OverflowInt(resolved) { out.SetInt(resolved) return true } case uint64: if !isDuration && resolved <= math.MaxInt64 { intVal := int64(resolved) if !out.OverflowInt(intVal) { out.SetInt(intVal) return true } } case float64: if !isDuration && resolved >= math.MinInt64 && resolved <= math.MaxInt64 { intVal := int64(resolved) // Verify conversion is lossless (handles // floating-point precision) if float64(intVal) == resolved && !out.OverflowInt(intVal) { out.SetInt(intVal) return true } } case string: if out.Type() == durationType { d, err := time.ParseDuration(resolved) if err == nil { out.SetInt(int64(d)) return true } } } case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: switch resolved := resolved.(type) { case int: if resolved >= 0 && !out.OverflowUint(uint64(resolved)) { out.SetUint(uint64(resolved)) return true } case int64: if resolved >= 0 && !out.OverflowUint(uint64(resolved)) { out.SetUint(uint64(resolved)) return true } case uint64: if !out.OverflowUint(resolved) { out.SetUint(resolved) return true } case float64: if resolved >= 0 && resolved <= math.MaxUint64 { uintVal := uint64(resolved) // Verify conversion is lossless (handles // floating-point precision) if float64(uintVal) == resolved && !out.OverflowUint(uintVal) { out.SetUint(uintVal) return true } } } case reflect.Float32, reflect.Float64: // Allow int to float conversion switch resolved := resolved.(type) { case int: out.SetFloat(float64(resolved)) return true case int64: out.SetFloat(float64(resolved)) return true case uint64: out.SetFloat(float64(resolved)) return true } case reflect.String: // Allow int to string conversion out.SetString(n.Value) return true case reflect.Interface: out.Set(reflect.ValueOf(resolved)) return true } c.tagError(n, intTag, out) return false } // constructBool constructs a !!bool tagged value into various Go types. func (c *Constructor) constructBool(n *Node, resolved any, out reflect.Value) bool { switch out.Kind() { case reflect.Bool: switch resolved := resolved.(type) { case bool: out.SetBool(resolved) return true case string: // This offers some compatibility with the 1.1 spec // (https://yaml.org/type/bool.html). // It only works if explicitly attempting to construct // into a typed bool value. switch resolved { case "y", "Y", "yes", "Yes", "YES", "on", "On", "ON": out.SetBool(true) return true case "n", "N", "no", "No", "NO", "off", "Off", "OFF": out.SetBool(false) return true } } case reflect.String: // Allow bool to be constructed as string (e.g., true -> "true") out.SetString(n.Value) return true case reflect.Interface: out.Set(reflect.ValueOf(resolved)) return true } c.tagError(n, boolTag, out) return false } // constructFloat constructs a !!float tagged value into various Go types. func (c *Constructor) constructFloat(n *Node, resolved any, out reflect.Value) bool { switch out.Kind() { case reflect.Float32, reflect.Float64: switch resolved := resolved.(type) { case int: out.SetFloat(float64(resolved)) return true case int64: out.SetFloat(float64(resolved)) return true case uint64: out.SetFloat(float64(resolved)) return true case float64: out.SetFloat(resolved) return true } case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: // Allow float to int conversion if lossless if fval, ok := resolved.(float64); ok { if fval >= math.MinInt64 && fval <= math.MaxInt64 { intVal := int64(fval) if float64(intVal) == fval && !out.OverflowInt(intVal) { out.SetInt(intVal) return true } } } case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: // Allow float to uint conversion if lossless if fval, ok := resolved.(float64); ok { if fval >= 0 && fval <= math.MaxUint64 { uintVal := uint64(fval) if float64(uintVal) == fval && !out.OverflowUint(uintVal) { out.SetUint(uintVal) return true } } } case reflect.String: out.SetString(n.Value) return true case reflect.Interface: out.Set(reflect.ValueOf(resolved)) return true } c.tagError(n, floatTag, out) return false } // constructTimestamp constructs a !!timestamp tagged value into various Go // types. func (c *Constructor) constructTimestamp(n *Node, resolved any, out reflect.Value) bool { switch out.Kind() { case reflect.Struct: if resolvedv := reflect.ValueOf(resolved); out.Type() == resolvedv.Type() { out.Set(resolvedv) return true } case reflect.Interface: out.Set(reflect.ValueOf(resolved)) return true } c.tagError(n, timestampTag, out) return false } // constructBinary constructs a !!binary tagged value into various Go types. func (c *Constructor) constructBinary(n *Node, resolved any, out reflect.Value) bool { switch out.Kind() { case reflect.String: out.SetString(resolved.(string)) return true case reflect.Slice: // allow decoding !!binary-tagged value into []byte specifically if out.Type().Elem().Kind() == reflect.Uint8 { out.SetBytes([]byte(resolved.(string))) return true } case reflect.Interface: out.Set(reflect.ValueOf(resolved)) return true } c.tagError(n, binaryTag, out) return false } // constructNull constructs a !!null tagged value into various Go types. func (c *Constructor) constructNull(n *Node, resolved any, out reflect.Value) bool { return c.null(out) } // constructMerge handles !!merge tagged keys. // Merge keys are directives, not values, so construction always fails. // They are handled specially by the mapping() function. func (c *Constructor) constructMerge(n *Node, resolved any, out reflect.Value) bool { return false } // -------------------------------------------------------------------------- // Node Kind Handlers // document constructs a DocumentNode by processing its single content node. func (c *Constructor) document(n *Node, out reflect.Value) (good bool) { if len(n.Content) == 1 { c.doc = n c.Construct(n.Content[0], out) return true } return false } // alias constructs an AliasNode by following the alias reference and // tracking alias depth to detect circular references. func (c *Constructor) alias(n *Node, out reflect.Value) (good bool) { if c.aliases[n] { // TODO this could actually be allowed in some circumstances. Fail(formatComposerError( fmt.Sprintf("anchor '%s' value contains itself", n.Value), Mark{Line: n.Line, Column: n.Column}, )) } c.aliases[n] = true c.aliasDepth++ good = c.Construct(n.Alias, out) c.aliasDepth-- delete(c.aliases, n) return good } // scalar constructs a ScalarNode by resolving its tag and value, then // dispatching to the appropriate tag-specific constructor or using // TextUnmarshaler if available. func (c *Constructor) scalar(n *Node, out reflect.Value) bool { // Resolve the tag and value var tag string var resolved any if n.indicatedString() { tag = strTag resolved = n.Value } else { tag, resolved = resolve(n.Tag, n.Value) if tag == binaryTag { data, err := base64.StdEncoding.DecodeString(resolved.(string)) if err != nil { Fail(formatConstructorError( fmt.Errorf("!!binary value contains invalid base64 data"), Mark{Line: n.Line, Column: n.Column}, )) } resolved = string(data) } } // Handle null if resolved == nil { return c.null(out) } // Fast path: exact type match if resolvedv := reflect.ValueOf(resolved); out.Type() == resolvedv.Type() { out.Set(resolvedv) return true } // Handle TextUnmarshaler interface if out.CanAddr() { u, ok := out.Addr().Interface().(encoding.TextUnmarshaler) if ok { var text []byte if tag == binaryTag { text = []byte(resolved.(string)) } else { text = []byte(n.Value) } err := u.UnmarshalText(text) if err != nil { c.TypeErrors = append(c.TypeErrors, formatConstructorError(err, Mark{Line: n.Line, Column: n.Column})) return false } return true } } // Dispatch to tag-specific constructor if constructor, ok := scalarConstructors[tag]; ok { return constructor(c, n, resolved, out) } // Unknown tag - try some fallback behaviors switch out.Kind() { case reflect.Interface: // For interface{} targets, accept any resolved value out.Set(reflect.ValueOf(resolved)) return true case reflect.Struct: // For struct targets with matching types, try direct assignment if resolvedv := reflect.ValueOf(resolved); out.Type() == resolvedv.Type() { out.Set(resolvedv) return true } } // No constructor and no fallback worked c.tagError(n, tag, out) return false } // sequence constructs a SequenceNode into a Go slice, array, or interface. func (c *Constructor) sequence(n *Node, out reflect.Value) (good bool) { l := len(n.Content) var iface reflect.Value switch out.Kind() { case reflect.Slice: out.Set(reflect.MakeSlice(out.Type(), l, l)) case reflect.Array: if l != out.Len() { Fail(formatConstructorError( fmt.Errorf("invalid array: want %d elements but got %d", out.Len(), l), Mark{Line: n.Line, Column: n.Column}, )) } case reflect.Interface: // No type hints. Will have to use a generic sequence. iface = out out = settableValueOf(make([]any, l)) default: c.tagError(n, seqTag, out) return false } et := out.Type().Elem() j := 0 for i := 0; i < l; i++ { e := reflect.New(et).Elem() if ok := c.Construct(n.Content[i], e); ok { out.Index(j).Set(e) j++ } } if out.Kind() != reflect.Array { out.Set(out.Slice(0, j)) } if iface.IsValid() { iface.Set(out) } return true } // mapping constructs a MappingNode into a Go map, struct, or interface. // It handles key uniqueness checking, merge keys, and type-appropriate // map construction (string-keyed vs general). func (c *Constructor) mapping(n *Node, out reflect.Value) (good bool) { l := len(n.Content) if c.UniqueKeys { nerrs := len(c.TypeErrors) for i := 0; i < l; i += 2 { ni := n.Content[i] for j := i + 2; j < l; j += 2 { nj := n.Content[j] if ni.Kind == nj.Kind && ni.Value == nj.Value { c.TypeErrors = append(c.TypeErrors, formatConstructorError( fmt.Errorf("mapping key %#v already defined at line %d", nj.Value, ni.Line), Mark{Line: nj.Line, Column: nj.Column}, )) } } } if len(c.TypeErrors) > nerrs { return false } } switch out.Kind() { case reflect.Struct: return c.mappingStruct(n, out) case reflect.Map: // okay case reflect.Interface: iface := out if isStringMap(n) { out = reflect.MakeMap(c.stringMapType) } else { out = reflect.MakeMap(c.generalMapType) } iface.Set(out) default: c.tagError(n, mapTag, out) return false } outt := out.Type() kt := outt.Key() et := outt.Elem() stringMapType := c.stringMapType generalMapType := c.generalMapType if outt.Elem() == ifaceType { if outt.Key().Kind() == reflect.String { c.stringMapType = outt } else if outt.Key() == ifaceType { c.generalMapType = outt } } mergedFields := c.mergedFields c.mergedFields = nil var mergeNode *Node mapIsNew := false if out.IsNil() { out.Set(reflect.MakeMap(outt)) mapIsNew = true } for i := 0; i < l; i += 2 { if isMerge(n.Content[i]) { mergeNode = n.Content[i+1] continue } k := reflect.New(kt).Elem() if c.Construct(n.Content[i], k) { if mergedFields != nil { ki := k.Interface() if c.getPossiblyUnhashableKey(mergedFields, ki, n.Content[i]) { continue } c.setPossiblyUnhashableKey(mergedFields, ki, true, n.Content[i]) } kkind := k.Kind() if kkind == reflect.Interface { kkind = k.Elem().Kind() } if kkind == reflect.Map || kkind == reflect.Slice { Fail(formatConstructorError( fmt.Errorf("cannot use '%#v' as a map key; try decoding into yaml.Node", k.Interface()), Mark{Line: n.Content[i].Line, Column: n.Content[i].Column}, )) } e := reflect.New(et).Elem() if c.Construct(n.Content[i+1], e) || n.Content[i+1].ShortTag() == nullTag && (mapIsNew || !out.MapIndex(k).IsValid()) { out.SetMapIndex(k, e) } } } c.mergedFields = mergedFields if mergeNode != nil { c.merge(n, mergeNode, out) } c.stringMapType = stringMapType c.generalMapType = generalMapType return true } // -------------------------------------------------------------------------- // Mapping/Struct Support // mappingStruct constructs a MappingNode into a struct value. // It handles field matching by name, inline fields, inline maps, merge keys, // and enforces known fields and unique keys when configured. func (c *Constructor) mappingStruct(n *Node, out reflect.Value) (good bool) { sinfo, err := getStructInfo(out.Type()) if err != nil { panic(err) } var inlineMap reflect.Value var elemType reflect.Type if sinfo.InlineMap != -1 { inlineMap = out.Field(sinfo.InlineMap) elemType = inlineMap.Type().Elem() } for _, index := range sinfo.InlineConstructors { field := c.fieldByIndex(n, out, index) c.prepare(n, field) } mergedFields := c.mergedFields c.mergedFields = nil var mergeNode *Node var doneFields []bool if c.UniqueKeys { doneFields = make([]bool, len(sinfo.FieldsList)) } name := settableValueOf("") l := len(n.Content) for i := 0; i < l; i += 2 { ni := n.Content[i] if isMerge(ni) { mergeNode = n.Content[i+1] continue } if !c.Construct(ni, name) { continue } sname := name.String() if mergedFields != nil { if mergedFields[sname] { continue } mergedFields[sname] = true } if info, ok := sinfo.FieldsMap[sname]; ok { if c.UniqueKeys { if doneFields[info.Id] { c.TypeErrors = append(c.TypeErrors, formatConstructorError( fmt.Errorf("field %s already set in type %s", name.String(), out.Type()), Mark{Line: ni.Line, Column: ni.Column}, )) continue } doneFields[info.Id] = true } var field reflect.Value if info.Inline == nil { field = out.Field(info.Num) } else { field = c.fieldByIndex(n, out, info.Inline) } c.Construct(n.Content[i+1], field) } else if sinfo.InlineMap != -1 { if inlineMap.IsNil() { inlineMap.Set(reflect.MakeMap(inlineMap.Type())) } value := reflect.New(elemType).Elem() c.Construct(n.Content[i+1], value) inlineMap.SetMapIndex(name, value) } else if c.KnownFields { c.TypeErrors = append(c.TypeErrors, formatConstructorError( fmt.Errorf("field %s not found in type %s", name.String(), out.Type()), Mark{Line: ni.Line, Column: ni.Column}, )) } } c.mergedFields = mergedFields if mergeNode != nil { c.merge(n, mergeNode, out) } return true } // merge processes a merge key (<<) by constructing the merge value into out. // The merge value can be a single mapping, an alias to a mapping, or a // sequence of mappings. // Fields from the parent mapping take precedence over merged fields. func (c *Constructor) merge(parent *Node, merge *Node, out reflect.Value) { mergedFields := c.mergedFields if mergedFields == nil { c.mergedFields = make(map[any]bool) for i := 0; i < len(parent.Content); i += 2 { k := reflect.New(ifaceType).Elem() if c.Construct(parent.Content[i], k) { c.setPossiblyUnhashableKey(c.mergedFields, k.Interface(), true, parent.Content[i]) } } } switch merge.Kind { case MappingNode: c.Construct(merge, out) case AliasNode: if merge.Alias != nil && merge.Alias.Kind != MappingNode { failWantMap(merge.Alias) } c.Construct(merge, out) case SequenceNode: for i := 0; i < len(merge.Content); i++ { ni := merge.Content[i] if ni.Kind == AliasNode { if ni.Alias != nil && ni.Alias.Kind != MappingNode { failWantMap(ni.Alias) } } else if ni.Kind != MappingNode { failWantMap(ni) } c.Construct(ni, out) } default: failWantMap(merge) } c.mergedFields = mergedFields } // isStringMap checks if a MappingNode has only string or merge keys. // This determines whether to use map[string]any or map[any]any when // constructing into an interface{}. func isStringMap(n *Node) bool { if n.Kind != MappingNode { return false } l := len(n.Content) for i := 0; i < l; i += 2 { shortTag := n.Content[i].ShortTag() if shortTag != strTag && shortTag != mergeTag { return false } } return true } // isMerge checks if a node is a merge key (!!merge tag). func isMerge(n *Node) bool { return n.Kind == ScalarNode && shortTag(n.Tag) == mergeTag } // failWantMap panics with an error message for invalid merge key values. func failWantMap(n *Node) { Fail(formatConstructorError( fmt.Errorf("map merge requires map or sequence of maps as the value"), Mark{Line: n.Line, Column: n.Column}, )) } // -------------------------------------------------------------------------- // Utility Methods // prepare initializes and dereferences pointers and calls UnmarshalYAML // if a value is found to implement it. // It returns the initialized and dereferenced out value, whether // construction was already done by UnmarshalYAML, and if so whether // its types constructed appropriately. // // If n holds a null value, prepare returns before doing anything. func (c *Constructor) prepare(n *Node, out reflect.Value) (newout reflect.Value, constructed, good bool) { if n.ShortTag() == nullTag { return out, false, false } again := true for again { again = false if out.Kind() == reflect.Pointer { if out.IsNil() { out.Set(reflect.New(out.Type().Elem())) } out = out.Elem() again = true } if out.CanAddr() { // Try yaml.Unmarshaler (from root package) first if called, good := c.tryCallYAMLConstructor(n, out); called { return out, true, good } outi := out.Addr().Interface() // Check for libyaml.constructor if u, ok := outi.(constructor); ok { good = c.callConstructor(n, u) return out, true, good } if u, ok := outi.(legacyConstructor); ok { good = c.callLegacyConstructor(n, u) return out, true, good } } } return out, false, false } // fieldByIndex returns the struct field at the given index path, initializing // any nil pointers along the way. func (c *Constructor) fieldByIndex(n *Node, v reflect.Value, index []int) (field reflect.Value) { if n.ShortTag() == nullTag { return reflect.Value{} } for _, num := range index { for { if v.Kind() == reflect.Pointer { if v.IsNil() { v.Set(reflect.New(v.Type().Elem())) } v = v.Elem() continue } break } v = v.Field(num) } return v } // tryCallYAMLConstructor checks if the value has an UnmarshalYAML method that // takes a *Node from an allowlisted v3 yaml package and calls it if found. // This handles backward compatibility with types that implement the v3 // yaml.Unmarshaler interface instead of the native libyaml.constructor. func (c *Constructor) tryCallYAMLConstructor(n *Node, out reflect.Value) (called bool, good bool) { if !out.CanAddr() { return false, false } addr := out.Addr() // Check for UnmarshalYAML method method := addr.MethodByName("UnmarshalYAML") if !method.IsValid() { return false, false } // Check method signature: func(*yaml.Node) error mtype := method.Type() if mtype.NumIn() != 1 || mtype.NumOut() != 1 { return false, false } // Check if parameter is a pointer to a Node-like struct paramType := mtype.In(0) if paramType.Kind() != reflect.Ptr { return false, false } elemType := paramType.Elem() if elemType.Kind() != reflect.Struct { return false, false } // Only accept *Node from allowlisted v3 yaml packages whose Node type // is assumed to have a compatible memory layout with libyaml.Node. // The unsafe pointer cast below is only safe for these packages. if elemType.Name() != "Node" || !isYAMLNodePkg(elemType.PkgPath()) { return false, false } // Return type must be error retType := mtype.Out(0) if retType.Kind() != reflect.Interface || retType.Name() != "error" { return false, false } // Call the method with a converted node. // The allowlisted v3 packages define their own Node type that is // assumed to have a compatible memory layout with libyaml.Node. nodeValue := reflect.NewAt(elemType, reflect.ValueOf(n).UnsafePointer()) results := method.Call([]reflect.Value{nodeValue}) err := results[0].Interface() if err == nil { return true, true } switch e := err.(type) { case *LoadErrors: c.TypeErrors = append(c.TypeErrors, e.Errors...) return true, false default: c.TypeErrors = append(c.TypeErrors, formatConstructorError( err.(error), Mark{Line: n.Line, Column: n.Column}, )) return true, false } } // callConstructor invokes the UnmarshalYAML method on a value implementing // the constructor interface, handling errors appropriately. func (c *Constructor) callConstructor(n *Node, u constructor) (good bool) { err := u.UnmarshalYAML(n) switch e := err.(type) { case nil: return true case *LoadErrors: c.TypeErrors = append(c.TypeErrors, e.Errors...) return false default: c.TypeErrors = append(c.TypeErrors, formatConstructorError( err, Mark{Line: n.Line, Column: n.Column}, )) return false } } // callLegacyConstructor invokes the UnmarshalYAML method on a value // implementing the old-style legacyConstructor interface. func (c *Constructor) callLegacyConstructor(n *Node, u legacyConstructor) (good bool) { terrlen := len(c.TypeErrors) err := u.UnmarshalYAML(func(v any) (err error) { defer handleErr(&err) c.Construct(n, reflect.ValueOf(v)) if len(c.TypeErrors) > terrlen { issues := c.TypeErrors[terrlen:] c.TypeErrors = c.TypeErrors[:terrlen] return &LoadErrors{issues} } return nil }) switch e := err.(type) { case nil: return true case *LoadErrors: c.TypeErrors = append(c.TypeErrors, e.Errors...) return false default: c.TypeErrors = append(c.TypeErrors, formatConstructorError( err, Mark{Line: n.Line, Column: n.Column}, )) return false } } // tagError records a type construction error indicating that a node with a // given tag cannot be constructed into the target type. func (c *Constructor) tagError(n *Node, tag string, out reflect.Value) { if n.Tag != "" { tag = n.Tag } value := n.Value if tag != seqTag && tag != mapTag { if len(value) > 10 { value = " `" + value[:7] + "...`" } else { value = " `" + value + "`" } } c.TypeErrors = append(c.TypeErrors, formatConstructorError( fmt.Errorf("cannot construct %s%s into %s", shortTag(tag), value, out.Type()), Mark{Line: n.Line, Column: n.Column}, )) } // null constructs a null value by setting the target to its zero value. // Only works for nillable types (interface, pointer, map, slice). func (c *Constructor) null(out reflect.Value) bool { if out.CanAddr() { switch out.Kind() { case reflect.Interface, reflect.Pointer, reflect.Map, reflect.Slice: out.Set(reflect.Zero(out.Type())) return true } } return false } // isTextUnmarshaler checks if a value implements [encoding.TextUnmarshaler]. // It dereferences pointers to check the underlying type. func isTextUnmarshaler(out reflect.Value) bool { // Dereference pointers to check the underlying type, // similar to how prepare() handles Constructor checks. for out.Kind() == reflect.Pointer { if out.IsNil() { // Create a new instance to check the type out = reflect.New(out.Type().Elem()).Elem() } else { out = out.Elem() } } if out.CanAddr() { _, ok := out.Addr().Interface().(encoding.TextUnmarshaler) return ok } return false } // settableValueOf returns a settable [reflect.Value] for the given value. func settableValueOf(i any) reflect.Value { v := reflect.ValueOf(i) sv := reflect.New(v.Type()).Elem() sv.Set(v) return sv } // setPossiblyUnhashableKey sets a map key, recovering from panics if the key // type is unhashable. func (c *Constructor) setPossiblyUnhashableKey(m map[any]bool, key any, value bool, n *Node) { defer func() { if err := recover(); err != nil { Fail(formatConstructorError( fmt.Errorf("%v", err), Mark{Line: n.Line, Column: n.Column}, )) } }() m[key] = value } // getPossiblyUnhashableKey gets a map key value, recovering from panics if // the key type is unhashable. func (c *Constructor) getPossiblyUnhashableKey(m map[any]bool, key any, n *Node) bool { defer func() { if err := recover(); err != nil { Fail(formatConstructorError( fmt.Errorf("%v", err), Mark{Line: n.Line, Column: n.Column}, )) } }() return m[key] } // formatConstructorError creates a LoadError for constructor-stage errors. func formatConstructorError(err error, mark Mark) *LoadError { return &LoadError{ Stage: ConstructorStage, Mark: mark, Message: err.Error(), err: err, } } // formatConstructorErrorContext creates a LoadError with both context and // problem information for constructor-stage errors. func formatConstructorErrorContext(context string, contextMark Mark, err error, mark Mark) *LoadError { return &LoadError{ Stage: ConstructorStage, ContextMark: contextMark, ContextMsg: context, Mark: mark, Message: err.Error(), err: err, } } golang-go.yaml-yaml-v4-4.0.0~rc5/internal/libyaml/constructor_test.go000066400000000000000000000014231521353103300256410ustar00rootroot00000000000000// Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Tests for the constructor stage. // Verifies YAML node to Go value conversion and error handling. package libyaml import ( "reflect" "testing" "go.yaml.in/yaml/v4/internal/testutil/assert" ) func TestConstructor(t *testing.T) { RunTestCases(t, "constructor.yaml", map[string]TestHandler{ "scalar-resolution": func(t *testing.T, tc TestCase) { t.Helper() // Load the YAML result, err := LoadAny([]byte(tc.Yaml)) assert.NoErrorf(t, err, "LoadAny() error: %v", err) // Compare the result with expected value if !reflect.DeepEqual(result, tc.Want) { t.Errorf("LoadAny() = %v (type: %T), want %v (type: %T)", result, result, tc.Want, tc.Want) } }, }) } golang-go.yaml-yaml-v4-4.0.0~rc5/internal/libyaml/desolver.go000066400000000000000000000107621521353103300240460ustar00rootroot00000000000000// Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Desolver stage: Removes inferable tags from YAML nodes. // This is the inverse of the Resolver - it walks a tagged node tree and // removes tags that can be inferred during parsing, producing cleaner YAML // output without unnecessary type annotations. package libyaml // Desolver handles tag removal for YAML nodes during serialization. // It removes tags that would be automatically resolved to the same type // during parsing, making the output cleaner and more readable. type Desolver struct { opts *Options } // NewDesolver creates a new Desolver with the given options. func NewDesolver(opts *Options) *Desolver { return &Desolver{opts: opts} } // Desolve walks the node tree and removes tags that can be inferred. // This is the inverse of Resolver - it takes a fully-tagged node tree // (from Representer) and removes unnecessary tags to produce clean output. // // For scalar nodes: if the value would resolve to the same tag when parsed, // the tag is removed. For strings that would resolve differently, the tag is // removed and quoting style is set to preserve the string type. // // For collection nodes (maps/sequences): default tags (!!map, !!seq) are // removed since they're implied by the structure. func (d *Desolver) Desolve(n *Node) { if n == nil { return } switch n.Kind { case ScalarNode: d.desolveScalar(n) case DocumentNode, SequenceNode, MappingNode: d.desolveCollection(n) // Recursively desolve children for _, child := range n.Content { d.Desolve(child) } case AliasNode: // Alias nodes don't have tags to remove } } // desolveScalar removes tags from scalar nodes when they can be inferred. func (d *Desolver) desolveScalar(n *Node) { // If explicitly tagged by user (TaggedStyle), keep it if n.Style&TaggedStyle != 0 { return } // Empty tag means it's already untagged - nothing to do if n.Tag == "" { return } stag := shortTag(n.Tag) // Check if this is a standard scalar tag that we can potentially remove isStandardTag := false switch stag { case nullTag, boolTag, strTag, intTag, floatTag, timestampTag: isStandardTag = true case binaryTag: // Binary scalars are not implicitly resolvable - never remove. return case mergeTag: // Elide the implicit !!merge tag when the value is the canonical // merge key marker. The TaggedStyle early-return above already // preserves !!merge when it was explicit in the source. if n.Value == "<<" { n.Tag = "" } return default: // Custom tag - preserve it return } // Only process standard tags from here if !isStandardTag { return } // What tag would this value resolve to? rtag, _ := resolve("", n.Value) // If resolved tag matches current tag, we can elide the tag if rtag == stag { // Tag can be inferred - remove it n.Tag = "" } else if stag == strTag { // This is a string type, but would resolve to something else. // Remove the tag and force quoting to preserve string type. n.Tag = "" // If not already quoted, set quote style based on content if n.Style&(SingleQuotedStyle|DoubleQuotedStyle|LiteralStyle|FoldedStyle) == 0 { // Determine quote style based on options or default to single quotes if d.opts != nil { // Convert ScalarStyle to Style switch d.opts.QuotePreference.ScalarStyle() { case DOUBLE_QUOTED_SCALAR_STYLE: n.Style |= DoubleQuotedStyle default: n.Style |= SingleQuotedStyle } } else { n.Style |= SingleQuotedStyle } } } else if stag == floatTag || stag == intTag { // For numeric type mismatches (like float64(1) → "1" with !!float tag): // Elide the tag and let YAML resolve naturally. // Without the tag, "1" resolves as !!int, which may change the type, // but that's acceptable for cleaner output (and matches old behavior). n.Tag = "" } // For other standard tags with mismatches, keep the tag to preserve type } // desolveCollection removes default tags from collection nodes. func (d *Desolver) desolveCollection(n *Node) { // If explicitly tagged by user, keep it if n.Style&TaggedStyle != 0 { return } stag := shortTag(n.Tag) switch n.Kind { case MappingNode: // !!map is the default for mappings - remove it if stag == mapTag { n.Tag = "" } case SequenceNode: // !!seq is the default for sequences - remove it if stag == seqTag { n.Tag = "" } case DocumentNode: // Documents don't have tags in YAML output n.Tag = "" } // For other tags, keep them - they're explicit type information } golang-go.yaml-yaml-v4-4.0.0~rc5/internal/libyaml/desolver_test.go000066400000000000000000000052111521353103300250760ustar00rootroot00000000000000// Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Tests for the Desolver stage package libyaml import ( "testing" "go.yaml.in/yaml/v4/internal/testutil/assert" ) func TestDesolver(t *testing.T) { RunTestCases(t, "desolver.yaml", map[string]TestHandler{ "desolve-inferable": func(t *testing.T, tc TestCase) { t.Helper() node := &Node{ Kind: ScalarNode, Tag: tc.Node.Tag, Value: tc.Node.Value, } d := NewDesolver(nil) d.Desolve(node) // Extract want fields from tc.Want (type any) wantMap := tc.Want.(map[string]any) wantTag := wantMap["tag"].(string) // Check tag assert.Equal(t, wantTag, node.Tag) // Check style if wantStyle, ok := wantMap["style"].(string); ok { hasQuote := node.Style&(SingleQuotedStyle|DoubleQuotedStyle) != 0 switch wantStyle { case "Plain": assert.False(t, hasQuote) case "Single": assert.True(t, hasQuote) } } }, "desolve-preserve": func(t *testing.T, tc TestCase) { t.Helper() node := &Node{ Kind: ScalarNode, Tag: tc.Node.Tag, } // Handle kind for collection tests if tc.Node.Kind != "" { switch tc.Node.Kind { case "Mapping": node.Kind = MappingNode case "Sequence": node.Kind = SequenceNode } } else { // Scalar node needs value node.Value = tc.Node.Value } // Handle style for explicitly tagged tests if tc.Node.Style == "Tagged" { node.Style = TaggedStyle } d := NewDesolver(nil) d.Desolve(node) // Extract want fields wantMap := tc.Want.(map[string]any) wantTag := wantMap["tag"].(string) // Check tag is preserved assert.Equal(t, wantTag, node.Tag) // Check style if present if wantStyle, ok := wantMap["style"].(string); ok { hasQuote := node.Style&(SingleQuotedStyle|DoubleQuotedStyle) != 0 switch wantStyle { case "Plain": assert.False(t, hasQuote) case "Single": assert.True(t, hasQuote) } } }, "desolve-string-quoting": func(t *testing.T, tc TestCase) { t.Helper() node := &Node{ Kind: ScalarNode, Tag: tc.Node.Tag, Value: tc.Node.Value, } d := NewDesolver(nil) d.Desolve(node) // Extract want fields wantMap := tc.Want.(map[string]any) wantTag := wantMap["tag"].(string) // Check tag removed assert.Equal(t, wantTag, node.Tag) // Check style if wantStyle, ok := wantMap["style"].(string); ok { hasQuote := node.Style&(SingleQuotedStyle|DoubleQuotedStyle) != 0 switch wantStyle { case "Plain": assert.False(t, hasQuote) case "Single": assert.True(t, hasQuote) } } }, }) } golang-go.yaml-yaml-v4-4.0.0~rc5/internal/libyaml/doc.go000066400000000000000000000004521521353103300227630ustar00rootroot00000000000000// Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Package libyaml contains internal helpers for working with YAML // // It's a reworked version of the original libyaml package from go-yaml v2/v3, // adapted to work with Go specifications package libyaml golang-go.yaml-yaml-v4-4.0.0~rc5/internal/libyaml/dumper.go000066400000000000000000000100111521353103300235020ustar00rootroot00000000000000// Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // This file contains the Dumper API for writing YAML documents. // // Primary functions: // - Dump: Encode value(s) to YAML (use WithAll for multi-doc) // - NewDumper: Create a streaming dumper to io.Writer package libyaml import ( "bytes" "io" "reflect" ) // A Dumper writes YAML values to an output stream with configurable options. // It uses a 3-stage pipeline mirroring the Loader: // 1. Representer: Go values → Tagged Node tree // 2. Desolver: Remove inferable tags // 3. Serializer: Node tree → Events → YAML type Dumper struct { representer *Representer desolver *Desolver serializer *Serializer options *Options } // NewDumper returns a new Dumper that writes to w with the given options. // // The Dumper should be closed after use to flush all data to w. func NewDumper(w io.Writer, opts ...Option) (*Dumper, error) { o, err := ApplyOptions(opts...) if err != nil { return nil, err } return &Dumper{ representer: NewRepresenter(o), // No writer - builds nodes desolver: NewDesolver(o), serializer: NewSerializer(w, o), // Writer here - emits YAML options: o, }, nil } // Dump encodes a value to YAML with the given options. // // By default, Dump encodes a single value as a single YAML document. // // Use WithAllDocuments() to encode multiple values as a multi-document stream: // // docs := []Config{config1, config2, config3} // yaml.Dump(docs, yaml.WithAllDocuments()) // // When WithAllDocuments is used, in must be a slice. // Each element is encoded as a separate YAML document with "---" separators. // // See [Marshal] for details about the conversion of Go values to YAML. func Dump(in any, opts ...Option) (out []byte, err error) { defer handleErr(&err) o, err := ApplyOptions(opts...) if err != nil { return nil, err } var buf bytes.Buffer d, err := NewDumper(&buf, func(opts *Options) error { *opts = *o // Copy options return nil }) if err != nil { return nil, err } if o.AllDocuments { // Multi-document mode: in must be a slice inVal := reflect.ValueOf(in) if inVal.Kind() != reflect.Slice { return nil, &DumpError{ Stage: RepresenterStage, Message: "WithAllDocuments requires a slice input", } } // Dump each element as a separate document for i := 0; i < inVal.Len(); i++ { if err := d.Dump(inVal.Index(i).Interface()); err != nil { return nil, err } } } else { // Single-document mode if err := d.Dump(in); err != nil { return nil, err } } if err := d.Close(); err != nil { return nil, err } return buf.Bytes(), nil } // Dump writes the YAML encoding of v to the stream. // // If multiple values are dumped to the stream, the second and subsequent // documents will be preceded with a "---" document separator. // // See the documentation for [Marshal] for details about the conversion of Go // values to YAML. func (d *Dumper) Dump(v any) (err error) { defer handleErr(&err) // Stage 1: Represent - Go values → Tagged Node tree node := d.representer.Represent("", reflect.ValueOf(v)) // Stage 2: Desolve - Remove inferable tags d.desolver.Desolve(node) // Stage 3: Serialize - Node tree → Events → YAML d.serializer.Serialize(node) return nil } // Close closes the Dumper by writing any remaining data. // It does not write a stream terminating string "...". func (d *Dumper) Close() (err error) { defer handleErr(&err) d.serializer.Finish() return nil } // SetIndent changes the indentation used when encoding. // This is used by the legacy Encoder.SetIndent() method. func (d *Dumper) SetIndent(spaces int) { if spaces < 0 { failDumpf(SerializerStage, "cannot indent to a negative number of spaces") } // Set on serializer's emitter d.serializer.Emitter.BestIndent = spaces } // SetCompactSeqIndent controls whether '- ' is considered part of the indentation. // This is used by the legacy Encoder methods. func (d *Dumper) SetCompactSeqIndent(compact bool) { d.serializer.Emitter.CompactSequenceIndent = compact } golang-go.yaml-yaml-v4-4.0.0~rc5/internal/libyaml/dumper_test.go000066400000000000000000000050201521353103300245450ustar00rootroot00000000000000// Tests for the Dump API, including WithAllDocuments functionality. package libyaml import ( "strings" "testing" "go.yaml.in/yaml/v4/internal/testutil/assert" ) // TestDump_SingleValue tests dumping a single value func TestDump_SingleValue(t *testing.T) { type Config struct { Name string `yaml:"name"` } config := Config{Name: "myconfig"} data, err := Dump(config) assert.NoError(t, err) // Should not have document separator for single document assert.True(t, strings.Contains(string(data), "name: myconfig")) } // TestDumpWithAllDocuments_TypedSlice tests dumping multiple values from typed slice func TestDumpWithAllDocuments_TypedSlice(t *testing.T) { type Config struct { Name string `yaml:"name"` } configs := []Config{ {Name: "first"}, {Name: "second"}, {Name: "third"}, } data, err := Dump(configs, WithAllDocuments()) assert.NoError(t, err) // Should have document separators assert.True(t, strings.Contains(string(data), "---")) assert.True(t, strings.Contains(string(data), "name: first")) assert.True(t, strings.Contains(string(data), "name: second")) assert.True(t, strings.Contains(string(data), "name: third")) } // TestDumpWithAllDocuments_UntypedSlice tests dumping multiple values from []any func TestDumpWithAllDocuments_UntypedSlice(t *testing.T) { docs := []any{ map[string]string{"name": "first"}, map[string]string{"name": "second"}, } data, err := Dump(docs, WithAllDocuments()) assert.NoError(t, err) // Should have document separator assert.True(t, strings.Contains(string(data), "---")) assert.True(t, strings.Contains(string(data), "name: first")) assert.True(t, strings.Contains(string(data), "name: second")) } // TestDumpWithAllDocuments_EmptySlice tests dumping an empty slice func TestDumpWithAllDocuments_EmptySlice(t *testing.T) { var docs []any data, err := Dump(docs, WithAllDocuments()) // Empty slice produces an empty YAML stream // This may produce an error or empty output depending on implementation if err != nil { // It's acceptable for empty slice to produce error t.Logf("Empty slice produced error (acceptable): %v", err) } else { // Or it might produce empty/minimal output assert.True(t, len(data) < 50) } } // TestDumpWithAllDocuments_NonSlice tests that WithAllDocuments with non-slice returns error func TestDumpWithAllDocuments_NonSlice(t *testing.T) { single := map[string]string{"name": "single"} _, err := Dump(single, WithAllDocuments()) assert.NotNil(t, err) assert.ErrorMatches(t, ".*WithAllDocuments requires a slice input.*", err) } golang-go.yaml-yaml-v4-4.0.0~rc5/internal/libyaml/emitter.go000066400000000000000000001753431521353103300237030ustar00rootroot00000000000000// Copyright 2006-2010 Kirill Simonov // Copyright 2011-2019 Canonical Ltd // Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 AND MIT // Emitter stage: Generates YAML output from events. // Handles formatting, indentation, line wrapping, and output buffering. package libyaml import ( "bytes" "fmt" "io" ) // WriteHandler is called when the [Emitter] needs to flush the accumulated // characters to the output. The handler should write @a size bytes of the // @a buffer to the output. // // @param[in,out] data A pointer to an application data specified by // yamlEmitter.setOutput(). // @param[in] buffer The buffer with bytes to be written. // @param[in] size The size of the buffer. // // @returns On success, the handler should return @c 1. If the handler failed, // the returned value should be @c 0. type WriteHandler func(emitter *Emitter, buffer []byte) error // EmitterState represents the current state of the emitter. type EmitterState int // The emitter states. const ( // Expect STREAM-START. EMIT_STREAM_START_STATE EmitterState = iota EMIT_FIRST_DOCUMENT_START_STATE // Expect the first DOCUMENT-START or STREAM-END. EMIT_DOCUMENT_START_STATE // Expect DOCUMENT-START or STREAM-END. EMIT_DOCUMENT_CONTENT_STATE // Expect the content of a document. EMIT_DOCUMENT_END_STATE // Expect DOCUMENT-END. EMIT_FLOW_SEQUENCE_FIRST_ITEM_STATE // Expect the first item of a flow sequence. EMIT_FLOW_SEQUENCE_TRAIL_ITEM_STATE // Expect the next item of a flow sequence, with the comma already written out EMIT_FLOW_SEQUENCE_ITEM_STATE // Expect an item of a flow sequence. EMIT_FLOW_MAPPING_FIRST_KEY_STATE // Expect the first key of a flow mapping. EMIT_FLOW_MAPPING_TRAIL_KEY_STATE // Expect the next key of a flow mapping, with the comma already written out EMIT_FLOW_MAPPING_KEY_STATE // Expect a key of a flow mapping. EMIT_FLOW_MAPPING_SIMPLE_VALUE_STATE // Expect a value for a simple key of a flow mapping. EMIT_FLOW_MAPPING_VALUE_STATE // Expect a value of a flow mapping. EMIT_BLOCK_SEQUENCE_FIRST_ITEM_STATE // Expect the first item of a block sequence. EMIT_BLOCK_SEQUENCE_ITEM_STATE // Expect an item of a block sequence. EMIT_BLOCK_MAPPING_FIRST_KEY_STATE // Expect the first key of a block mapping. EMIT_BLOCK_MAPPING_KEY_STATE // Expect the key of a block mapping. EMIT_BLOCK_MAPPING_SIMPLE_VALUE_STATE // Expect a value for a simple key of a block mapping. EMIT_BLOCK_MAPPING_VALUE_STATE // Expect a value of a block mapping. EMIT_END_STATE // Expect nothing. ) // Emitter holds all information about the current state of the emitter. type Emitter struct { // Writer stuff write_handler WriteHandler // Write handler. output_buffer *[]byte // String output data. output_writer io.Writer // File output data. buffer []byte // The working buffer. buffer_pos int // The current position of the buffer. encoding Encoding // The stream encoding. // Emitter stuff canonical bool // If the output is in the canonical style? BestIndent int // The number of indentation spaces. best_width int // The preferred width of the output lines. unicode bool // Allow unescaped non-ASCII characters? line_break LineBreak // The preferred line break. quotePreference QuoteStyle // Preferred quote style when quoting is required. state EmitterState // The current emitter state. states []EmitterState // The stack of states. events []Event // The event queue. events_head int // The head of the event queue. indents []int // The stack of indentation levels. tag_directives []TagDirective // The list of tag directives. indent int // The current indentation level. CompactSequenceIndent bool // Is '- ' is considered part of the indentation for sequence elements? flow_level int // The current flow level. root_context bool // Is it the document root context? sequence_context bool // Is it a sequence context? mapping_context bool // Is it a mapping context? simple_key_context bool // Is it a simple mapping key context? line int // The current line. column int // The current column. whitespace bool // If the last character was a whitespace? indention bool // If the last character was an indentation character (' ', '-', '?', ':')? OpenEnded bool // If an explicit document end is required? space_above bool // Is there's an empty line above? foot_indent int // The indent used to write the foot comment above, or -1 if none. // Anchor analysis. anchor_data struct { anchor []byte // The anchor value. alias bool // Is it an alias? } // Tag analysis. tag_data struct { handle []byte // The tag handle. suffix []byte // The tag suffix. } // Scalar analysis. scalar_data struct { value []byte // The scalar value. multiline bool // Does the scalar contain line breaks? flow_plain_allowed bool // Can the scalar be expressed in the flow plain style? block_plain_allowed bool // Can the scalar be expressed in the block plain style? single_quoted_allowed bool // Can the scalar be expressed in the single quoted style? block_allowed bool // Can the scalar be expressed in the literal or folded styles? style ScalarStyle // The output style. } // Comments HeadComment []byte LineComment []byte FootComment []byte TailComment []byte key_line_comment []byte // Representer stuff opened bool // If the stream was already opened? closed bool // If the stream was already closed? // The information associated with the document nodes. anchors *struct { references int // The number of references. anchor int // The anchor id. serialized bool // If the node has been emitted? } last_anchor_id int // The last assigned anchor id. } // NewEmitter creates a new emitter object. func NewEmitter() Emitter { return Emitter{ buffer: make([]byte, output_buffer_size), states: make([]EmitterState, 0, initial_stack_size), events: make([]Event, 0, initial_queue_size), best_width: -1, } } // Emit an event. func (emitter *Emitter) Emit(event *Event) error { emitter.events = append(emitter.events, *event) for !emitter.needMoreEvents() { event := &emitter.events[emitter.events_head] if err := emitter.analyzeEvent(event); err != nil { return err } if err := emitter.stateMachine(event); err != nil { return err } event.Delete() emitter.events_head++ } return nil } // Delete an emitter object. func (emitter *Emitter) Delete() { *emitter = Emitter{} } // String write handler. func yamlStringWriteHandler(emitter *Emitter, buffer []byte) error { *emitter.output_buffer = append(*emitter.output_buffer, buffer...) return nil } // yamlWriterWriteHandler uses emitter.output_writer to write the // emitted text. func yamlWriterWriteHandler(emitter *Emitter, buffer []byte) error { _, err := emitter.output_writer.Write(buffer) return err } // SetOutputString sets a string output. func (emitter *Emitter) SetOutputString(output_buffer *[]byte) { if emitter.write_handler != nil { panic("must set the output target only once") } emitter.write_handler = yamlStringWriteHandler emitter.output_buffer = output_buffer } // SetOutputWriter sets a file output. func (emitter *Emitter) SetOutputWriter(w io.Writer) { if emitter.write_handler != nil { panic("must set the output target only once") } emitter.write_handler = yamlWriterWriteHandler emitter.output_writer = w } // SetEncoding sets the output encoding. func (emitter *Emitter) SetEncoding(encoding Encoding) { if emitter.encoding != ANY_ENCODING { panic("must set the output encoding only once") } emitter.encoding = encoding } // SetCanonical sets the canonical output style. func (emitter *Emitter) SetCanonical(canonical bool) { emitter.canonical = canonical } // SetIndent sets the indentation increment. func (emitter *Emitter) SetIndent(indent int) { if indent < 2 || indent > 9 { indent = 2 } emitter.BestIndent = indent } // SetWidth sets the preferred line width. func (emitter *Emitter) SetWidth(width int) { if width < 0 { width = -1 } emitter.best_width = width } // SetUnicode sets if unescaped non-ASCII characters are allowed. func (emitter *Emitter) SetUnicode(unicode bool) { emitter.unicode = unicode } // SetLineBreak sets the preferred line break character. func (emitter *Emitter) SetLineBreak(line_break LineBreak) { emitter.line_break = line_break } // State dispatcher. func (emitter *Emitter) stateMachine(event *Event) error { switch emitter.state { default: case EMIT_STREAM_START_STATE: return emitter.emitStreamStart(event) case EMIT_FIRST_DOCUMENT_START_STATE: return emitter.emitDocumentStart(event, true) case EMIT_DOCUMENT_START_STATE: return emitter.emitDocumentStart(event, false) case EMIT_DOCUMENT_CONTENT_STATE: return emitter.emitDocumentContent(event) case EMIT_DOCUMENT_END_STATE: return emitter.emitDocumentEnd(event) case EMIT_FLOW_SEQUENCE_FIRST_ITEM_STATE: return emitter.emitFlowSequenceItem(event, true, false) case EMIT_FLOW_SEQUENCE_TRAIL_ITEM_STATE: return emitter.emitFlowSequenceItem(event, false, true) case EMIT_FLOW_SEQUENCE_ITEM_STATE: return emitter.emitFlowSequenceItem(event, false, false) case EMIT_FLOW_MAPPING_FIRST_KEY_STATE: return emitter.emitFlowMappingKey(event, true, false) case EMIT_FLOW_MAPPING_TRAIL_KEY_STATE: return emitter.emitFlowMappingKey(event, false, true) case EMIT_FLOW_MAPPING_KEY_STATE: return emitter.emitFlowMappingKey(event, false, false) case EMIT_FLOW_MAPPING_SIMPLE_VALUE_STATE: return emitter.emitFlowMappingValue(event, true) case EMIT_FLOW_MAPPING_VALUE_STATE: return emitter.emitFlowMappingValue(event, false) case EMIT_BLOCK_SEQUENCE_FIRST_ITEM_STATE: return emitter.emitBlockSequenceItem(event, true) case EMIT_BLOCK_SEQUENCE_ITEM_STATE: return emitter.emitBlockSequenceItem(event, false) case EMIT_BLOCK_MAPPING_FIRST_KEY_STATE: return emitter.emitBlockMappingKey(event, true) case EMIT_BLOCK_MAPPING_KEY_STATE: return emitter.emitBlockMappingKey(event, false) case EMIT_BLOCK_MAPPING_SIMPLE_VALUE_STATE: return emitter.emitBlockMappingValue(event, true) case EMIT_BLOCK_MAPPING_VALUE_STATE: return emitter.emitBlockMappingValue(event, false) case EMIT_END_STATE: return EmitterError{ Message: "expected nothing after STREAM-END", } } panic("invalid emitter state") } // Check if we need to accumulate more events before emitting. // // We accumulate extra // - 1 event for DOCUMENT-START // - 2 events for SEQUENCE-START // - 3 events for MAPPING-START func (emitter *Emitter) needMoreEvents() bool { if emitter.events_head == len(emitter.events) { return true } var accumulate int switch emitter.events[emitter.events_head].Type { case DOCUMENT_START_EVENT: accumulate = 1 case SEQUENCE_START_EVENT: accumulate = 2 case MAPPING_START_EVENT: accumulate = 3 default: return false } if len(emitter.events)-emitter.events_head > accumulate { return false } var level int for i := emitter.events_head; i < len(emitter.events); i++ { switch emitter.events[i].Type { case STREAM_START_EVENT, DOCUMENT_START_EVENT, SEQUENCE_START_EVENT, MAPPING_START_EVENT: level++ case STREAM_END_EVENT, DOCUMENT_END_EVENT, SEQUENCE_END_EVENT, MAPPING_END_EVENT: level-- } if level == 0 { return false } } return true } // Expect STREAM-START. func (emitter *Emitter) emitStreamStart(event *Event) error { if event.Type != STREAM_START_EVENT { return EmitterError{ Message: "expected STREAM-START", } } if emitter.encoding == ANY_ENCODING { emitter.encoding = event.encoding if emitter.encoding == ANY_ENCODING { emitter.encoding = UTF8_ENCODING } } if emitter.BestIndent < 2 || emitter.BestIndent > 9 { emitter.BestIndent = 2 } if emitter.best_width >= 0 && emitter.best_width <= emitter.BestIndent*2 { emitter.best_width = 80 } if emitter.best_width < 0 { emitter.best_width = 1<<31 - 1 } if emitter.line_break == ANY_BREAK { emitter.line_break = LN_BREAK } emitter.indent = -1 emitter.line = 0 emitter.column = 0 emitter.whitespace = true emitter.indention = true emitter.space_above = true emitter.foot_indent = -1 if emitter.encoding != UTF8_ENCODING { if err := emitter.writeBom(); err != nil { return err } } emitter.state = EMIT_FIRST_DOCUMENT_START_STATE return nil } // Expect DOCUMENT-START or STREAM-END. func (emitter *Emitter) emitDocumentStart(event *Event, first bool) error { if event.Type == DOCUMENT_START_EVENT { if event.versionDirective != nil { if err := emitter.analyzeVersionDirective(event.versionDirective); err != nil { return err } } for i := 0; i < len(event.tagDirectives); i++ { tag_directive := &event.tagDirectives[i] if err := emitter.analyzeTagDirective(tag_directive); err != nil { return err } if err := emitter.appendTagDirective(tag_directive, false); err != nil { return err } } for i := 0; i < len(default_tag_directives); i++ { tag_directive := &default_tag_directives[i] if err := emitter.appendTagDirective(tag_directive, true); err != nil { return err } } implicit := event.Implicit if !first || emitter.canonical { implicit = false } if emitter.OpenEnded && (event.versionDirective != nil || len(event.tagDirectives) > 0) { if err := emitter.writeIndicator([]byte("..."), true, false, false); err != nil { return err } if err := emitter.writeIndent(); err != nil { return err } } if event.versionDirective != nil { implicit = false if err := emitter.writeIndicator([]byte("%YAML"), true, false, false); err != nil { return err } if err := emitter.writeIndicator([]byte("1.1"), true, false, false); err != nil { return err } if err := emitter.writeIndent(); err != nil { return err } } if len(event.tagDirectives) > 0 { implicit = false for i := 0; i < len(event.tagDirectives); i++ { tag_directive := &event.tagDirectives[i] if err := emitter.writeIndicator([]byte("%TAG"), true, false, false); err != nil { return err } if err := emitter.writeTagHandle(tag_directive.handle); err != nil { return err } if err := emitter.writeTagContent(tag_directive.prefix, true); err != nil { return err } if err := emitter.writeIndent(); err != nil { return err } } } if emitter.checkEmptyDocument() { implicit = false } if !implicit { if err := emitter.writeIndent(); err != nil { return err } if err := emitter.writeIndicator([]byte("---"), true, false, false); err != nil { return err } if emitter.canonical || true { if err := emitter.writeIndent(); err != nil { return err } } } if len(emitter.HeadComment) > 0 { if err := emitter.processHeadComment(); err != nil { return err } if err := emitter.putLineBreak(); err != nil { return err } } emitter.state = EMIT_DOCUMENT_CONTENT_STATE return nil } if event.Type == STREAM_END_EVENT { if emitter.OpenEnded { if err := emitter.writeIndicator([]byte("..."), true, false, false); err != nil { return err } if err := emitter.writeIndent(); err != nil { return err } } if err := emitter.flush(); err != nil { return err } emitter.state = EMIT_END_STATE return nil } return EmitterError{ Message: "expected DOCUMENT-START or STREAM-END", } } // Expect the root node. func (emitter *Emitter) emitDocumentContent(event *Event) error { emitter.states = append(emitter.states, EMIT_DOCUMENT_END_STATE) if err := emitter.processHeadComment(); err != nil { return err } if err := emitter.emitNode(event, true, false, false, false); err != nil { return err } if err := emitter.processLineComment(); err != nil { return err } if err := emitter.processFootComment(); err != nil { return err } return nil } // Expect DOCUMENT-END. func (emitter *Emitter) emitDocumentEnd(event *Event) error { if event.Type != DOCUMENT_END_EVENT { return EmitterError{ Message: "expected DOCUMENT-END", } } // [Go] Force document foot separation. emitter.foot_indent = 0 if err := emitter.processFootComment(); err != nil { return err } emitter.foot_indent = -1 if err := emitter.writeIndent(); err != nil { return err } if !event.Implicit { // [Go] Allocate the slice elsewhere. if err := emitter.writeIndicator([]byte("..."), true, false, false); err != nil { return err } if err := emitter.writeIndent(); err != nil { return err } } if err := emitter.flush(); err != nil { return err } emitter.state = EMIT_DOCUMENT_START_STATE emitter.tag_directives = emitter.tag_directives[:0] return nil } // Expect a flow item node. func (emitter *Emitter) emitFlowSequenceItem(event *Event, first, trail bool) error { if first { if err := emitter.writeIndicator([]byte{'['}, true, true, false); err != nil { return err } if err := emitter.increaseIndent(true, false); err != nil { return err } emitter.flow_level++ } if event.Type == SEQUENCE_END_EVENT { if emitter.canonical && !first && !trail { if err := emitter.writeIndicator([]byte{','}, false, false, false); err != nil { return err } } emitter.flow_level-- emitter.indent = emitter.indents[len(emitter.indents)-1] emitter.indents = emitter.indents[:len(emitter.indents)-1] if emitter.column == 0 || emitter.canonical && !first { if err := emitter.writeIndent(); err != nil { return err } } if err := emitter.writeIndicator([]byte{']'}, false, false, false); err != nil { return err } if err := emitter.processLineComment(); err != nil { return err } if err := emitter.processFootComment(); err != nil { return err } emitter.state = emitter.states[len(emitter.states)-1] emitter.states = emitter.states[:len(emitter.states)-1] return nil } if !first && !trail { if err := emitter.writeIndicator([]byte{','}, false, false, false); err != nil { return err } } if err := emitter.processHeadComment(); err != nil { return err } if emitter.column == 0 { if err := emitter.writeIndent(); err != nil { return err } } if emitter.canonical || emitter.column > emitter.best_width { if err := emitter.writeIndent(); err != nil { return err } } if len(emitter.LineComment)+len(emitter.FootComment)+len(emitter.TailComment) > 0 { emitter.states = append(emitter.states, EMIT_FLOW_SEQUENCE_TRAIL_ITEM_STATE) } else { emitter.states = append(emitter.states, EMIT_FLOW_SEQUENCE_ITEM_STATE) } if err := emitter.emitNode(event, false, true, false, false); err != nil { return err } if len(emitter.LineComment)+len(emitter.FootComment)+len(emitter.TailComment) > 0 { if err := emitter.writeIndicator([]byte{','}, false, false, false); err != nil { return err } } if err := emitter.processLineComment(); err != nil { return err } if err := emitter.processFootComment(); err != nil { return err } return nil } // Expect a flow key node. func (emitter *Emitter) emitFlowMappingKey(event *Event, first, trail bool) error { if first { if err := emitter.writeIndicator([]byte{'{'}, true, true, false); err != nil { return err } if err := emitter.increaseIndent(true, false); err != nil { return err } emitter.flow_level++ } if event.Type == MAPPING_END_EVENT { if (emitter.canonical || len(emitter.HeadComment)+len(emitter.FootComment)+len(emitter.TailComment) > 0) && !first && !trail { if err := emitter.writeIndicator([]byte{','}, false, false, false); err != nil { return err } } if err := emitter.processHeadComment(); err != nil { return err } emitter.flow_level-- emitter.indent = emitter.indents[len(emitter.indents)-1] emitter.indents = emitter.indents[:len(emitter.indents)-1] if emitter.canonical && !first { if err := emitter.writeIndent(); err != nil { return err } } if err := emitter.writeIndicator([]byte{'}'}, false, false, false); err != nil { return err } if err := emitter.processLineComment(); err != nil { return err } if err := emitter.processFootComment(); err != nil { return err } emitter.state = emitter.states[len(emitter.states)-1] emitter.states = emitter.states[:len(emitter.states)-1] return nil } if !first && !trail { if err := emitter.writeIndicator([]byte{','}, false, false, false); err != nil { return err } } if err := emitter.processHeadComment(); err != nil { return err } if emitter.column == 0 { if err := emitter.writeIndent(); err != nil { return err } } if emitter.canonical || emitter.column > emitter.best_width { if err := emitter.writeIndent(); err != nil { return err } } if !emitter.canonical && emitter.checkSimpleKey() { emitter.states = append(emitter.states, EMIT_FLOW_MAPPING_SIMPLE_VALUE_STATE) return emitter.emitNode(event, false, false, true, true) } if err := emitter.writeIndicator([]byte{'?'}, true, false, false); err != nil { return err } emitter.states = append(emitter.states, EMIT_FLOW_MAPPING_VALUE_STATE) return emitter.emitNode(event, false, false, true, false) } // Expect a flow value node. func (emitter *Emitter) emitFlowMappingValue(event *Event, simple bool) error { if simple { if err := emitter.writeIndicator([]byte{':'}, false, false, false); err != nil { return err } } else { if emitter.canonical || emitter.column > emitter.best_width { if err := emitter.writeIndent(); err != nil { return err } } if err := emitter.writeIndicator([]byte{':'}, true, false, false); err != nil { return err } } if len(emitter.LineComment)+len(emitter.FootComment)+len(emitter.TailComment) > 0 { emitter.states = append(emitter.states, EMIT_FLOW_MAPPING_TRAIL_KEY_STATE) } else { emitter.states = append(emitter.states, EMIT_FLOW_MAPPING_KEY_STATE) } if err := emitter.emitNode(event, false, false, true, false); err != nil { return err } if len(emitter.LineComment)+len(emitter.FootComment)+len(emitter.TailComment) > 0 { if err := emitter.writeIndicator([]byte{','}, false, false, false); err != nil { return err } } if err := emitter.processLineComment(); err != nil { return err } if err := emitter.processFootComment(); err != nil { return err } return nil } // Expect a block item node. func (emitter *Emitter) emitBlockSequenceItem(event *Event, first bool) error { if first { // emitter.mapping context tells us if we are currently in a // mapping context. emitter.column tells us which column we // are in the yaml output. 0 is the first char of the column. // emitter.indentation tells us if the last character was an // indentation character. // emitter.compact_sequence_indent tells us if '- ' is // considered part of the indentation for sequence elements. // So, `seq` means that we are in a mapping context, and we are // either at the first char of the column or the last character // was not an indentation character, and we consider '- ' part // of the indentation for sequence elements. seq := emitter.mapping_context && (emitter.column == 0 || !emitter.indention) && emitter.CompactSequenceIndent if err := emitter.increaseIndentCompact(false, false, seq); err != nil { return err } } if event.Type == SEQUENCE_END_EVENT { emitter.indent = emitter.indents[len(emitter.indents)-1] emitter.indents = emitter.indents[:len(emitter.indents)-1] emitter.state = emitter.states[len(emitter.states)-1] emitter.states = emitter.states[:len(emitter.states)-1] return nil } if err := emitter.processHeadComment(); err != nil { return err } if err := emitter.writeIndent(); err != nil { return err } if err := emitter.writeIndicator([]byte{'-'}, true, false, true); err != nil { return err } emitter.states = append(emitter.states, EMIT_BLOCK_SEQUENCE_ITEM_STATE) if err := emitter.emitNode(event, false, true, false, false); err != nil { return err } if err := emitter.processLineComment(); err != nil { return err } if err := emitter.processFootComment(); err != nil { return err } return nil } // Expect a block key node. func (emitter *Emitter) emitBlockMappingKey(event *Event, first bool) error { if first { if err := emitter.increaseIndent(false, false); err != nil { return err } } if err := emitter.processHeadComment(); err != nil { return err } if event.Type == MAPPING_END_EVENT { emitter.indent = emitter.indents[len(emitter.indents)-1] emitter.indents = emitter.indents[:len(emitter.indents)-1] emitter.state = emitter.states[len(emitter.states)-1] emitter.states = emitter.states[:len(emitter.states)-1] return nil } if err := emitter.writeIndent(); err != nil { return err } if len(emitter.LineComment) > 0 { // [Go] A line comment was provided for the key. That's unusual as the // scanner associates line comments with the value. Either way, // save the line comment and render it appropriately later. emitter.key_line_comment = emitter.LineComment emitter.LineComment = nil } if emitter.checkSimpleKey() { emitter.states = append(emitter.states, EMIT_BLOCK_MAPPING_SIMPLE_VALUE_STATE) if err := emitter.emitNode(event, false, false, true, true); err != nil { return err } if event.Type == ALIAS_EVENT { // make sure there's a space after the alias return emitter.put(' ') } return nil } if err := emitter.writeIndicator([]byte{'?'}, true, false, true); err != nil { return err } emitter.states = append(emitter.states, EMIT_BLOCK_MAPPING_VALUE_STATE) return emitter.emitNode(event, false, false, true, false) } // Expect a block value node. func (emitter *Emitter) emitBlockMappingValue(event *Event, simple bool) error { if simple { if err := emitter.writeIndicator([]byte{':'}, false, false, false); err != nil { return err } } else { if err := emitter.writeIndent(); err != nil { return err } if err := emitter.writeIndicator([]byte{':'}, true, false, true); err != nil { return err } } if len(emitter.key_line_comment) > 0 { // [Go] Line comments are generally associated with the value, // but when there's no value on the same line as a mapping key // they end up attached to the key itself. if event.Type == SCALAR_EVENT { if len(emitter.LineComment) == 0 { // A scalar is coming and it has no line // comments by itself yet, so just let it // handle the line comment as usual. If it has // a line comment, we can't have both so the // one from the key is lost. emitter.LineComment = emitter.key_line_comment emitter.key_line_comment = nil } } else if event.SequenceStyle() != FLOW_SEQUENCE_STYLE && (event.Type == MAPPING_START_EVENT || event.Type == SEQUENCE_START_EVENT) { // An indented block follows, so write the comment // right now. emitter.LineComment, emitter.key_line_comment = emitter.key_line_comment, emitter.LineComment if err := emitter.processLineComment(); err != nil { return err } emitter.LineComment, emitter.key_line_comment = emitter.key_line_comment, emitter.LineComment } } emitter.states = append(emitter.states, EMIT_BLOCK_MAPPING_KEY_STATE) if err := emitter.emitNode(event, false, false, true, false); err != nil { return err } if err := emitter.processLineComment(); err != nil { return err } if err := emitter.processFootComment(); err != nil { return err } return nil } // Expect a node. func (emitter *Emitter) emitNode(event *Event, root bool, sequence bool, mapping bool, simple_key bool, ) error { emitter.root_context = root emitter.sequence_context = sequence emitter.mapping_context = mapping emitter.simple_key_context = simple_key switch event.Type { case ALIAS_EVENT: return emitter.emitAlias(event) case SCALAR_EVENT: return emitter.emitScalar(event) case SEQUENCE_START_EVENT: return emitter.emitSequenceStart(event) case MAPPING_START_EVENT: return emitter.emitMappingStart(event) default: return EmitterError{ Message: fmt.Sprintf("expected SCALAR, SEQUENCE-START, MAPPING-START, or ALIAS, but got %v", event.Type), } } } // Expect ALIAS. func (emitter *Emitter) emitAlias(event *Event) error { if err := emitter.processAnchor(); err != nil { return err } emitter.state = emitter.states[len(emitter.states)-1] emitter.states = emitter.states[:len(emitter.states)-1] return nil } // Expect SCALAR. func (emitter *Emitter) emitScalar(event *Event) error { if err := emitter.selectScalarStyle(event); err != nil { return err } if err := emitter.processAnchor(); err != nil { return err } if err := emitter.processTag(); err != nil { return err } if err := emitter.increaseIndent(true, false); err != nil { return err } if err := emitter.processScalar(); err != nil { return err } emitter.indent = emitter.indents[len(emitter.indents)-1] emitter.indents = emitter.indents[:len(emitter.indents)-1] emitter.state = emitter.states[len(emitter.states)-1] emitter.states = emitter.states[:len(emitter.states)-1] return nil } // Expect SEQUENCE-START. func (emitter *Emitter) emitSequenceStart(event *Event) error { if err := emitter.processAnchor(); err != nil { return err } if err := emitter.processTag(); err != nil { return err } if emitter.flow_level > 0 || emitter.canonical || event.SequenceStyle() == FLOW_SEQUENCE_STYLE || emitter.checkEmptySequence() { emitter.state = EMIT_FLOW_SEQUENCE_FIRST_ITEM_STATE } else { emitter.state = EMIT_BLOCK_SEQUENCE_FIRST_ITEM_STATE } return nil } // Expect MAPPING-START. func (emitter *Emitter) emitMappingStart(event *Event) error { if err := emitter.processAnchor(); err != nil { return err } if err := emitter.processTag(); err != nil { return err } if emitter.flow_level > 0 || emitter.canonical || event.MappingStyle() == FLOW_MAPPING_STYLE || emitter.checkEmptyMapping() { emitter.state = EMIT_FLOW_MAPPING_FIRST_KEY_STATE } else { emitter.state = EMIT_BLOCK_MAPPING_FIRST_KEY_STATE } return nil } // Check if the document content is an empty scalar. func (emitter *Emitter) checkEmptyDocument() bool { return false // [Go] Huh? } // Check if the next events represent an empty sequence. func (emitter *Emitter) checkEmptySequence() bool { if len(emitter.events)-emitter.events_head < 2 { return false } return emitter.events[emitter.events_head].Type == SEQUENCE_START_EVENT && emitter.events[emitter.events_head+1].Type == SEQUENCE_END_EVENT } // Check if the next events represent an empty mapping. func (emitter *Emitter) checkEmptyMapping() bool { if len(emitter.events)-emitter.events_head < 2 { return false } return emitter.events[emitter.events_head].Type == MAPPING_START_EVENT && emitter.events[emitter.events_head+1].Type == MAPPING_END_EVENT } // Check if the next node can be expressed as a simple key. func (emitter *Emitter) checkSimpleKey() bool { length := 0 switch emitter.events[emitter.events_head].Type { case ALIAS_EVENT: length += len(emitter.anchor_data.anchor) case SCALAR_EVENT: if emitter.scalar_data.multiline { return false } length += len(emitter.anchor_data.anchor) + len(emitter.tag_data.handle) + len(emitter.tag_data.suffix) + len(emitter.scalar_data.value) case SEQUENCE_START_EVENT: if !emitter.checkEmptySequence() { return false } length += len(emitter.anchor_data.anchor) + len(emitter.tag_data.handle) + len(emitter.tag_data.suffix) case MAPPING_START_EVENT: if !emitter.checkEmptyMapping() { return false } length += len(emitter.anchor_data.anchor) + len(emitter.tag_data.handle) + len(emitter.tag_data.suffix) default: return false } return length <= 128 } // Write an anchor. func (emitter *Emitter) processAnchor() error { if emitter.anchor_data.anchor == nil { return nil } c := []byte{'&'} if emitter.anchor_data.alias { c[0] = '*' } if err := emitter.writeIndicator(c, true, false, false); err != nil { return err } return emitter.writeAnchor(emitter.anchor_data.anchor) } // Write a tag. func (emitter *Emitter) processTag() error { if len(emitter.tag_data.handle) == 0 && len(emitter.tag_data.suffix) == 0 { return nil } if len(emitter.tag_data.handle) > 0 { if err := emitter.writeTagHandle(emitter.tag_data.handle); err != nil { return err } if len(emitter.tag_data.suffix) > 0 { if err := emitter.writeTagContent(emitter.tag_data.suffix, false); err != nil { return err } } } else { // [Go] Allocate these slices elsewhere. if err := emitter.writeIndicator([]byte("!<"), true, false, false); err != nil { return err } if err := emitter.writeTagContent(emitter.tag_data.suffix, false); err != nil { return err } if err := emitter.writeIndicator([]byte{'>'}, false, false, false); err != nil { return err } } return nil } // Write a scalar. func (emitter *Emitter) processScalar() error { switch emitter.scalar_data.style { case PLAIN_SCALAR_STYLE: return emitter.writePlainScalar(emitter.scalar_data.value, !emitter.simple_key_context) case SINGLE_QUOTED_SCALAR_STYLE: return emitter.writeSingleQuotedScalar(emitter.scalar_data.value, !emitter.simple_key_context) case DOUBLE_QUOTED_SCALAR_STYLE: return emitter.writeDoubleQuotedScalar(emitter.scalar_data.value, !emitter.simple_key_context) case LITERAL_SCALAR_STYLE: return emitter.writeLiteralScalar(emitter.scalar_data.value) case FOLDED_SCALAR_STYLE: return emitter.writeFoldedScalar(emitter.scalar_data.value) } panic("unknown scalar style") } // Write a head comment. func (emitter *Emitter) processHeadComment() error { if len(emitter.TailComment) > 0 { if err := emitter.writeIndent(); err != nil { return err } if err := emitter.writeComment(emitter.TailComment); err != nil { return err } emitter.TailComment = emitter.TailComment[:0] emitter.foot_indent = emitter.indent if emitter.foot_indent < 0 { emitter.foot_indent = 0 } } if len(emitter.HeadComment) == 0 { return nil } if err := emitter.writeIndent(); err != nil { return err } if err := emitter.writeComment(emitter.HeadComment); err != nil { return err } emitter.HeadComment = emitter.HeadComment[:0] return nil } // processLineComment preserves the original signature and delegates to // processLineCommentLinebreak passing false for linebreak func (emitter *Emitter) processLineComment() error { return emitter.processLineCommentLinebreak(false) } // Write a line comment. func (emitter *Emitter) processLineCommentLinebreak(linebreak bool) error { if len(emitter.LineComment) == 0 { // The next 3 lines are needed to resolve an issue with leading newlines // See https://github.com/go-yaml/yaml/issues/755 // When linebreak is set to true, put_break will be called and will add // the needed newline. if linebreak { if err := emitter.putLineBreak(); err != nil { return err } } return nil } if !emitter.whitespace { if err := emitter.put(' '); err != nil { return err } } if err := emitter.writeComment(emitter.LineComment); err != nil { return err } emitter.LineComment = emitter.LineComment[:0] return nil } // Write a foot comment. func (emitter *Emitter) processFootComment() error { if len(emitter.FootComment) == 0 { return nil } if err := emitter.writeIndent(); err != nil { return err } if err := emitter.writeComment(emitter.FootComment); err != nil { return err } emitter.FootComment = emitter.FootComment[:0] emitter.foot_indent = emitter.indent if emitter.foot_indent < 0 { emitter.foot_indent = 0 } return nil } // Check if a %YAML directive is valid. func (emitter *Emitter) analyzeVersionDirective(version_directive *VersionDirective) error { if version_directive.major != 1 || version_directive.minor != 1 { return EmitterError{ Message: "incompatible %YAML directive", } } return nil } // Check if a %TAG directive is valid. func (emitter *Emitter) analyzeTagDirective(tag_directive *TagDirective) error { handle := tag_directive.handle prefix := tag_directive.prefix if len(handle) == 0 { return EmitterError{ Message: "tag handle must not be empty", } } if handle[0] != '!' { return EmitterError{ Message: "tag handle must start with '!'", } } if handle[len(handle)-1] != '!' { return EmitterError{ Message: "tag handle must end with '!'", } } for i := 1; i < len(handle)-1; i += width(handle[i]) { if !isAlpha(handle, i) { return EmitterError{ Message: "tag handle must contain alphanumerical characters only", } } } if len(prefix) == 0 { return EmitterError{ Message: "tag prefix must not be empty", } } return nil } // Check if an anchor is valid. func (emitter *Emitter) analyzeAnchor(anchor []byte, alias bool) error { if len(anchor) == 0 { problem := "anchor value must not be empty" if alias { problem = "alias value must not be empty" } return EmitterError{ Message: problem, } } for i := 0; i < len(anchor); i += width(anchor[i]) { if !isAnchorChar(anchor, i) { problem := "anchor value must contain valid characters only" if alias { problem = "alias value must contain valid characters only" } return EmitterError{ Message: problem, } } } emitter.anchor_data.anchor = anchor emitter.anchor_data.alias = alias return nil } // Check if a tag is valid. func (emitter *Emitter) analyzeTag(tag []byte) error { if len(tag) == 0 { return EmitterError{ Message: "tag value must not be empty", } } for i := 0; i < len(emitter.tag_directives); i++ { tag_directive := &emitter.tag_directives[i] if bytes.HasPrefix(tag, tag_directive.prefix) { emitter.tag_data.handle = tag_directive.handle emitter.tag_data.suffix = tag[len(tag_directive.prefix):] return nil } } emitter.tag_data.suffix = tag return nil } // Check if a scalar is valid. func (emitter *Emitter) analyzeScalar(value []byte) error { var block_indicators, flow_indicators, line_breaks, special_characters, tab_characters, leading_space, leading_break, trailing_space, trailing_break, break_space, space_break, preceded_by_whitespace, followed_by_whitespace, previous_space, previous_break bool emitter.scalar_data.value = value if len(value) == 0 { emitter.scalar_data.multiline = false emitter.scalar_data.flow_plain_allowed = false emitter.scalar_data.block_plain_allowed = true emitter.scalar_data.single_quoted_allowed = true emitter.scalar_data.block_allowed = false return nil } if len(value) >= 3 && ((value[0] == '-' && value[1] == '-' && value[2] == '-') || (value[0] == '.' && value[1] == '.' && value[2] == '.')) { block_indicators = true flow_indicators = true } preceded_by_whitespace = true for i, w := 0, 0; i < len(value); i += w { w = width(value[i]) followed_by_whitespace = i+w >= len(value) || isBlank(value, i+w) if i == 0 { switch value[i] { case '#', ',', '[', ']', '{', '}', '&', '*', '!', '|', '>', '\'', '"', '%', '@', '`': flow_indicators = true block_indicators = true case '?', ':': flow_indicators = true if followed_by_whitespace { block_indicators = true } case '-': if followed_by_whitespace { flow_indicators = true block_indicators = true } } } else { switch value[i] { case ',', '?', '[', ']', '{', '}': flow_indicators = true case ':': flow_indicators = true if followed_by_whitespace { block_indicators = true } case '#': if preceded_by_whitespace { flow_indicators = true block_indicators = true } } } if value[i] == '\t' { tab_characters = true } else if !isPrintable(value, i) || !isASCII(value, i) && !emitter.unicode { special_characters = true } if isSpace(value, i) { if i == 0 { leading_space = true } if i+width(value[i]) == len(value) { trailing_space = true } if previous_break { break_space = true } previous_space = true previous_break = false } else if isLineBreak(value, i) { line_breaks = true if i == 0 { leading_break = true } if i+width(value[i]) == len(value) { trailing_break = true } if previous_space { space_break = true } previous_space = false previous_break = true } else { previous_space = false previous_break = false } // [Go]: Why 'z'? Couldn't be the end of the string as that's // the loop condition. preceded_by_whitespace = isBlankOrZero(value, i) } emitter.scalar_data.multiline = line_breaks emitter.scalar_data.flow_plain_allowed = true emitter.scalar_data.block_plain_allowed = true emitter.scalar_data.single_quoted_allowed = true emitter.scalar_data.block_allowed = true if leading_space || leading_break || trailing_space || trailing_break { emitter.scalar_data.flow_plain_allowed = false emitter.scalar_data.block_plain_allowed = false } if trailing_space { emitter.scalar_data.block_allowed = false } if break_space { emitter.scalar_data.flow_plain_allowed = false emitter.scalar_data.block_plain_allowed = false emitter.scalar_data.single_quoted_allowed = false } if space_break || tab_characters || special_characters { emitter.scalar_data.flow_plain_allowed = false emitter.scalar_data.block_plain_allowed = false emitter.scalar_data.single_quoted_allowed = false } if space_break || special_characters { emitter.scalar_data.block_allowed = false } if line_breaks { emitter.scalar_data.flow_plain_allowed = false emitter.scalar_data.block_plain_allowed = false } if flow_indicators { emitter.scalar_data.flow_plain_allowed = false } if block_indicators { emitter.scalar_data.block_plain_allowed = false } return nil } // Check if the event data is valid. func (emitter *Emitter) analyzeEvent(event *Event) error { emitter.anchor_data.anchor = nil emitter.tag_data.handle = nil emitter.tag_data.suffix = nil emitter.scalar_data.value = nil if len(event.HeadComment) > 0 { emitter.HeadComment = event.HeadComment } if len(event.LineComment) > 0 { emitter.LineComment = event.LineComment } if len(event.FootComment) > 0 { emitter.FootComment = event.FootComment } if len(event.TailComment) > 0 { emitter.TailComment = event.TailComment } switch event.Type { case ALIAS_EVENT: if err := emitter.analyzeAnchor(event.Anchor, true); err != nil { return err } case SCALAR_EVENT: if len(event.Anchor) > 0 { if err := emitter.analyzeAnchor(event.Anchor, false); err != nil { return err } } if len(event.Tag) > 0 && (emitter.canonical || (!event.Implicit && !event.quoted_implicit)) { if err := emitter.analyzeTag(event.Tag); err != nil { return err } } if err := emitter.analyzeScalar(event.Value); err != nil { return err } case SEQUENCE_START_EVENT: if len(event.Anchor) > 0 { if err := emitter.analyzeAnchor(event.Anchor, false); err != nil { return err } } if len(event.Tag) > 0 && (emitter.canonical || !event.Implicit) { if err := emitter.analyzeTag(event.Tag); err != nil { return err } } case MAPPING_START_EVENT: if len(event.Anchor) > 0 { if err := emitter.analyzeAnchor(event.Anchor, false); err != nil { return err } } if len(event.Tag) > 0 && (emitter.canonical || !event.Implicit) { if err := emitter.analyzeTag(event.Tag); err != nil { return err } } } return nil } // Determine an acceptable scalar style. func (emitter *Emitter) selectScalarStyle(event *Event) error { no_tag := len(emitter.tag_data.handle) == 0 && len(emitter.tag_data.suffix) == 0 if no_tag && !event.Implicit && !event.quoted_implicit { return EmitterError{ Message: "neither tag nor implicit flags are specified", } } style := event.ScalarStyle() if style == ANY_SCALAR_STYLE { style = PLAIN_SCALAR_STYLE } if emitter.canonical { style = DOUBLE_QUOTED_SCALAR_STYLE } if emitter.simple_key_context && emitter.scalar_data.multiline { style = DOUBLE_QUOTED_SCALAR_STYLE } if style == PLAIN_SCALAR_STYLE { if emitter.flow_level > 0 && !emitter.scalar_data.flow_plain_allowed || emitter.flow_level == 0 && !emitter.scalar_data.block_plain_allowed { style = emitter.requiredQuoteStyle() } if len(emitter.scalar_data.value) == 0 && (emitter.flow_level > 0 || emitter.simple_key_context) { style = emitter.requiredQuoteStyle() } if no_tag && !event.Implicit { style = emitter.requiredQuoteStyle() } } if style == SINGLE_QUOTED_SCALAR_STYLE { if !emitter.scalar_data.single_quoted_allowed { style = DOUBLE_QUOTED_SCALAR_STYLE } } if style == LITERAL_SCALAR_STYLE || style == FOLDED_SCALAR_STYLE { if !emitter.scalar_data.block_allowed || emitter.flow_level > 0 || emitter.simple_key_context { style = DOUBLE_QUOTED_SCALAR_STYLE } } if no_tag && !event.quoted_implicit && style != PLAIN_SCALAR_STYLE { emitter.tag_data.handle = []byte{'!'} } emitter.scalar_data.style = style return nil } // Write the BOM character. func (emitter *Emitter) writeBom() error { if err := emitter.flushIfNeeded(); err != nil { return err } pos := emitter.buffer_pos emitter.buffer[pos+0] = '\xEF' emitter.buffer[pos+1] = '\xBB' emitter.buffer[pos+2] = '\xBF' emitter.buffer_pos += 3 return nil } // writeIndent writes the appropriate indentation to the output. func (emitter *Emitter) writeIndent() error { indent := emitter.indent if indent < 0 { indent = 0 } if !emitter.indention || emitter.column > indent || (emitter.column == indent && !emitter.whitespace) { if err := emitter.putLineBreak(); err != nil { return err } } if emitter.foot_indent == indent { if err := emitter.putLineBreak(); err != nil { return err } } for emitter.column < indent { if err := emitter.put(' '); err != nil { return err } } emitter.whitespace = true emitter.space_above = false emitter.foot_indent = -1 return nil } // writeIndicator writes a YAML indicator (like ':', '-', '?') to the output. func (emitter *Emitter) writeIndicator(indicator []byte, need_whitespace, is_whitespace, is_indention bool) error { if need_whitespace && !emitter.whitespace { if err := emitter.put(' '); err != nil { return err } } if err := emitter.writeAll(indicator); err != nil { return err } emitter.whitespace = is_whitespace emitter.indention = (emitter.indention && is_indention) emitter.OpenEnded = false return nil } // writeAnchor writes an anchor name to the output. func (emitter *Emitter) writeAnchor(value []byte) error { if err := emitter.writeAll(value); err != nil { return err } emitter.whitespace = false emitter.indention = false return nil } // writeTagHandle writes a tag handle to the output. func (emitter *Emitter) writeTagHandle(value []byte) error { if !emitter.whitespace { if err := emitter.put(' '); err != nil { return err } } if err := emitter.writeAll(value); err != nil { return err } emitter.whitespace = false emitter.indention = false return nil } // writeTagContent writes a tag URI to the output, URL-encoding special // characters as needed. func (emitter *Emitter) writeTagContent(value []byte, need_whitespace bool) error { if need_whitespace && !emitter.whitespace { if err := emitter.put(' '); err != nil { return err } } for i := 0; i < len(value); { var must_write bool switch value[i] { case ';', '/', '?', ':', '@', '&', '=', '+', '$', ',', '_', '.', '~', '*', '\'', '(', ')', '[', ']': must_write = true default: must_write = isAlpha(value, i) } if must_write { if err := emitter.write(value, &i); err != nil { return err } } else { w := width(value[i]) for k := 0; k < w; k++ { octet := value[i] i++ if err := emitter.put('%'); err != nil { return err } c := octet >> 4 if c < 10 { c += '0' } else { c += 'A' - 10 } if err := emitter.put(c); err != nil { return err } c = octet & 0x0f if c < 10 { c += '0' } else { c += 'A' - 10 } if err := emitter.put(c); err != nil { return err } } } } emitter.whitespace = false emitter.indention = false return nil } // writePlainScalar writes a plain (unquoted) scalar to the output, handling // line breaks and wrapping as needed. func (emitter *Emitter) writePlainScalar(value []byte, allow_breaks bool) error { if len(value) > 0 && !emitter.whitespace { if err := emitter.put(' '); err != nil { return err } } spaces := false breaks := false for i := 0; i < len(value); { if isSpace(value, i) { if allow_breaks && !spaces && emitter.column > emitter.best_width && !isSpace(value, i+1) { if err := emitter.writeIndent(); err != nil { return err } i += width(value[i]) } else { if err := emitter.write(value, &i); err != nil { return err } } spaces = true } else if isLineBreak(value, i) { if !breaks && value[i] == '\n' { if err := emitter.putLineBreak(); err != nil { return err } } if err := emitter.writeLineBreak(value, &i); err != nil { return err } breaks = true } else { if breaks { if err := emitter.writeIndent(); err != nil { return err } } if err := emitter.write(value, &i); err != nil { return err } emitter.indention = false spaces = false breaks = false } } if len(value) > 0 { emitter.whitespace = false } emitter.indention = false if emitter.root_context { emitter.OpenEnded = true } return nil } // writeSingleQuotedScalar writes a single-quoted scalar to the output, // escaping single quotes and handling line breaks. func (emitter *Emitter) writeSingleQuotedScalar(value []byte, allow_breaks bool) error { if err := emitter.writeIndicator([]byte{'\''}, true, false, false); err != nil { return err } spaces := false breaks := false for i := 0; i < len(value); { if isSpace(value, i) { if allow_breaks && !spaces && emitter.column > emitter.best_width && i > 0 && i < len(value)-1 && !isSpace(value, i+1) { if err := emitter.writeIndent(); err != nil { return err } i += width(value[i]) } else { if err := emitter.write(value, &i); err != nil { return err } } spaces = true } else if isLineBreak(value, i) { if !breaks && value[i] == '\n' { if err := emitter.putLineBreak(); err != nil { return err } } if err := emitter.writeLineBreak(value, &i); err != nil { return err } breaks = true } else { if breaks { if err := emitter.writeIndent(); err != nil { return err } } if value[i] == '\'' { if err := emitter.put('\''); err != nil { return err } } if err := emitter.write(value, &i); err != nil { return err } emitter.indention = false spaces = false breaks = false } } if err := emitter.writeIndicator([]byte{'\''}, false, false, false); err != nil { return err } emitter.whitespace = false emitter.indention = false return nil } // writeDoubleQuotedScalar writes a double-quoted scalar to the output, // escaping special characters and handling Unicode. func (emitter *Emitter) writeDoubleQuotedScalar(value []byte, allow_breaks bool) error { spaces := false if err := emitter.writeIndicator([]byte{'"'}, true, false, false); err != nil { return err } for i := 0; i < len(value); { if !isPrintable(value, i) || (!emitter.unicode && !isASCII(value, i)) || isBOM(value, i) || isLineBreak(value, i) || value[i] == '"' || value[i] == '\\' { octet := value[i] var w int var v rune switch { case octet&0x80 == 0x00: w, v = 1, rune(octet&0x7F) case octet&0xE0 == 0xC0: w, v = 2, rune(octet&0x1F) case octet&0xF0 == 0xE0: w, v = 3, rune(octet&0x0F) case octet&0xF8 == 0xF0: w, v = 4, rune(octet&0x07) } for k := 1; k < w; k++ { octet = value[i+k] v = (v << 6) + (rune(octet) & 0x3F) } i += w if err := emitter.put('\\'); err != nil { return err } var err error switch v { case 0x00: err = emitter.put('0') case 0x07: err = emitter.put('a') case 0x08: err = emitter.put('b') case 0x09: err = emitter.put('t') case 0x0A: err = emitter.put('n') case 0x0b: err = emitter.put('v') case 0x0c: err = emitter.put('f') case 0x0d: err = emitter.put('r') case 0x1b: err = emitter.put('e') case 0x22: err = emitter.put('"') case 0x5c: err = emitter.put('\\') case 0x85: err = emitter.put('N') case 0xA0: err = emitter.put('_') case 0x2028: err = emitter.put('L') case 0x2029: err = emitter.put('P') default: if v <= 0xFF { err = emitter.put('x') w = 2 } else if v <= 0xFFFF { err = emitter.put('u') w = 4 } else { err = emitter.put('U') w = 8 } for k := (w - 1) * 4; err == nil && k >= 0; k -= 4 { digit := byte((v >> uint(k)) & 0x0F) if digit < 10 { err = emitter.put(digit + '0') } else { err = emitter.put(digit + 'A' - 10) } } } if err != nil { return err } spaces = false } else if isSpace(value, i) { if allow_breaks && !spaces && emitter.column > emitter.best_width && i > 0 && i < len(value)-1 { if err := emitter.writeIndent(); err != nil { return err } if isSpace(value, i+1) { if err := emitter.put('\\'); err != nil { return err } } i += width(value[i]) } else if err := emitter.write(value, &i); err != nil { return err } spaces = true } else { if err := emitter.write(value, &i); err != nil { return err } spaces = false } } if err := emitter.writeIndicator([]byte{'"'}, false, false, false); err != nil { return err } emitter.whitespace = false emitter.indention = false return nil } // writeBlockScalarHints writes the indentation and chomping indicators for // block scalars. func (emitter *Emitter) writeBlockScalarHints(value []byte) error { if isSpace(value, 0) { // https://github.com/yaml/go-yaml/issues/65 // isLineBreak(value, 0) removed as the linebreak will only // write the indentation value. indent_hint := []byte{'0' + byte(emitter.BestIndent)} if err := emitter.writeIndicator(indent_hint, false, false, false); err != nil { return err } } emitter.OpenEnded = false var chomp_hint [1]byte if len(value) == 0 { chomp_hint[0] = '-' } else { i := len(value) - 1 for value[i]&0xC0 == 0x80 { i-- } if !isLineBreak(value, i) { chomp_hint[0] = '-' } else if i == 0 { chomp_hint[0] = '+' emitter.OpenEnded = true } else { i-- for value[i]&0xC0 == 0x80 { i-- } if isLineBreak(value, i) { chomp_hint[0] = '+' emitter.OpenEnded = true } } } if chomp_hint[0] != 0 { if err := emitter.writeIndicator(chomp_hint[:], false, false, false); err != nil { return err } } return nil } // writeLiteralScalar writes a literal block scalar (|) to the output, // preserving line breaks exactly. func (emitter *Emitter) writeLiteralScalar(value []byte) error { if err := emitter.writeIndicator([]byte{'|'}, true, false, false); err != nil { return err } if err := emitter.writeBlockScalarHints(value); err != nil { return err } if err := emitter.processLineCommentLinebreak(true); err != nil { return err } emitter.whitespace = true breaks := true for i := 0; i < len(value); { if isLineBreak(value, i) { if err := emitter.writeLineBreak(value, &i); err != nil { return err } breaks = true } else { if breaks { if err := emitter.writeIndent(); err != nil { return err } } if err := emitter.write(value, &i); err != nil { return err } emitter.indention = false breaks = false } } return nil } // writeFoldedScalar writes a folded block scalar (>) to the output, folding // long lines at appropriate breaks. func (emitter *Emitter) writeFoldedScalar(value []byte) error { if err := emitter.writeIndicator([]byte{'>'}, true, false, false); err != nil { return err } if err := emitter.writeBlockScalarHints(value); err != nil { return err } if err := emitter.processLineCommentLinebreak(true); err != nil { return err } emitter.whitespace = true breaks := true leading_spaces := true for i := 0; i < len(value); { if isLineBreak(value, i) { if !breaks && !leading_spaces && value[i] == '\n' { k := 0 for isLineBreak(value, k) { k += width(value[k]) } if !isBlankOrZero(value, k) { if err := emitter.putLineBreak(); err != nil { return err } } } if err := emitter.writeLineBreak(value, &i); err != nil { return err } breaks = true } else { if breaks { if err := emitter.writeIndent(); err != nil { return err } leading_spaces = isBlank(value, i) } if !breaks && isSpace(value, i) && !isSpace(value, i+1) && emitter.column > emitter.best_width { if err := emitter.writeIndent(); err != nil { return err } i += width(value[i]) } else { if err := emitter.write(value, &i); err != nil { return err } } emitter.indention = false breaks = false } } return nil } // writeComment writes a comment to the output, ensuring each line starts // with '#' and handling line breaks appropriately. func (emitter *Emitter) writeComment(comment []byte) error { breaks := false pound := false for i := 0; i < len(comment); { if isLineBreak(comment, i) { if err := emitter.writeLineBreak(comment, &i); err != nil { return err } breaks = true pound = false } else { if breaks { if err := emitter.writeIndent(); err != nil { return err } } if !pound { if comment[i] != '#' { if err := emitter.put('#'); err != nil { return err } if err := emitter.put(' '); err != nil { return err } } pound = true } if err := emitter.write(comment, &i); err != nil { return err } emitter.indention = false breaks = false } } if !breaks { if err := emitter.putLineBreak(); err != nil { return err } } emitter.whitespace = true return nil } // Flush the buffer if needed. func (emitter *Emitter) flushIfNeeded() error { if emitter.buffer_pos+5 >= len(emitter.buffer) { return emitter.flush() } return nil } // Put a character to the output buffer. func (emitter *Emitter) put(value byte) error { if emitter.buffer_pos+5 >= len(emitter.buffer) { if err := emitter.flush(); err != nil { return err } } emitter.buffer[emitter.buffer_pos] = value emitter.buffer_pos++ emitter.column++ return nil } // Put a line break to the output buffer. func (emitter *Emitter) putLineBreak() error { if emitter.buffer_pos+5 >= len(emitter.buffer) { if err := emitter.flush(); err != nil { return err } } switch emitter.line_break { case CR_BREAK: emitter.buffer[emitter.buffer_pos] = '\r' emitter.buffer_pos += 1 case LN_BREAK: emitter.buffer[emitter.buffer_pos] = '\n' emitter.buffer_pos += 1 case CRLN_BREAK: emitter.buffer[emitter.buffer_pos+0] = '\r' emitter.buffer[emitter.buffer_pos+1] = '\n' emitter.buffer_pos += 2 default: panic("unknown line break setting") } if emitter.column == 0 { emitter.space_above = true } emitter.column = 0 emitter.line++ // [Go] Do this here and below and drop from everywhere else (see // commented lines). emitter.indention = true return nil } // Copy a character from a string into buffer. func (emitter *Emitter) write(s []byte, i *int) error { if emitter.buffer_pos+5 >= len(emitter.buffer) { if err := emitter.flush(); err != nil { return err } } p := emitter.buffer_pos w := width(s[*i]) switch w { case 4: emitter.buffer[p+3] = s[*i+3] fallthrough case 3: emitter.buffer[p+2] = s[*i+2] fallthrough case 2: emitter.buffer[p+1] = s[*i+1] fallthrough case 1: emitter.buffer[p+0] = s[*i+0] default: panic("unknown character width") } emitter.column++ emitter.buffer_pos += w *i += w return nil } // Write a whole string into buffer. func (emitter *Emitter) writeAll(s []byte) error { for i := 0; i < len(s); { if err := emitter.write(s, &i); err != nil { return err } } return nil } // Copy a line break character from a string into buffer. func (emitter *Emitter) writeLineBreak(s []byte, i *int) error { if s[*i] == '\n' { if err := emitter.putLineBreak(); err != nil { return err } *i++ } else { if err := emitter.write(s, i); err != nil { return err } if emitter.column == 0 { emitter.space_above = true } emitter.column = 0 emitter.line++ // [Go] Do this here and above and drop from everywhere else // (see commented lines). emitter.indention = true } return nil } // Append a directive to the directives stack. func (emitter *Emitter) appendTagDirective(value *TagDirective, allow_duplicates bool) error { for i := 0; i < len(emitter.tag_directives); i++ { if bytes.Equal(value.handle, emitter.tag_directives[i].handle) { if allow_duplicates { return nil } return EmitterError{ Message: "duplicate %TAG directive", } } } // [Go] Do we actually need to copy this given garbage collection // and the lack of deallocating destructors? tag_copy := TagDirective{ handle: make([]byte, len(value.handle)), prefix: make([]byte, len(value.prefix)), } copy(tag_copy.handle, value.handle) copy(tag_copy.prefix, value.prefix) emitter.tag_directives = append(emitter.tag_directives, tag_copy) return nil } // Increase the indentation level. func (emitter *Emitter) increaseIndentCompact(flow, indentless bool, compact_seq bool) error { emitter.indents = append(emitter.indents, emitter.indent) if emitter.indent < 0 { if flow { emitter.indent = emitter.BestIndent } else { emitter.indent = 0 } } else if !indentless { // [Go] This was changed so that indentations are more regular. if emitter.states[len(emitter.states)-1] == EMIT_BLOCK_SEQUENCE_ITEM_STATE { // The first indent inside a sequence will just skip // the "- " indicator. emitter.indent += 2 } else { // Everything else aligns to the chosen indentation. emitter.indent = emitter.BestIndent * ((emitter.indent + emitter.BestIndent) / emitter.BestIndent) if compact_seq { // The value compact_seq passed in is almost // always set to `false` when this function is // called, except when we are dealing with // sequence nodes. So this gets triggered to // subtract 2 only when we are increasing the // indent to account for sequence nodes, which // will be correct because we need to subtract // 2 to account for the - at the beginning of // the sequence node. emitter.indent = emitter.indent - 2 } } } return nil } // emitter preserves the original signature and delegates to // increaseIndentCompact without compact-sequence indentation func (emitter *Emitter) increaseIndent(flow, indentless bool) error { return emitter.increaseIndentCompact(flow, indentless, false) } // silentNilEvent checks if an event represents an implicit null scalar that // can be omitted in non-canonical mode. func (emitter *Emitter) silentNilEvent(event *Event) bool { return event.Type == SCALAR_EVENT && event.Implicit && !emitter.canonical && len(emitter.scalar_data.value) == 0 } // requiredQuoteStyle returns the appropriate quote style based on the // emitter's quotePreference setting. func (emitter *Emitter) requiredQuoteStyle() ScalarStyle { if emitter.quotePreference == QuoteDouble { return DOUBLE_QUOTED_SCALAR_STYLE } return SINGLE_QUOTED_SCALAR_STYLE } golang-go.yaml-yaml-v4-4.0.0~rc5/internal/libyaml/emitter_test.go000066400000000000000000000023611521353103300247270ustar00rootroot00000000000000// Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Tests for the emitter stage. // Verifies YAML output generation from events. package libyaml import ( "bytes" "strings" "testing" "go.yaml.in/yaml/v4/internal/testutil/assert" ) func TestEmitter(t *testing.T) { RunTestCases(t, "emitter.yaml", map[string]TestHandler{ "emit": RunEmitTest, "emit-config": RunEmitTest, "roundtrip": RunRoundTripTest, "emit-writer": runEmitWriterTest, "api-new": runAPINewTest, "api-method": runAPIMethodTest, "api-panic": runAPIPanicTest, "api-delete": runAPIDeleteTest, "api-new-event": runAPINewEventTest, }) } func runEmitWriterTest(t *testing.T, tc TestCase) { t.Helper() var events []Event for _, eventSpec := range tc.Events { events = append(events, CreateEventFromSpec(t, eventSpec)) } emitter := NewEmitter() var buf bytes.Buffer emitter.SetOutputWriter(&buf) for i := range events { err := emitter.Emit(&events[i]) assert.NoErrorf(t, err, "Emit() error: %v", err) } result := buf.String() for _, expected := range tc.WantContains { assert.Truef(t, strings.Contains(result, expected), "output should contain %q, got %q", expected, result) } } golang-go.yaml-yaml-v4-4.0.0~rc5/internal/libyaml/errors.go000066400000000000000000000173001521353103300235320ustar00rootroot00000000000000// Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Error types for YAML parsing and emitting. // Provides structured error reporting with line/column information. package libyaml import ( "errors" "fmt" "strings" ) // Stage identifies the processing stage where an error occurred during YAML // loading or dumping. type Stage string const ( // Load stages ReaderStage Stage = "reader" // Input reading and encoding ScannerStage Stage = "scanner" // Tokenization ParserStage Stage = "parser" // Event stream parsing ComposerStage Stage = "composer" // Node tree construction ResolverStage Stage = "resolver" // Tag resolution ConstructorStage Stage = "constructor" // Go value construction // Dump stages RepresenterStage Stage = "representer" // Go value to Node tree SerializerStage Stage = "serializer" // Node tree to events EmitterStage Stage = "emitter" // Events to YAML bytes WriterStage Stage = "writer" // Output writing ) // LoadError represents an error that occurred while loading a YAML document. // // It provides detailed location information and identifies the processing // stage where the error occurred. type LoadError struct { Stage Stage // Processing stage where error occurred Message string // Error description // Position information Mark Mark // Primary error position ContextMark Mark // Optional context position (e.g., start of construct) ContextMsg string // Optional context message // Error chaining err error // Underlying error (for Unwrap support) } // Error returns the error message with stage and position information. // Format: "go-yaml load error in at L:C: " // Or with context: "go-yaml load error in () at L:C-L:C: " func (e *LoadError) Error() string { if len(e.ContextMsg) > 0 { return fmt.Sprintf("go-yaml load error in %s (%s) at %s: %s", e.Stage, e.ContextMsg, e.ContextMark.rangeString(e.Mark), e.Message) } return fmt.Sprintf("go-yaml load error in %s at %s: %s", e.Stage, e.Mark.shortString(), e.Message) } // simpleError returns the error message without the "yaml: Load error (in stage)" prefix. // Used for formatting errors within LoadErrors collections. // Format: "line L: " (backwards compatible - no column info) func (e *LoadError) simpleError() string { var builder strings.Builder if len(e.ContextMsg) > 0 { fmt.Fprintf(&builder, "%s at %s: ", e.ContextMsg, e.ContextMark) } if len(e.ContextMsg) == 0 || e.ContextMark != e.Mark { if e.Mark.Line > 0 { fmt.Fprintf(&builder, "line %d: ", e.Mark.Line) } else { builder.WriteString(": ") } } builder.WriteString(e.Message) return builder.String() } // Unwrap returns the underlying error. func (e *LoadError) Unwrap() error { return e.err } // NewLoadError creates a LoadError with an underlying cause. // The cause is accessible via Unwrap for use with [errors.Is] and [errors.As]. func NewLoadError(stage Stage, message string, mark Mark, cause error) *LoadError { return &LoadError{ Stage: stage, Message: message, Mark: mark, err: cause, } } // DumpError represents an error that occurred while dumping a YAML document. // // It identifies the processing stage where the error occurred and provides // an optional underlying cause via Unwrap. type DumpError struct { Stage Stage // Processing stage where error occurred Message string // Error description // Error chaining err error // Underlying error (for Unwrap support) } // Error returns the error message with stage information. // Format: "go-yaml dump error in : " func (e *DumpError) Error() string { return fmt.Sprintf("go-yaml dump error in %s: %s", e.Stage, e.Message) } // Unwrap returns the underlying error. func (e *DumpError) Unwrap() error { return e.err } // NewDumpError creates a DumpError with an underlying cause. // The cause is accessible via Unwrap for use with [errors.Is] and [errors.As]. func NewDumpError(stage Stage, message string, cause error) *DumpError { return &DumpError{Stage: stage, Message: message, err: cause} } // failDump panics with a YAMLError wrapping a DumpError for the given stage. // If err is exactly a *DumpError it is passed through unchanged to avoid // double-wrapping (e.g. a user MarshalYAML that returns yaml.NewDumpError). // Errors that merely wrap a *DumpError are treated as ordinary errors so that // the outer wrapper's message and context are preserved. func failDump(stage Stage, err error) { if de, ok := err.(*DumpError); ok { panic(&YAMLError{de}) } panic(&YAMLError{&DumpError{Stage: stage, Message: err.Error(), err: err}}) } // failDumpf panics with a YAMLError wrapping a formatted DumpError. func failDumpf(stage Stage, format string, args ...any) { panic(&YAMLError{&DumpError{Stage: stage, Message: fmt.Sprintf(format, args...)}}) } // EmitterError represents an error that occurred during emitting. type EmitterError struct { Message string } // Error returns the error message. func (e EmitterError) Error() string { return fmt.Sprintf("yaml: %s", e.Message) } // WriterError represents an error that occurred while writing output. type WriterError struct { Err error } // Error returns the error message. func (e WriterError) Error() string { return fmt.Sprintf("yaml: %s", e.Err) } // Unwrap returns the underlying error. func (e WriterError) Unwrap() error { return e.Err } // LoadErrors is returned when one or more fields cannot be properly decoded. type LoadErrors struct { Errors []*LoadError } // Error returns a formatted error message listing all construct errors. func (e *LoadErrors) Error() string { var b strings.Builder b.WriteString("yaml: construct errors: ") for i, err := range e.Errors { if i > 0 { b.WriteString("; ") } b.WriteString(err.simpleError()) } return b.String() } // As implements [errors.As] for Go versions prior to 1.20 that don't support // the Unwrap() []error interface. It allows [LoadErrors] to match against // *LoadError or *TypeError targets. func (e *LoadErrors) As(target any) bool { switch t := target.(type) { case **LoadError: if len(e.Errors) == 0 { return false } *t = e.Errors[0] return true case **TypeError: var msgs []string for _, err := range e.Errors { msgs = append(msgs, err.simpleError()) } *t = &TypeError{Errors: msgs} return true } return false } // Is implements [errors.Is] for Go versions prior to 1.20 that don't support // the Unwrap() []error interface. It checks if any wrapped error matches // the target error. func (e *LoadErrors) Is(target error) bool { for _, err := range e.Errors { if errors.Is(err, target) { return true } } return false } // TypeError is a legacy error type retained for compatibility. // // A TypeError is returned by Unmarshal when one or more fields in // the YAML document cannot be properly decoded into the requested // types. When this error is returned, the value is still // unmarshaled partially. // // Deprecated: Use [LoadErrors] instead. type TypeError struct { Errors []string } // Error returns a formatted error message listing all unmarshal errors. func (e *TypeError) Error() string { return fmt.Sprintf("yaml: unmarshal errors: %s", strings.Join(e.Errors, "; ")) } // YAMLError is an internal error wrapper type. type YAMLError struct { Err error } // Error returns the error message. func (e *YAMLError) Error() string { return e.Err.Error() } // handleErr recovers from panics caused by yaml errors. // It's used in defer statements to convert YAMLError panics into regular errors. func handleErr(err *error) { if v := recover(); v != nil { if e, ok := v.(*YAMLError); ok { *err = e.Err } else { panic(v) } } } golang-go.yaml-yaml-v4-4.0.0~rc5/internal/libyaml/errors_test.go000066400000000000000000000216431521353103300245760ustar00rootroot00000000000000// Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Tests for error types. // Verifies error formatting, unwrapping, and error matching. package libyaml import ( "errors" "testing" "go.yaml.in/yaml/v4/internal/testutil/assert" ) func TestErrors(t *testing.T) { RunTestCases(t, "errors.yaml", map[string]TestHandler{ "load-error": runLoadErrorTest, "dump-error": runDumpErrorTest, "emitter-error": runEmitterYAMLErrorTest, "writer-error": runWriterYAMLErrorTest, "load-errors": runLoadErrorsTest, "load-errors-as": runLoadErrorsAsTest, "load-errors-is": runLoadErrorsIsTest, "type-error": runTypeYAMLErrorTest, }) } func runLoadErrorTest(t *testing.T, tc TestCase) { t.Helper() // Extract error spec from 'from' field errorSpec, ok := tc.From.(map[string]any) assert.Truef(t, ok, "from should be map[string]any, got %T", tc.From) err := buildLoadError(t, errorSpec) got := err.Error() want, ok := tc.Want.(string) assert.Truef(t, ok, "want should be string, got %T", tc.Want) assert.Equalf(t, want, got, "error message mismatch") // Verify Stage field if specified if stageStr, ok := errorSpec["stage"].(string); ok { assert.Equalf(t, Stage(stageStr), err.Stage, "Stage mismatch") } // Test Unwrap if specified if tc.Also == "unwrap" { unwrapped := err.Unwrap() if err.err != nil { assert.NotNilf(t, unwrapped, "Unwrap() should return non-nil when Err is set") assert.Equalf(t, err.err.Error(), unwrapped.Error(), "Unwrap() error message mismatch") } else { if unwrapped != nil { t.Fatalf("Unwrap() should return nil when Err is not set, got %v", unwrapped) } } } } func runDumpErrorTest(t *testing.T, tc TestCase) { t.Helper() errorSpec, ok := tc.From.(map[string]any) assert.Truef(t, ok, "from should be map[string]any, got %T", tc.From) err := buildDumpError(t, errorSpec) if err == nil { t.Fatal("buildDumpError returned nil") } got := err.Error() want, ok := tc.Want.(string) assert.Truef(t, ok, "want should be string, got %T", tc.Want) assert.Equalf(t, want, got, "error message mismatch") // Verify Stage field if specified; fail if defined but not a string if stageVal, hasStage := errorSpec["stage"]; hasStage { stageStr, ok := stageVal.(string) assert.Truef(t, ok, "stage should be string, got %T", stageVal) if ok { assert.Equalf(t, Stage(stageStr), err.Stage, "Stage mismatch") } } // Test Unwrap if specified if tc.Also == "unwrap" { unwrapped := err.Unwrap() if err.err != nil { assert.NotNilf(t, unwrapped, "Unwrap() should return non-nil when err is set") assert.Equalf(t, err.err.Error(), unwrapped.Error(), "Unwrap() error message mismatch") } else { if unwrapped != nil { t.Fatalf("Unwrap() should return nil when err is not set, got %v", unwrapped) } } } } func buildDumpError(t *testing.T, spec map[string]any) *DumpError { t.Helper() err := &DumpError{ Stage: Stage(getString(t, spec, "stage")), Message: getString(t, spec, "message"), } // Add underlying error if specified if errMsg, ok := spec["err"].(string); ok { err.err = errors.New(errMsg) } return err } func runEmitterYAMLErrorTest(t *testing.T, tc TestCase) { t.Helper() errorSpec, ok := tc.From.(map[string]any) assert.Truef(t, ok, "from should be map[string]any, got %T", tc.From) message := getString(t, errorSpec, "message") err := EmitterError{Message: message} got := err.Error() want, ok := tc.Want.(string) assert.Truef(t, ok, "want should be string, got %T", tc.Want) assert.Equalf(t, want, got, "error message mismatch") } func runWriterYAMLErrorTest(t *testing.T, tc TestCase) { t.Helper() errorSpec, ok := tc.From.(map[string]any) assert.Truef(t, ok, "from should be map[string]any, got %T", tc.From) message := getString(t, errorSpec, "message") err := WriterError{Err: errors.New(message)} got := err.Error() want, ok := tc.Want.(string) assert.Truef(t, ok, "want should be string, got %T", tc.Want) assert.Equalf(t, want, got, "error message mismatch") // Test Unwrap if specified if tc.Also == "unwrap" { unwrapped := err.Unwrap() assert.NotNilf(t, unwrapped, "Unwrap() should return non-nil") assert.Equalf(t, message, unwrapped.Error(), "Unwrap() error message mismatch") } } func runLoadErrorsTest(t *testing.T, tc TestCase) { t.Helper() errorSpec, ok := tc.From.(map[string]any) assert.Truef(t, ok, "from should be map[string]any, got %T", tc.From) errList := buildLoadErrorList(t, errorSpec) err := &LoadErrors{Errors: errList} got := err.Error() want, ok := tc.Want.(string) assert.Truef(t, ok, "want should be string, got %T", tc.Want) assert.Equalf(t, want, got, "error message mismatch") } func runLoadErrorsAsTest(t *testing.T, tc TestCase) { t.Helper() errorSpec, ok := tc.From.(map[string]any) assert.Truef(t, ok, "from should be map[string]any, got %T", tc.From) errList := buildLoadErrorList(t, errorSpec) err := &LoadErrors{Errors: errList} switch tc.As { case "TypeError": var target *TypeError gotAs := errors.As(err, &target) assert.Equalf(t, tc.WantAs, gotAs, "errors.As result mismatch") if tc.WantAs && target != nil { assert.Equalf(t, len(tc.WantMessages), len(target.Errors), "TypeError.Errors length mismatch") for i, wantMsg := range tc.WantMessages { wantStr, ok := wantMsg.(string) assert.Truef(t, ok, "want_messages[%d] should be string, got %T", i, wantMsg) assert.Equalf(t, wantStr, target.Errors[i], "TypeError.Errors[%d] mismatch", i) } } default: t.Fatalf("unknown as type: %s", tc.As) } } func runLoadErrorsIsTest(t *testing.T, tc TestCase) { t.Helper() errorSpec, ok := tc.From.(map[string]any) assert.Truef(t, ok, "from should be map[string]any, got %T", tc.From) errList := buildLoadErrorList(t, errorSpec) err := &LoadErrors{Errors: errList} // Check if any of the wrapped errors contains the target message gotIs := false for _, cerr := range err.Errors { if cerr.err != nil && cerr.err.Error() == tc.Is { gotIs = true break } } assert.Equalf(t, tc.WantIs, gotIs, "errors.Is result mismatch") } func runTypeYAMLErrorTest(t *testing.T, tc TestCase) { t.Helper() errorSpec, ok := tc.From.(map[string]any) assert.Truef(t, ok, "from should be map[string]any, got %T", tc.From) errorMsgs := getStringSlice(t, errorSpec, "errors") err := &TypeError{Errors: errorMsgs} got := err.Error() want, ok := tc.Want.(string) assert.Truef(t, ok, "want should be string, got %T", tc.Want) assert.Equalf(t, want, got, "error message mismatch") } // Helper functions func buildLoadError(t *testing.T, spec map[string]any) *LoadError { t.Helper() err := &LoadError{ Stage: Stage(getString(t, spec, "stage")), Mark: buildMark(t, spec, "mark"), Message: getString(t, spec, "message"), } // Add context if specified if contextMsg, ok := spec["context_message"].(string); ok { err.ContextMsg = contextMsg err.ContextMark = buildMark(t, spec, "context_mark") } // Add underlying error if specified if errMsg, ok := spec["err"].(string); ok { err.err = errors.New(errMsg) } return err } func buildMark(t *testing.T, spec map[string]any, key string) Mark { t.Helper() markSpec, ok := spec[key].(map[string]any) if !ok { return Mark{} } return Mark{ Line: getInt(t, markSpec, "line"), Column: getInt(t, markSpec, "column"), Index: getInt(t, markSpec, "index"), } } func buildLoadErrorList(t *testing.T, spec map[string]any) []*LoadError { t.Helper() errorsSpec, ok := spec["errors"].([]any) if !ok { return nil } var result []*LoadError for _, errSpec := range errorsSpec { errMap, ok := errSpec.(map[string]any) assert.Truef(t, ok, "error spec should be map[string]any") line := getInt(t, errMap, "line") message := getString(t, errMap, "message") result = append(result, &LoadError{ Stage: ConstructorStage, Mark: Mark{Line: line}, Message: message, err: errors.New(message), }) } return result } func getString(t *testing.T, spec map[string]any, key string) string { t.Helper() v, ok := spec[key] if !ok { return "" } s, ok := v.(string) assert.Truef(t, ok, "%s should be string, got %T", key, v) return s } func getInt(t *testing.T, spec map[string]any, key string) int { t.Helper() v, ok := spec[key] if !ok { return 0 } i, ok := v.(int) assert.Truef(t, ok, "%s should be int, got %T", key, v) return i } func getBool(t *testing.T, spec map[string]any, key string) bool { t.Helper() v, ok := spec[key] if !ok { return false } b, ok := v.(bool) assert.Truef(t, ok, "%s should be bool, got %T", key, v) return b } func getStringSlice(t *testing.T, spec map[string]any, key string) []string { t.Helper() v, ok := spec[key] if !ok { return nil } slice, ok := v.([]any) assert.Truef(t, ok, "%s should be []any, got %T", key, v) var result []string for i, item := range slice { s, ok := item.(string) assert.Truef(t, ok, "%s[%d] should be string, got %T", key, i, item) result = append(result, s) } return result } golang-go.yaml-yaml-v4-4.0.0~rc5/internal/libyaml/loader.go000066400000000000000000000210171521353103300234640ustar00rootroot00000000000000// Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // This file contains the Loader API for reading YAML documents. // // Primary functions: // - Load: Decode YAML document(s) into a value (use WithAll for multi-doc) // - NewLoader: Create a streaming loader from io.Reader package libyaml import ( "bytes" "errors" "io" "reflect" ) // A Loader reads and loads YAML values from an input stream with configurable // options. type Loader struct { composer *Composer resolver *Resolver constructor *Constructor options *Options docCount int } // NewLoader returns a new Loader that reads from r with the given options. // // The Loader introduces its own buffering and may read data from r beyond the // YAML values requested. func NewLoader(r io.Reader, opts ...Option) (*Loader, error) { o, err := ApplyOptions(opts...) if err != nil { return nil, err } c := NewComposerFromReader(r, o) c.SetStreamNodes(o.StreamNodes) return &Loader{ composer: c, resolver: NewResolver(o), constructor: NewConstructor(o), options: o, }, nil } // Load loads YAML document(s) with the given options. // // By default, Load requires exactly one document in the input. // If zero documents are found, it returns an error. // If multiple documents are found, it returns an error. // // Use WithAllDocuments() to load all documents into a slice: // // var configs []Config // yaml.Load(multiDocYAML, &configs, yaml.WithAllDocuments()) // // When WithAllDocuments is used, out must be a pointer to a slice. // Each document is loaded into the slice element type. // Zero documents results in an empty slice (no error). // // Maps and pointers (to a struct, string, int, etc) are accepted as out // values. If an internal pointer within a struct is not initialized, // the yaml package will initialize it if necessary. The out parameter // must not be nil. // // The type of the loaded values should be compatible with the respective // values in out. If one or more values cannot be loaded due to type // mismatches, decoding continues partially until the end of the YAML // content, and a *yaml.LoadErrors is returned with details for all // missed values. // // Struct fields are only loaded if they are exported (have an upper case // first letter), and are loaded using the field name lowercased as the // default key. Custom keys may be defined via the "yaml" name in the field // tag: the content preceding the first comma is used as the key, and the // following comma-separated options control the loading and dumping behavior. // // For example: // // type T struct { // F int `yaml:"a,omitempty"` // B int // } // var t T // yaml.Load([]byte("a: 1\nb: 2"), &t) // // See the documentation of Dump for the format of tags and a list of // supported tag options. func Load(in []byte, out any, opts ...Option) error { o, err := ApplyOptions(opts...) if err != nil { return err } if o.AllDocuments { // Multi-document mode: out must be pointer to slice return loadAll(in, out, o) } // Single-document mode: exactly one document required return loadSingle(in, out, o) } // Load reads the next YAML-encoded document from its input and stores it // in the value pointed to by v. // // Returns [io.EOF] when there are no more documents to read. // If WithSingleDocument option was set and a document was already read, // subsequent calls return [io.EOF]. // // Maps and pointers (to a struct, string, int, etc) are accepted as v // values. If an internal pointer within a struct is not initialized, // the yaml package will initialize it if necessary. The v parameter // must not be nil. // // Struct fields are only loaded if they are exported (have an upper case // first letter), and are loaded using the field name lowercased as the // default key. Custom keys may be defined via the "yaml" name in the field // tag: the content preceding the first comma is used as the key, and the // following comma-separated options control the loading and dumping behavior. // // See the documentation of the package-level Load function for more details // about YAML to Go conversion and tag options. func (l *Loader) Load(v any) (err error) { defer handleErr(&err) if l.options.SingleDocument && l.docCount > 0 { return io.EOF } // Stage 1: Compose - parse events into node tree (unresolved tags) node := l.composer.Compose() // *Node if node == nil { return io.EOF } l.docCount++ // Stage 2: Resolve - determine implicit types for untagged scalars l.resolver.Resolve(node) // Stage 3: Construct - convert node tree to Go values out := reflect.ValueOf(v) if out.Kind() == reflect.Pointer && !out.IsNil() { out = out.Elem() } l.constructor.Construct(node, out) if len(l.constructor.TypeErrors) > 0 { typeErrors := l.constructor.TypeErrors l.constructor.TypeErrors = nil return &LoadErrors{Errors: typeErrors} } return nil } // loadAll loads all documents from the input into a slice. // The out parameter must be a non-nil pointer to a slice. // Each document is appended to the slice as an element. func loadAll(in []byte, out any, opts *Options) error { outVal := reflect.ValueOf(out) if outVal.Kind() != reflect.Pointer || outVal.IsNil() { msg := "yaml: WithAllDocuments requires a non-nil pointer to a slice" return &LoadErrors{Errors: []*LoadError{{ Stage: ConstructorStage, Message: msg, err: errors.New(msg), }}} } sliceVal := outVal.Elem() if sliceVal.Kind() != reflect.Slice { msg := "yaml: WithAllDocuments requires a pointer to a slice" return &LoadErrors{Errors: []*LoadError{{ Stage: ConstructorStage, Message: msg, err: errors.New(msg), }}} } // Create a new slice (clear existing content) sliceVal.Set(reflect.MakeSlice(sliceVal.Type(), 0, 0)) l, err := NewLoader(bytes.NewReader(in), func(o *Options) error { *o = *opts // Copy options return nil }) if err != nil { return err } elemType := sliceVal.Type().Elem() for { // Create new element of slice's element type elemPtr := reflect.New(elemType) err := l.Load(elemPtr.Interface()) if err == io.EOF { break } if err != nil { return err } // Append loaded element to slice sliceVal.Set(reflect.Append(sliceVal, elemPtr.Elem())) } return nil } // loadSingle loads exactly one document from the input. // Returns an error if the input contains zero or multiple documents // (unless FromLegacy option is set for backward compatibility). func loadSingle(in []byte, out any, opts *Options) error { l, err := NewLoader(bytes.NewReader(in), func(o *Options) error { *o = *opts // Copy options return nil }) if err != nil { return err } // Load first document err = l.Load(out) if err == io.EOF { msg := "yaml: no documents in stream" return &LoadErrors{Errors: []*LoadError{{ Stage: ConstructorStage, Message: msg, err: errors.New(msg), }}} } if err != nil { return err } // Skip trailing document check for legacy Unmarshal() compatibility if opts.FromLegacy { return nil } // Check for additional documents var dummy any err = l.Load(&dummy) if err != io.EOF { if err != nil { // Some other error occurred return err } // Successfully loaded a second document - this is an error in strict mode msg := "yaml: expected single document, found multiple" return &LoadErrors{Errors: []*LoadError{{ Stage: ConstructorStage, Message: msg, err: errors.New(msg), }}} } return nil } // SetKnownFields enables or disables strict field checking for subsequent Load // calls. // This is used by the legacy Decoder.KnownFields() method. func (l *Loader) SetKnownFields(enable bool) { l.constructor.KnownFields = enable } // ComposeAndResolve composes and resolves the next document from the input // and returns the node without constructing Go values. This is used by // Unmarshal() to support the Unmarshaler interface. func (l *Loader) ComposeAndResolve() *Node { if l.options.SingleDocument && l.docCount > 0 { return nil } // Stage 1: Compose - parse events into node tree (unresolved tags) node := l.composer.Compose() if node == nil { return nil } l.docCount++ // Stage 2: Resolve - determine implicit types for untagged scalars l.resolver.Resolve(node) return node } // LoadAny parses YAML data into generic Go structures (map[string]any, []any). // // Useful for test data loading where the structure is unknown at compile time. // This is a convenience wrapper around Load with an any target. func LoadAny(data []byte) (any, error) { var result any if err := Load(data, &result); err != nil { return nil, err } return result, nil } golang-go.yaml-yaml-v4-4.0.0~rc5/internal/libyaml/loader_test.go000066400000000000000000000166651521353103300245400ustar00rootroot00000000000000// Tests for the streaming Loader API, including StreamNode functionality // and multi-document streaming. package libyaml import ( "bytes" "io" "testing" "go.yaml.in/yaml/v4/internal/testutil/assert" ) // TestStreamNodeEmptyStream tests that an empty stream returns a single StreamNode func TestStreamNodeEmptyStream(t *testing.T) { input := []byte("") loader, err := NewLoader(bytes.NewReader(input), WithStreamNodes()) assert.NoError(t, err) var nodes []Node for { var node Node err := loader.Load(&node) if err == io.EOF { break } assert.NoError(t, err) nodes = append(nodes, node) } // Empty stream should return exactly one StreamNode assert.Equal(t, 1, len(nodes)) assert.Equal(t, StreamNode, nodes[0].Kind) } // TestStreamNodeSingleDocument tests the pattern [Stream, Doc, Stream] for single document func TestStreamNodeSingleDocument(t *testing.T) { input := []byte("key: value\n") loader, err := NewLoader(bytes.NewReader(input), WithStreamNodes()) assert.NoError(t, err) var nodes []Node for { var node Node err := loader.Load(&node) if err == io.EOF { break } assert.NoError(t, err) nodes = append(nodes, node) } // Single document should return [Stream, Doc, Stream] assert.Equal(t, 3, len(nodes)) assert.Equal(t, StreamNode, nodes[0].Kind) assert.Equal(t, DocumentNode, nodes[1].Kind) assert.Equal(t, StreamNode, nodes[2].Kind) } // TestStreamNodeMultiDocument tests interleaved pattern for multi-document stream func TestStreamNodeMultiDocument(t *testing.T) { input := []byte("---\nkey1: value1\n---\nkey2: value2\n") loader, err := NewLoader(bytes.NewReader(input), WithStreamNodes()) assert.NoError(t, err) var nodes []Node for { var node Node err := loader.Load(&node) if err == io.EOF { break } assert.NoError(t, err) nodes = append(nodes, node) } // Two documents should return [Stream, Doc, Stream, Doc, Stream] assert.Equal(t, 5, len(nodes)) assert.Equal(t, StreamNode, nodes[0].Kind) assert.Equal(t, DocumentNode, nodes[1].Kind) assert.Equal(t, StreamNode, nodes[2].Kind) assert.Equal(t, DocumentNode, nodes[3].Kind) assert.Equal(t, StreamNode, nodes[4].Kind) } // TestStreamNodeDirectives tests that directives are captured on StreamNodes func TestStreamNodeDirectives(t *testing.T) { input := []byte("%YAML 1.1\n%TAG ! tag:example.com,2000:app/\n---\nkey: value\n") loader, err := NewLoader(bytes.NewReader(input), WithStreamNodes()) assert.NoError(t, err) var nodes []Node for { var node Node err := loader.Load(&node) if err == io.EOF { break } assert.NoError(t, err) nodes = append(nodes, node) } // Should return [Stream, Doc, Stream] assert.Equal(t, 3, len(nodes)) // First StreamNode should have encoding assert.Equal(t, StreamNode, nodes[0].Kind) if nodes[0].Stream == nil || nodes[0].Stream.Encoding == 0 { t.Fatal("first stream node should have encoding set") } // Second node is the StreamNode before the document with directives // Note: directives appear on the StreamNode BEFORE the document streamNode := nodes[0] if streamNode.Stream != nil && streamNode.Stream.Version != nil { assert.Equal(t, 1, streamNode.Stream.Version.Major) assert.Equal(t, 1, streamNode.Stream.Version.Minor) } if streamNode.Stream != nil && len(streamNode.Stream.TagDirectives) > 0 { found := false for _, td := range streamNode.Stream.TagDirectives { if td.Handle == "!" && td.Prefix == "tag:example.com,2000:app/" { found = true break } } assert.True(t, found) } } // TestStreamNodeEncoding tests that encoding is captured on first StreamNode func TestStreamNodeEncoding(t *testing.T) { input := []byte("key: value\n") loader, err := NewLoader(bytes.NewReader(input), WithStreamNodes()) assert.NoError(t, err) var node Node err = loader.Load(&node) assert.NoError(t, err) // First node should be a StreamNode with encoding assert.Equal(t, StreamNode, node.Kind) // Encoding should be set (non-zero) if node.Stream == nil || node.Stream.Encoding == 0 { t.Fatal("stream node should have encoding set") } } // TestWithoutStreamNodes tests backward compatibility (default behavior) func TestWithoutStreamNodes(t *testing.T) { input := []byte("---\nkey1: value1\n---\nkey2: value2\n") loader, err := NewLoader(bytes.NewReader(input)) assert.NoError(t, err) var nodes []Node for { var node Node err := loader.Load(&node) if err == io.EOF { break } assert.NoError(t, err) nodes = append(nodes, node) } // Without stream nodes, should only return DocumentNodes assert.Equal(t, 2, len(nodes)) assert.Equal(t, DocumentNode, nodes[0].Kind) assert.Equal(t, DocumentNode, nodes[1].Kind) } // TestStreamNodeDisabled tests explicitly disabling stream nodes func TestStreamNodeDisabled(t *testing.T) { input := []byte("key: value\n") loader, err := NewLoader(bytes.NewReader(input), WithStreamNodes(false)) assert.NoError(t, err) var node Node err = loader.Load(&node) assert.NoError(t, err) // Should get a DocumentNode, not a StreamNode assert.Equal(t, DocumentNode, node.Kind) } // TestLoadWithAllDocuments_TypedSlice tests loading multiple documents into a typed slice func TestLoadWithAllDocuments_TypedSlice(t *testing.T) { type Config struct { Name string `yaml:"name"` } input := []byte("---\nname: first\n---\nname: second\n---\nname: third\n") var configs []Config err := Load(input, &configs, WithAllDocuments()) assert.NoError(t, err) assert.Equal(t, 3, len(configs)) assert.Equal(t, "first", configs[0].Name) assert.Equal(t, "second", configs[1].Name) assert.Equal(t, "third", configs[2].Name) } // TestLoadWithAllDocuments_UntypedSlice tests loading multiple documents into []any func TestLoadWithAllDocuments_UntypedSlice(t *testing.T) { input := []byte("---\nname: first\n---\nname: second\n") var docs []any err := Load(input, &docs, WithAllDocuments()) assert.NoError(t, err) assert.Equal(t, 2, len(docs)) } // TestLoadWithAllDocuments_EmptyInput tests that 0 documents with WithAllDocuments results in empty slice func TestLoadWithAllDocuments_EmptyInput(t *testing.T) { input := []byte("") var docs []any err := Load(input, &docs, WithAllDocuments()) assert.NoError(t, err) assert.Equal(t, 0, len(docs)) } // TestLoadWithAllDocuments_NonSlice tests that WithAllDocuments with non-slice target returns error func TestLoadWithAllDocuments_NonSlice(t *testing.T) { input := []byte("---\nname: first\n---\nname: second\n") var single map[string]any err := Load(input, &single, WithAllDocuments()) assert.NotNil(t, err) assert.ErrorMatches(t, ".*WithAllDocuments requires a pointer to a slice.*", err) } // TestLoad_SingleDocument tests loading exactly one document func TestLoad_SingleDocument(t *testing.T) { type Config struct { Name string `yaml:"name"` } input := []byte("name: myconfig\n") var config Config err := Load(input, &config) assert.NoError(t, err) assert.Equal(t, "myconfig", config.Name) } // TestLoad_ZeroDocuments tests that 0 documents returns error func TestLoad_ZeroDocuments(t *testing.T) { input := []byte("") var config map[string]any err := Load(input, &config) assert.NotNil(t, err) assert.ErrorMatches(t, ".*no documents in stream.*", err) } // TestLoad_MultipleDocuments tests that 2+ documents returns error func TestLoad_MultipleDocuments(t *testing.T) { input := []byte("---\nname: first\n---\nname: second\n") var config map[string]any err := Load(input, &config) assert.NotNil(t, err) assert.ErrorMatches(t, ".*expected single document, found multiple.*", err) } golang-go.yaml-yaml-v4-4.0.0~rc5/internal/libyaml/node.go000066400000000000000000000327461521353103300231560ustar00rootroot00000000000000// Copyright 2011-2019 Canonical Ltd // Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Node types and constants for YAML tree representation. // Defines Kind, Style, and Node structure for intermediate YAML representation. package libyaml import ( "reflect" "strings" "unicode" "unicode/utf8" ) // Tag constants for YAML types const ( nullTag = "!!null" boolTag = "!!bool" strTag = "!!str" intTag = "!!int" floatTag = "!!float" timestampTag = "!!timestamp" seqTag = "!!seq" mapTag = "!!map" binaryTag = "!!binary" mergeTag = "!!merge" ) // longTagPrefix is the standard YAML tag prefix for core types. const longTagPrefix = "tag:yaml.org,2002:" // longTags maps short tags to their long form representations. // shortTags maps long tags to their short form representations. var ( longTags = make(map[string]string) shortTags = make(map[string]string) ) // init initializes the tag conversion maps. func init() { for _, stag := range []string{nullTag, boolTag, strTag, intTag, floatTag, timestampTag, seqTag, mapTag, binaryTag, mergeTag} { ltag := longTag(stag) longTags[stag] = ltag shortTags[ltag] = stag } } // shortTag converts a long-form tag to its short form (e.g., "tag:yaml.org,2002:str" to "!!str"). func shortTag(tag string) string { if strings.HasPrefix(tag, longTagPrefix) { if stag, ok := shortTags[tag]; ok { return stag } return "!!" + tag[len(longTagPrefix):] } return tag } // longTag converts a short-form tag to its long form (e.g., "!!str" to "tag:yaml.org,2002:str"). func longTag(tag string) string { if strings.HasPrefix(tag, "!!") { if ltag, ok := longTags[tag]; ok { return ltag } return longTagPrefix + tag[2:] } return tag } // Kind represents the type of YAML node type Kind uint32 // Kind constants define the different types of YAML nodes. const ( DocumentNode Kind = 1 << iota SequenceNode MappingNode ScalarNode AliasNode StreamNode ) // Style represents the formatting style of a YAML node type Style uint32 // Style constants define different formatting styles for YAML nodes. const ( TaggedStyle Style = 1 << iota DoubleQuotedStyle SingleQuotedStyle LiteralStyle FoldedStyle FlowStyle ) // StreamVersionDirective represents a YAML %YAML version directive for stream nodes. type StreamVersionDirective struct { Major int Minor int } // StreamTagDirective represents a YAML %TAG directive for stream nodes. type StreamTagDirective struct { Handle string Prefix string } // Stream holds stream-level metadata for StreamNode. // This includes encoding, version directive, and tag directives. type Stream struct { Encoding Encoding Version *StreamVersionDirective TagDirectives []StreamTagDirective } // Node represents an element in the YAML document hierarchy. While documents // are typically encoded and decoded into higher level types, such as structs // and maps, Node is an intermediate representation that allows detailed // control over the content being decoded or encoded. // // It's worth noting that although Node offers access into details such as // line numbers, columns, and comments, the content when re-encoded will not // have its original textual representation preserved. An effort is made to // render the data pleasantly, and to preserve comments near the data they // describe, though. // // Values that make use of the Node type interact with the yaml package in the // same way any other type would do, by encoding and decoding yaml data // directly or indirectly into them. // // For example: // // var person struct { // Name string // Address yaml.Node // } // err := yaml.Unmarshal(data, &person) // // Or by itself: // // var person Node // err := yaml.Unmarshal(data, &person) type Node struct { // Kind defines whether the node is a document, a mapping, a sequence, // a scalar value, or an alias to another node. The specific data type of // scalar nodes may be obtained via the ShortTag and LongTag methods. Kind Kind // Style allows customizing the appearance of the node in the tree. Style Style // Tag holds the YAML tag defining the data type for the value. // When decoding, this field will always be set to the resolved tag, // even when it wasn't explicitly provided in the YAML content. // When encoding, if this field is unset the value type will be // implied from the node properties, and if it is set, it will only // be serialized into the representation if TaggedStyle is used or // the implicit tag diverges from the provided one. Tag string // Value holds the unescaped and unquoted representation of the value. Value string // Anchor holds the anchor name for this node, which allows aliases to point to it. Anchor string // Alias holds the node that this alias points to. Only valid when Kind is AliasNode. Alias *Node // Content holds contained nodes for documents, mappings, and sequences. Content []*Node // HeadComment holds any comments in the lines preceding the node and // not separated by an empty line. HeadComment string // LineComment holds any comments at the end of the line where the node is in. LineComment string // FootComment holds any comments following the node and before empty lines. FootComment string // Line and Column hold the node position in the decoded YAML text. // These fields are not respected when encoding the node. Line int Column int // Stream holds stream metadata (non-nil only when Kind == StreamNode). Stream *Stream } // IsZero returns whether the node has all of its fields unset. func (n *Node) IsZero() bool { return n.Kind == 0 && n.Style == 0 && n.Tag == "" && n.Value == "" && n.Anchor == "" && n.Alias == nil && n.Content == nil && n.HeadComment == "" && n.LineComment == "" && n.FootComment == "" && n.Line == 0 && n.Column == 0 && n.Stream == nil } // LongTag returns the long form of the tag that indicates the data type for // the node. If the Tag field isn't explicitly defined, one will be computed // based on the node properties. func (n *Node) LongTag() string { return longTag(n.ShortTag()) } // ShortTag returns the short form of the YAML tag that indicates data type for // the node. If the Tag field isn't explicitly defined, one will be computed // based on the node properties. func (n *Node) ShortTag() string { if n.indicatedString() { return strTag } if n.Tag == "" || n.Tag == "!" { switch n.Kind { case MappingNode: return mapTag case SequenceNode: return seqTag case AliasNode: if n.Alias != nil { return n.Alias.ShortTag() } case ScalarNode: return strTag case 0: // Special case to make the zero value convenient. if n.IsZero() { return nullTag } } return "" } return shortTag(n.Tag) } // indicatedString returns true if the node's style explicitly indicates a string type. func (n *Node) indicatedString() bool { return n.Kind == ScalarNode && (shortTag(n.Tag) == strTag || (n.Tag == "" || n.Tag == "!") && n.Style&(SingleQuotedStyle|DoubleQuotedStyle|LiteralStyle|FoldedStyle) != 0) } // shouldUseLiteralStyle determines if a string should use literal style. // It returns true if the string contains newlines AND meets additional criteria: // - is at least 2 characters long // - contains at least one non-whitespace character func shouldUseLiteralStyle(s string) bool { if !strings.Contains(s, "\n") || len(s) < 2 { return false } // Must contain at least one non-whitespace character for _, r := range s { if !unicode.IsSpace(r) { return true } } return false } // SetString is a convenience function that sets the node to a string value // and defines its style in a pleasant way depending on its content. func (n *Node) SetString(s string) { n.Kind = ScalarNode if utf8.ValidString(s) { n.Value = s n.Tag = strTag } else { n.Value = encodeBase64(s) n.Tag = binaryTag } if shouldUseLiteralStyle(n.Value) { n.Style = LiteralStyle } } // Decode decodes the node and stores its data into the value pointed to by v. // // See the documentation for Unmarshal for details about the // conversion of YAML into a Go value. func (n *Node) Decode(v any) (err error) { d := NewConstructor(DefaultOptions) defer handleErr(&err) out := reflect.ValueOf(v) if out.Kind() == reflect.Pointer && !out.IsNil() { out = out.Elem() } d.Construct(n, out) if len(d.TypeErrors) > 0 { return &LoadErrors{Errors: d.TypeErrors} } return nil } // Load decodes the node and stores its data into the value pointed to by v, // applying the given options. // // This method is useful when you need to preserve options like WithKnownFields() // inside custom UnmarshalYAML implementations. // // Maps and pointers (to a struct, string, int, etc) are accepted as v // values. If an internal pointer within a struct is not initialized, // the yaml package will initialize it if necessary. The v parameter // must not be nil. // // See the documentation of the package-level Load function for details // about YAML to Go conversion and tag options. func (n *Node) Load(v any, opts ...Option) (err error) { defer handleErr(&err) o, err := ApplyOptions(opts...) if err != nil { return err } d := NewConstructor(o) out := reflect.ValueOf(v) if out.Kind() == reflect.Pointer && !out.IsNil() { out = out.Elem() } d.Construct(n, out) if len(d.TypeErrors) > 0 { return &LoadErrors{Errors: d.TypeErrors} } return nil } // Encode encodes value v and stores its representation in n. // // See the documentation for Marshal for details about the // conversion of Go values into YAML. func (n *Node) Encode(v any) (err error) { defer handleErr(&err) // Use the 3-stage dump pipeline with round-trip to preserve styles r := NewRepresenter(DefaultOptions) node := r.Represent("", reflect.ValueOf(v)) d := NewDesolver(DefaultOptions) d.Desolve(node) s := NewSerializer(nil, DefaultOptions) var out []byte s.Emitter.SetOutputString(&out) s.Serialize(node) s.Finish() // Parse back to get styles p := NewComposer(out, nil) p.Textless = true defer p.Destroy() doc := p.Compose() NewResolver(nil).Resolve(doc) *n = *doc.Content[0] return nil } // Dump encodes value v and stores its representation in n, // applying the given options. // // This method is useful when you need to apply specific encoding options // while building Node trees programmatically. // // See the documentation for Marshal for details about the // conversion of Go values into YAML. func (n *Node) Dump(v any, opts ...Option) (err error) { defer handleErr(&err) o, err := ApplyOptions(opts...) if err != nil { return err } // Use the 3-stage dump pipeline with round-trip to preserve styles r := NewRepresenter(o) node := r.Represent("", reflect.ValueOf(v)) d := NewDesolver(o) d.Desolve(node) s := NewSerializer(nil, o) var out []byte s.Emitter.SetOutputString(&out) s.Serialize(node) s.Finish() // Parse back to get styles p := NewComposer(out, nil) p.Textless = true defer p.Destroy() doc := p.Compose() NewResolver(nil).Resolve(doc) *n = *doc.Content[0] return nil } // Marshaler interface may be implemented by types to customize their // behavior when being marshaled into a YAML document. type Marshaler interface { MarshalYAML() (any, error) } // Unmarshaler is the interface implemented by types that can unmarshal // a YAML description of themselves. type Unmarshaler interface { UnmarshalYAML(node *Node) error } // IsZeroer is used to check whether an object is zero to determine whether // it should be omitted when marshaling with the ,omitempty flag. One notable // implementation is [time.Time]. type IsZeroer interface { IsZero() bool } // FromYAMLNode is a new interface that types can implement to customize // their unmarshaling behavior. It receives a Node directly and modifies // the receiver in place. // This is the preferred interface for new code. // // Note: This interface is reserved for the v4 API and is not yet fully // integrated into the current implementation. type FromYAMLNode interface { FromYAMLNode(*Node) error } // ToYAMLNode is a new interface that types can implement to customize // their marshaling behavior. It returns a Node directly. // This is the preferred interface for new code. // // Note: This interface is reserved for the v4 API and is not yet fully // integrated into the current implementation. type ToYAMLNode interface { ToYAMLNode() (*Node, error) } // isZero reports whether v represents the zero value for its type. // If v implements the IsZeroer interface, IsZero() is called. // Otherwise, zero is determined by checking type-specific conditions. // This is used to determine omitempty behavior when marshaling. func isZero(v reflect.Value) bool { kind := v.Kind() if z, ok := v.Interface().(IsZeroer); ok { if (kind == reflect.Pointer || kind == reflect.Interface) && v.IsNil() { return true } return z.IsZero() } switch kind { case reflect.String: return len(v.String()) == 0 case reflect.Interface, reflect.Pointer: return v.IsNil() case reflect.Slice: return v.Len() == 0 case reflect.Map: return v.Len() == 0 case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return v.Int() == 0 case reflect.Float32, reflect.Float64: return v.Float() == 0 case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: return v.Uint() == 0 case reflect.Bool: return !v.Bool() case reflect.Struct: vt := v.Type() for i := v.NumField() - 1; i >= 0; i-- { if vt.Field(i).PkgPath != "" { continue // Private field } if !isZero(v.Field(i)) { return false } } return true } return false } golang-go.yaml-yaml-v4-4.0.0~rc5/internal/libyaml/node_test.go000066400000000000000000000157751521353103300242200ustar00rootroot00000000000000// Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Tests for node.go functions and methods. package libyaml import ( "reflect" "testing" "go.yaml.in/yaml/v4/internal/testutil/assert" "go.yaml.in/yaml/v4/internal/testutil/datatest" ) func TestNode(t *testing.T) { handlers := map[string]TestHandler{ "isZero": runIsZeroTest, "set-string": runSetStringTest, "set-string-binary": runSetStringBinaryTest, "short-tag": runShortTagTest, "long-tag": runLongTagTest, "node-is-zero": runNodeIsZeroTest, "should-literal": runShouldLiteralTest, } RunTestCases(t, "node.yaml", handlers) } // runIsZeroTest tests the isZero function func runIsZeroTest(t *testing.T, tc TestCase) { t.Helper() var v reflect.Value // Handle special modifiers in 'also' field switch tc.Also { case "slice": // Nil slice case if tc.From == nil { v = reflect.ValueOf(([]int)(nil)) } else { // Convert from to slice if slice, ok := tc.From.([]any); ok { v = reflect.ValueOf(slice) } else { t.Fatalf("expected slice, got %T", tc.From) } } case "map": // Nil map case if tc.From == nil { v = reflect.ValueOf((map[string]any)(nil)) } else { // Convert from to map if m, ok := tc.From.(map[string]any); ok { v = reflect.ValueOf(m) } else { t.Fatalf("expected map, got %T", tc.From) } } default: // Regular value v = reflect.ValueOf(tc.From) } got := isZero(v) want := datatest.WantBool(t, tc.Want, false) assert.Equalf(t, want, got, "isZero() = %v, want %v", got, want) } // runSetStringTest tests the SetString method func runSetStringTest(t *testing.T, tc TestCase) { t.Helper() str, ok := tc.From.(string) if !ok { t.Fatalf("from should be string, got %T", tc.From) } node := &Node{} node.SetString(str) wantMap, ok := tc.Want.(map[string]any) if !ok { t.Fatalf("want should be a map, got %T", tc.Want) } // Check Kind if wantKind, ok := wantMap["kind"].(string); ok { gotKind := kindToString(node.Kind) assert.Equalf(t, wantKind, gotKind, "Kind = %v, want %v", gotKind, wantKind) } // Check Tag if wantTag, ok := wantMap["tag"].(string); ok { assert.Equalf(t, wantTag, node.Tag, "Tag = %v, want %v", node.Tag, wantTag) } // Check Value if wantValue, ok := wantMap["value"].(string); ok { assert.Equalf(t, wantValue, node.Value, "Value = %v, want %v", node.Value, wantValue) } // Check Style if wantStyle, ok := wantMap["style"].(string); ok { gotStyle := styleToString(node.Style) assert.Equalf(t, wantStyle, gotStyle, "Style = %v, want %v", gotStyle, wantStyle) } } // runSetStringBinaryTest tests SetString with invalid UTF-8 func runSetStringBinaryTest(t *testing.T, tc TestCase) { t.Helper() // Get binary input from hex input := HexToBytes(t, tc.InputHex) str := string(input) node := &Node{} node.SetString(str) wantMap, ok := tc.Want.(map[string]any) if !ok { t.Fatalf("want should be a map, got %T", tc.Want) } // Check Kind if wantKind, ok := wantMap["kind"].(string); ok { gotKind := kindToString(node.Kind) assert.Equalf(t, wantKind, gotKind, "Kind = %v, want %v", gotKind, wantKind) } // Check Tag if wantTag, ok := wantMap["tag"].(string); ok { assert.Equalf(t, wantTag, node.Tag, "Tag = %v, want %v", node.Tag, wantTag) } // For binary data, we just verify it's base64 encoded (not checking exact value) if node.Tag == binaryTag { assert.Truef(t, len(node.Value) > 0, "binary value should not be empty") } // Check Style if wantStyle, ok := wantMap["style"].(string); ok { gotStyle := styleToString(node.Style) assert.Equalf(t, wantStyle, gotStyle, "Style = %v, want %v", gotStyle, wantStyle) } } // runShortTagTest tests the shortTag function func runShortTagTest(t *testing.T, tc TestCase) { t.Helper() node := nodeFromSpec(t, tc.Node) got := node.ShortTag() want, ok := tc.Want.(string) if !ok { t.Fatalf("want should be string, got %T", tc.Want) } assert.Equalf(t, want, got, "ShortTag() = %v, want %v", got, want) } // runLongTagTest tests the longTag function func runLongTagTest(t *testing.T, tc TestCase) { t.Helper() node := nodeFromSpec(t, tc.Node) got := node.LongTag() want, ok := tc.Want.(string) if !ok { t.Fatalf("want should be string, got %T", tc.Want) } assert.Equalf(t, want, got, "LongTag() = %v, want %v", got, want) } // runNodeIsZeroTest tests the Node.IsZero method func runNodeIsZeroTest(t *testing.T, tc TestCase) { t.Helper() node := nodeFromSpec(t, tc.Node) got := node.IsZero() want := datatest.WantBool(t, tc.Want, false) assert.Equalf(t, want, got, "Node.IsZero() = %v, want %v", got, want) } // runShouldLiteralTest tests the shouldUseLiteralStyle helper func runShouldLiteralTest(t *testing.T, tc TestCase) { t.Helper() str, ok := tc.From.(string) if !ok { t.Fatalf("from should be string, got %T", tc.From) } got := shouldUseLiteralStyle(str) want := datatest.WantBool(t, tc.Want, false) assert.Equalf(t, want, got, "shouldUseLiteralStyle() = %v, want %v", got, want) } // nodeFromSpec creates a Node from a NodeSpec func nodeFromSpec(t *testing.T, spec NodeSpec) *Node { t.Helper() node := &Node{} // Set Kind if spec.Kind != "" { node.Kind = parseKind(t, spec.Kind) } // Set Tag node.Tag = spec.Tag // Set Value node.Value = spec.Value // Set Style if spec.Style != "" { node.Style = parseStyle(t, spec.Style) } return node } // parseKind converts a string to Kind func parseKind(t *testing.T, s string) Kind { t.Helper() switch s { case "Document": return DocumentNode case "Sequence": return SequenceNode case "Mapping": return MappingNode case "Scalar": return ScalarNode case "Alias": return AliasNode case "Stream": return StreamNode case "": return 0 default: t.Fatalf("unknown kind: %s", s) return 0 } } // parseStyle converts a string to Style func parseStyle(t *testing.T, s string) Style { t.Helper() switch s { case "Tagged": return TaggedStyle case "DoubleQuoted": return DoubleQuotedStyle case "SingleQuoted": return SingleQuotedStyle case "Literal": return LiteralStyle case "Folded": return FoldedStyle case "Flow": return FlowStyle case "": return 0 default: t.Fatalf("unknown style: %s", s) return 0 } } // kindToString converts Kind to string for comparison func kindToString(k Kind) string { switch k { case DocumentNode: return "DocumentNode" case SequenceNode: return "SequenceNode" case MappingNode: return "MappingNode" case ScalarNode: return "ScalarNode" case AliasNode: return "AliasNode" case StreamNode: return "StreamNode" case 0: return "" default: return "unknown" } } // styleToString converts Style to string for comparison func styleToString(s Style) string { switch s { case TaggedStyle: return "tagged" case DoubleQuotedStyle: return "double-quoted" case SingleQuotedStyle: return "single-quoted" case LiteralStyle: return "literal" case FoldedStyle: return "folded" case FlowStyle: return "flow" case 0: return "plain" default: return "unknown" } } golang-go.yaml-yaml-v4-4.0.0~rc5/internal/libyaml/options.go000066400000000000000000000330201521353103300237060ustar00rootroot00000000000000// Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Options configuration for loading and dumping YAML. // Provides centralized control for indentation, line width, strictness, and // more. package libyaml import ( "errors" "fmt" ) // Options holds configuration for both loading and dumping YAML. type Options struct { // Loading options KnownFields bool // Enforce known fields in structs SingleDocument bool // Only load first document UniqueKeys bool // Enforce unique keys in mappings StreamNodes bool // Enable stream node emission AllDocuments bool // Load/Dump all documents in multi-document streams // Dumping options Indent int // Indentation spaces (2-9) CompactSeqIndent bool // Whether '- ' counts as indentation LineWidth int // Preferred line width (-1 for unlimited) Unicode bool // Allow non-ASCII characters Canonical bool // Canonical YAML output LineBreak LineBreak // Line ending style ExplicitStart bool // Always emit --- ExplicitEnd bool // Always emit ... FlowSimpleCollections bool // Use flow style for simple collections QuotePreference QuoteStyle // Preferred quote style when quoting is required // Safety limit checks (set by ApplyOptions or WithPlugin(limit.New(...))) DepthCheck func(depth int, ctx *DepthContext) error AliasCheck func(aliasCount, constructCount int) error // Private options (not exported, used internally) FromLegacy bool // Indicates legacy Unmarshal()/Decoder path (check Unmarshaler, allow trailing content) } // Option allows configuring YAML loading and dumping operations. type Option func(*Options) error // DepthKind represents the type of nesting (flow or block). type DepthKind string // DepthKindFlow and DepthKindBlock are the possible values of DepthContext.Kind. const ( DepthKindFlow DepthKind = "flow" DepthKindBlock DepthKind = "block" ) // DepthContext holds context about a nesting depth check. type DepthContext struct { Kind DepthKind } // DefaultDepthCheck is the default depth check function. // It returns an error when depth exceeds 10000. func DefaultDepthCheck(depth int, ctx *DepthContext) error { const maxDepth = 10000 if depth > maxDepth { return fmt.Errorf("exceeded max depth of %d", maxDepth) } return nil } // DefaultAliasCheck is the default alias check function. // It uses a ratio-based heuristic to prevent DoS attacks via excessive aliasing. func DefaultAliasCheck(aliasCount, constructCount int) error { const ( aliasRatioRangeLow = 400000 aliasRatioRangeHigh = 4000000 aliasRatioRange = float64(aliasRatioRangeHigh - aliasRatioRangeLow) ) if aliasCount <= 100 || constructCount <= 1000 { return nil } var allowed float64 switch { case constructCount <= aliasRatioRangeLow: allowed = 0.99 case constructCount >= aliasRatioRangeHigh: allowed = 0.10 default: allowed = 0.99 - 0.89*(float64(constructCount-aliasRatioRangeLow)/aliasRatioRange) } if float64(aliasCount)/float64(constructCount) > allowed { return errors.New("document contains excessive aliasing") } return nil } // WithIndent sets the number of spaces to use for indentation when // dumping YAML content. // // Valid values are 2-9. Common choices: 2 (compact), 4 (readable). func WithIndent(indent int) Option { return func(o *Options) error { if indent < 2 || indent > 9 { return errors.New("yaml: indent must be between 2 and 9 spaces") } o.Indent = indent return nil } } // WithCompactSeqIndent configures whether the sequence indicator '- ' is // considered part of the indentation when dumping YAML content. // // If compact is true, '- ' is treated as part of the indentation. // If compact is false, '- ' is not treated as part of the indentation. // When called without arguments, defaults to true. func WithCompactSeqIndent(compact ...bool) Option { if len(compact) > 1 { return func(o *Options) error { return errors.New("yaml: WithCompactSeqIndent accepts at most one argument") } } val := len(compact) == 0 || compact[0] return func(o *Options) error { o.CompactSeqIndent = val return nil } } // WithKnownFields enables or disables strict field checking during YAML loading. // // When enabled, loading will return an error if the YAML input contains fields // that do not correspond to any fields in the target struct. // When called without arguments, defaults to true. func WithKnownFields(knownFields ...bool) Option { if len(knownFields) > 1 { return func(o *Options) error { return errors.New("yaml: WithKnownFields accepts at most one argument") } } val := len(knownFields) == 0 || knownFields[0] return func(o *Options) error { o.KnownFields = val return nil } } // WithSingleDocument configures the Loader to only process the first document // in a YAML stream. After the first document is loaded, subsequent calls to // Load will return [io.EOF]. // // When called without arguments, defaults to true. // // This is useful when you expect exactly one document and want behavior // similar to [Unmarshal]. func WithSingleDocument(singleDocument ...bool) Option { if len(singleDocument) > 1 { return func(o *Options) error { return errors.New("yaml: WithSingleDocument accepts at most one argument") } } val := len(singleDocument) == 0 || singleDocument[0] return func(o *Options) error { o.SingleDocument = val return nil } } // WithStreamNodes enables returning stream boundary nodes when loading YAML. // // When enabled, Loader.Load returns an interleaved sequence of StreamNode and // DocumentNode values: // // [StreamNode, DocNode, StreamNode, DocNode, ..., StreamNode] // // StreamNodes contain metadata about the stream including: // - Encoding (UTF-8, UTF-16LE, UTF-16BE) // - YAML version directive (%YAML) // - Tag directives (%TAG) // - Position information (Line, Column) // // An empty YAML stream returns a single StreamNode. // When called without arguments, defaults to true. // // The default is false. func WithStreamNodes(enable ...bool) Option { if len(enable) > 1 { return func(o *Options) error { return errors.New("yaml: WithStreamNodes accepts at most one argument") } } val := len(enable) == 0 || enable[0] return func(o *Options) error { o.StreamNodes = val return nil } } // WithAllDocuments enables multi-document mode for Load and Dump operations. // // When used with Load, the target must be a pointer to a slice. // All documents in the YAML stream will be decoded into the slice. // Zero documents results in an empty slice (no error). // // When used with Dump, the input must be a slice. // Each element will be encoded as a separate YAML document // with "---" separators. // // When called without arguments, defaults to true. // // The default is false (single-document mode). func WithAllDocuments(all ...bool) Option { if len(all) > 1 { return func(o *Options) error { return errors.New("yaml: WithAllDocuments accepts at most one argument") } } val := len(all) == 0 || all[0] return func(o *Options) error { o.AllDocuments = val return nil } } // WithLineWidth sets the preferred line width for YAML output. // // When encoding long strings, the encoder will attempt to wrap them at this // width using literal block style (|). Set to -1 or 0 for unlimited width. // // The default is 80 characters. func WithLineWidth(width int) Option { return func(o *Options) error { if width < 0 { width = -1 } o.LineWidth = width return nil } } // WithUnicode controls whether non-ASCII characters are allowed in YAML output. // // When true, non-ASCII characters appear as-is (e.g., "café"). // When false, non-ASCII characters are escaped (e.g., "caf\u00e9"). // When called without arguments, defaults to true. // // The default is true. func WithUnicode(unicode ...bool) Option { if len(unicode) > 1 { return func(o *Options) error { return errors.New("yaml: WithUnicode accepts at most one argument") } } val := len(unicode) == 0 || unicode[0] return func(o *Options) error { o.Unicode = val return nil } } // WithUniqueKeys enables or disables duplicate key detection during YAML loading. // // When enabled, loading will return an error if the YAML input contains // duplicate keys in any mapping. This is a security feature that prevents // key override attacks. // When called without arguments, defaults to true. // // The default is true. func WithUniqueKeys(uniqueKeys ...bool) Option { if len(uniqueKeys) > 1 { return func(o *Options) error { return errors.New("yaml: WithUniqueKeys accepts at most one argument") } } val := len(uniqueKeys) == 0 || uniqueKeys[0] return func(o *Options) error { o.UniqueKeys = val return nil } } // WithCanonical forces canonical YAML output format. // // When enabled, the encoder outputs strictly canonical YAML with explicit // tags for all values. This produces verbose output primarily useful for // debugging and YAML spec compliance testing. // When called without arguments, defaults to true. // // The default is false. func WithCanonical(canonical ...bool) Option { if len(canonical) > 1 { return func(o *Options) error { return errors.New("yaml: WithCanonical accepts at most one argument") } } val := len(canonical) == 0 || canonical[0] return func(o *Options) error { o.Canonical = val return nil } } // WithLineBreak sets the line ending style for YAML output. // // Available options: // - LineBreakLN: Unix-style \n (default) // - LineBreakCR: Old Mac-style \r // - LineBreakCRLN: Windows-style \r\n // // The default is LineBreakLN. func WithLineBreak(lineBreak LineBreak) Option { return func(o *Options) error { o.LineBreak = lineBreak return nil } } // WithExplicitStart controls whether document start markers (---) are always emitted. // // When true, every document begins with an explicit "---" marker. // When false (default), the marker is omitted for the first document. // When called without arguments, defaults to true. func WithExplicitStart(explicit ...bool) Option { if len(explicit) > 1 { return func(o *Options) error { return errors.New("yaml: WithExplicitStart accepts at most one argument") } } val := len(explicit) == 0 || explicit[0] return func(o *Options) error { o.ExplicitStart = val return nil } } // WithExplicitEnd controls whether document end markers (...) are always emitted. // // When true, every document ends with an explicit "..." marker. // When false (default), the marker is omitted. // When called without arguments, defaults to true. func WithExplicitEnd(explicit ...bool) Option { if len(explicit) > 1 { return func(o *Options) error { return errors.New("yaml: WithExplicitEnd accepts at most one argument") } } val := len(explicit) == 0 || explicit[0] return func(o *Options) error { o.ExplicitEnd = val return nil } } // WithFlowSimpleCollections controls whether simple collections use flow style. // // When true, sequences and mappings containing only scalar values (no nested // collections) are rendered in flow style if they fit within the line width. // Example: {name: test, count: 42} or [a, b, c] // When called without arguments, defaults to true. // // When false (default), all collections use block style. func WithFlowSimpleCollections(flow ...bool) Option { if len(flow) > 1 { return func(o *Options) error { return errors.New("yaml: WithFlowSimpleCollections accepts at most one argument") } } val := len(flow) == 0 || flow[0] return func(o *Options) error { o.FlowSimpleCollections = val return nil } } // WithQuotePreference sets the preferred quote style for strings that require // quoting. // // This option only affects strings that require quoting per the YAML spec. // Plain strings that don't need quoting remain unquoted regardless of this // setting. Quoting is required for: // - Strings that look like other YAML types (true, false, null, 123, etc.) // - Strings with leading/trailing whitespace // - Strings containing special YAML syntax characters // - Empty strings in certain contexts // // Quote styles: // - QuoteSingle: Use single quotes (v4 default) // - QuoteDouble: Use double quotes // - QuoteLegacy: Legacy v2/v3 behavior (mixed quoting) func WithQuotePreference(style QuoteStyle) Option { return func(o *Options) error { switch style { case QuoteSingle, QuoteDouble, QuoteLegacy: o.QuotePreference = style return nil default: return fmt.Errorf("invalid QuoteStyle value: %d", style) } } } // CombineOptions combines multiple options into a single Option. // This is useful for creating option presets or combining version defaults // with custom options. func CombineOptions(opts ...Option) Option { return func(o *Options) error { for _, opt := range opts { if err := opt(o); err != nil { return err } } return nil } } // ApplyOptions applies the given options to a new options struct. // Starts with v4 defaults. func ApplyOptions(opts ...Option) (*Options, error) { o := &Options{ Canonical: false, LineBreak: LN_BREAK, // v4 defaults Indent: 2, CompactSeqIndent: true, LineWidth: 80, Unicode: true, UniqueKeys: true, // Default safety limits DepthCheck: DefaultDepthCheck, AliasCheck: DefaultAliasCheck, } for _, opt := range opts { if err := opt(o); err != nil { return nil, err } } return o, nil } // DefaultOptions holds the default options for APIs that don't accept options. var DefaultOptions = &Options{ Indent: 4, LineWidth: -1, Unicode: true, UniqueKeys: true, QuotePreference: QuoteLegacy, } golang-go.yaml-yaml-v4-4.0.0~rc5/internal/libyaml/options_test.go000066400000000000000000000366431521353103300247630ustar00rootroot00000000000000// Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Tests for options.go functions and methods. package libyaml import ( "regexp" "testing" "go.yaml.in/yaml/v4/internal/testutil/assert" ) func TestOptions(t *testing.T) { handlers := map[string]TestHandler{ "with-indent": runWithIndentTest, "with-compact-seq-indent": runWithCompactSeqIndentTest, "with-known-fields": runWithKnownFieldsTest, "with-single-document": runWithSingleDocumentTest, "with-stream-nodes": runWithStreamNodesTest, "with-all-documents": runWithAllDocumentsTest, "with-line-width": runWithLineWidthTest, "with-unicode": runWithUnicodeTest, "with-unique-keys": runWithUniqueKeysTest, "with-canonical": runWithCanonicalTest, "with-line-break": runWithLineBreakTest, "with-explicit-start": runWithExplicitStartTest, "with-explicit-end": runWithExplicitEndTest, "with-flow-simple-collections": runWithFlowSimpleCollectionsTest, "with-quote-preference": runWithQuotePreferenceTest, "apply-options": runApplyOptionsTest, } RunTestCases(t, "options.yaml", handlers) } // runWithIndentTest tests WithIndent func runWithIndentTest(t *testing.T, tc TestCase) { t.Helper() indent, ok := tc.From.(int) if !ok { t.Fatalf("from should be int, got %T", tc.From) } opt := WithIndent(indent) opts := &Options{} err := opt(opts) if tc.Like != "" { // Expect error assert.NotNilf(t, err, "expected error matching %q", tc.Like) if err != nil { matched, _ := regexp.MatchString(tc.Like, err.Error()) assert.Truef(t, matched, "error %q should match %q", err.Error(), tc.Like) } } else { // Expect success assert.NoErrorf(t, err, "WithIndent(%d) error: %v", indent, err) checkWantFields(t, opts, tc.Want) } } // runWithCompactSeqIndentTest tests WithCompactSeqIndent func runWithCompactSeqIndentTest(t *testing.T, tc TestCase) { t.Helper() args := parseBoolSlice(t, tc.From) opt := WithCompactSeqIndent(args...) opts := &Options{} err := opt(opts) if tc.Like != "" { assert.NotNilf(t, err, "expected error matching %q", tc.Like) if err != nil { matched, _ := regexp.MatchString(tc.Like, err.Error()) assert.Truef(t, matched, "error %q should match %q", err.Error(), tc.Like) } } else { assert.NoErrorf(t, err, "WithCompactSeqIndent error: %v", err) checkWantFields(t, opts, tc.Want) } } // runWithKnownFieldsTest tests WithKnownFields func runWithKnownFieldsTest(t *testing.T, tc TestCase) { t.Helper() args := parseBoolSlice(t, tc.From) opt := WithKnownFields(args...) opts := &Options{} err := opt(opts) if tc.Like != "" { assert.NotNilf(t, err, "expected error matching %q", tc.Like) if err != nil { matched, _ := regexp.MatchString(tc.Like, err.Error()) assert.Truef(t, matched, "error %q should match %q", err.Error(), tc.Like) } } else { assert.NoErrorf(t, err, "WithKnownFields error: %v", err) checkWantFields(t, opts, tc.Want) } } // runWithSingleDocumentTest tests WithSingleDocument func runWithSingleDocumentTest(t *testing.T, tc TestCase) { t.Helper() args := parseBoolSlice(t, tc.From) opt := WithSingleDocument(args...) opts := &Options{} err := opt(opts) if tc.Like != "" { assert.NotNilf(t, err, "expected error matching %q", tc.Like) if err != nil { matched, _ := regexp.MatchString(tc.Like, err.Error()) assert.Truef(t, matched, "error %q should match %q", err.Error(), tc.Like) } } else { assert.NoErrorf(t, err, "WithSingleDocument error: %v", err) checkWantFields(t, opts, tc.Want) } } // runWithStreamNodesTest tests WithStreamNodes func runWithStreamNodesTest(t *testing.T, tc TestCase) { t.Helper() args := parseBoolSlice(t, tc.From) opt := WithStreamNodes(args...) opts := &Options{} err := opt(opts) if tc.Like != "" { assert.NotNilf(t, err, "expected error matching %q", tc.Like) if err != nil { matched, _ := regexp.MatchString(tc.Like, err.Error()) assert.Truef(t, matched, "error %q should match %q", err.Error(), tc.Like) } } else { assert.NoErrorf(t, err, "WithStreamNodes error: %v", err) checkWantFields(t, opts, tc.Want) } } // runWithAllDocumentsTest tests WithAllDocuments func runWithAllDocumentsTest(t *testing.T, tc TestCase) { t.Helper() args := parseBoolSlice(t, tc.From) opt := WithAllDocuments(args...) opts := &Options{} err := opt(opts) if tc.Like != "" { assert.NotNilf(t, err, "expected error matching %q", tc.Like) if err != nil { matched, _ := regexp.MatchString(tc.Like, err.Error()) assert.Truef(t, matched, "error %q should match %q", err.Error(), tc.Like) } } else { assert.NoErrorf(t, err, "WithAllDocuments error: %v", err) checkWantFields(t, opts, tc.Want) } } // runWithLineWidthTest tests WithLineWidth func runWithLineWidthTest(t *testing.T, tc TestCase) { t.Helper() width, ok := tc.From.(int) if !ok { t.Fatalf("from should be int, got %T", tc.From) } opt := WithLineWidth(width) opts := &Options{} err := opt(opts) assert.NoErrorf(t, err, "WithLineWidth(%d) error: %v", width, err) checkWantFields(t, opts, tc.Want) } // runWithUnicodeTest tests WithUnicode func runWithUnicodeTest(t *testing.T, tc TestCase) { t.Helper() args := parseBoolSlice(t, tc.From) opt := WithUnicode(args...) opts := &Options{} err := opt(opts) if tc.Like != "" { assert.NotNilf(t, err, "expected error matching %q", tc.Like) if err != nil { matched, _ := regexp.MatchString(tc.Like, err.Error()) assert.Truef(t, matched, "error %q should match %q", err.Error(), tc.Like) } } else { assert.NoErrorf(t, err, "WithUnicode error: %v", err) checkWantFields(t, opts, tc.Want) } } // runWithUniqueKeysTest tests WithUniqueKeys func runWithUniqueKeysTest(t *testing.T, tc TestCase) { t.Helper() args := parseBoolSlice(t, tc.From) opt := WithUniqueKeys(args...) opts := &Options{} err := opt(opts) if tc.Like != "" { assert.NotNilf(t, err, "expected error matching %q", tc.Like) if err != nil { matched, _ := regexp.MatchString(tc.Like, err.Error()) assert.Truef(t, matched, "error %q should match %q", err.Error(), tc.Like) } } else { assert.NoErrorf(t, err, "WithUniqueKeys error: %v", err) checkWantFields(t, opts, tc.Want) } } // runWithCanonicalTest tests WithCanonical func runWithCanonicalTest(t *testing.T, tc TestCase) { t.Helper() args := parseBoolSlice(t, tc.From) opt := WithCanonical(args...) opts := &Options{} err := opt(opts) if tc.Like != "" { assert.NotNilf(t, err, "expected error matching %q", tc.Like) if err != nil { matched, _ := regexp.MatchString(tc.Like, err.Error()) assert.Truef(t, matched, "error %q should match %q", err.Error(), tc.Like) } } else { assert.NoErrorf(t, err, "WithCanonical error: %v", err) checkWantFields(t, opts, tc.Want) } } // runWithLineBreakTest tests WithLineBreak func runWithLineBreakTest(t *testing.T, tc TestCase) { t.Helper() lineBreak := parseLineBreak(t, tc.From) opt := WithLineBreak(lineBreak) opts := &Options{} err := opt(opts) assert.NoErrorf(t, err, "WithLineBreak error: %v", err) checkWantFields(t, opts, tc.Want) } // runWithExplicitStartTest tests WithExplicitStart func runWithExplicitStartTest(t *testing.T, tc TestCase) { t.Helper() args := parseBoolSlice(t, tc.From) opt := WithExplicitStart(args...) opts := &Options{} err := opt(opts) if tc.Like != "" { assert.NotNilf(t, err, "expected error matching %q", tc.Like) if err != nil { matched, _ := regexp.MatchString(tc.Like, err.Error()) assert.Truef(t, matched, "error %q should match %q", err.Error(), tc.Like) } } else { assert.NoErrorf(t, err, "WithExplicitStart error: %v", err) checkWantFields(t, opts, tc.Want) } } // runWithExplicitEndTest tests WithExplicitEnd func runWithExplicitEndTest(t *testing.T, tc TestCase) { t.Helper() args := parseBoolSlice(t, tc.From) opt := WithExplicitEnd(args...) opts := &Options{} err := opt(opts) if tc.Like != "" { assert.NotNilf(t, err, "expected error matching %q", tc.Like) if err != nil { matched, _ := regexp.MatchString(tc.Like, err.Error()) assert.Truef(t, matched, "error %q should match %q", err.Error(), tc.Like) } } else { assert.NoErrorf(t, err, "WithExplicitEnd error: %v", err) checkWantFields(t, opts, tc.Want) } } // runWithFlowSimpleCollectionsTest tests WithFlowSimpleCollections func runWithFlowSimpleCollectionsTest(t *testing.T, tc TestCase) { t.Helper() args := parseBoolSlice(t, tc.From) opt := WithFlowSimpleCollections(args...) opts := &Options{} err := opt(opts) if tc.Like != "" { assert.NotNilf(t, err, "expected error matching %q", tc.Like) if err != nil { matched, _ := regexp.MatchString(tc.Like, err.Error()) assert.Truef(t, matched, "error %q should match %q", err.Error(), tc.Like) } } else { assert.NoErrorf(t, err, "WithFlowSimpleCollections error: %v", err) checkWantFields(t, opts, tc.Want) } } // runWithQuotePreferenceTest tests WithQuotePreference func runWithQuotePreferenceTest(t *testing.T, tc TestCase) { t.Helper() style := parseQuoteStyle(t, tc.From) opt := WithQuotePreference(style) opts := &Options{} err := opt(opts) if tc.Like != "" { assert.NotNilf(t, err, "expected error matching %q", tc.Like) if err != nil { matched, _ := regexp.MatchString(tc.Like, err.Error()) assert.Truef(t, matched, "error %q should match %q", err.Error(), tc.Like) } } else { assert.NoErrorf(t, err, "WithQuotePreference error: %v", err) checkWantFields(t, opts, tc.Want) } } // runApplyOptionsTest tests ApplyOptions func runApplyOptionsTest(t *testing.T, tc TestCase) { t.Helper() // Test with no options to verify v4 defaults opts, err := ApplyOptions() assert.NoErrorf(t, err, "ApplyOptions error: %v", err) if opts != nil { checkWantFields(t, opts, tc.Want) } } // Helper functions // parseBoolSlice converts tc.From to []bool func parseBoolSlice(t *testing.T, from any) []bool { t.Helper() slice, ok := from.([]any) if !ok { t.Fatalf("from should be []any, got %T", from) } result := make([]bool, len(slice)) for i, v := range slice { b, ok := v.(bool) if !ok { t.Fatalf("from[%d] should be bool, got %T", i, v) } result[i] = b } return result } // parseLineBreak converts string or int to LineBreak func parseLineBreak(t *testing.T, from any) LineBreak { t.Helper() switch v := from.(type) { case string: switch v { case "LN_BREAK": return LN_BREAK case "CR_BREAK": return CR_BREAK case "CRLN_BREAK": return CRLN_BREAK default: t.Fatalf("unknown LineBreak constant: %s", v) } case int: return LineBreak(v) default: t.Fatalf("from should be string or int, got %T", from) } return 0 } // parseQuoteStyle converts string or int to QuoteStyle func parseQuoteStyle(t *testing.T, from any) QuoteStyle { t.Helper() switch v := from.(type) { case string: switch v { case "QuoteSingle": return QuoteSingle case "QuoteDouble": return QuoteDouble case "QuoteLegacy": return QuoteLegacy default: t.Fatalf("unknown QuoteStyle constant: %s", v) } case int: return QuoteStyle(v) default: t.Fatalf("from should be string or int, got %T", from) } return 0 } // checkWantFields verifies expected fields in Options func checkWantFields(t *testing.T, opts *Options, want any) { t.Helper() if want == nil { return } wantMap, ok := want.(map[string]any) if !ok { t.Fatalf("want should be map, got %T", want) } for key, expectedValue := range wantMap { switch key { case "indent": expected, ok := expectedValue.(int) if !ok { t.Fatalf("want.indent should be int, got %T", expectedValue) } assert.Equalf(t, expected, opts.Indent, "Indent = %d, want %d", opts.Indent, expected) case "compact_seq_indent": expected, ok := expectedValue.(bool) if !ok { t.Fatalf("want.compact_seq_indent should be bool, got %T", expectedValue) } assert.Equalf(t, expected, opts.CompactSeqIndent, "CompactSeqIndent = %v, want %v", opts.CompactSeqIndent, expected) case "known_fields": expected, ok := expectedValue.(bool) if !ok { t.Fatalf("want.known_fields should be bool, got %T", expectedValue) } assert.Equalf(t, expected, opts.KnownFields, "KnownFields = %v, want %v", opts.KnownFields, expected) case "single_document": expected, ok := expectedValue.(bool) if !ok { t.Fatalf("want.single_document should be bool, got %T", expectedValue) } assert.Equalf(t, expected, opts.SingleDocument, "SingleDocument = %v, want %v", opts.SingleDocument, expected) case "stream_nodes": expected, ok := expectedValue.(bool) if !ok { t.Fatalf("want.stream_nodes should be bool, got %T", expectedValue) } assert.Equalf(t, expected, opts.StreamNodes, "StreamNodes = %v, want %v", opts.StreamNodes, expected) case "all_documents": expected, ok := expectedValue.(bool) if !ok { t.Fatalf("want.all_documents should be bool, got %T", expectedValue) } assert.Equalf(t, expected, opts.AllDocuments, "AllDocuments = %v, want %v", opts.AllDocuments, expected) case "line_width": expected, ok := expectedValue.(int) if !ok { t.Fatalf("want.line_width should be int, got %T", expectedValue) } assert.Equalf(t, expected, opts.LineWidth, "LineWidth = %d, want %d", opts.LineWidth, expected) case "unicode": expected, ok := expectedValue.(bool) if !ok { t.Fatalf("want.unicode should be bool, got %T", expectedValue) } assert.Equalf(t, expected, opts.Unicode, "Unicode = %v, want %v", opts.Unicode, expected) case "unique_keys": expected, ok := expectedValue.(bool) if !ok { t.Fatalf("want.unique_keys should be bool, got %T", expectedValue) } assert.Equalf(t, expected, opts.UniqueKeys, "UniqueKeys = %v, want %v", opts.UniqueKeys, expected) case "canonical": expected, ok := expectedValue.(bool) if !ok { t.Fatalf("want.canonical should be bool, got %T", expectedValue) } assert.Equalf(t, expected, opts.Canonical, "Canonical = %v, want %v", opts.Canonical, expected) case "line_break": expectedStr, ok := expectedValue.(string) if !ok { t.Fatalf("want.line_break should be string, got %T", expectedValue) } expected := parseLineBreak(t, expectedStr) assert.Equalf(t, expected, opts.LineBreak, "LineBreak = %v, want %v", opts.LineBreak, expected) case "explicit_start": expected, ok := expectedValue.(bool) if !ok { t.Fatalf("want.explicit_start should be bool, got %T", expectedValue) } assert.Equalf(t, expected, opts.ExplicitStart, "ExplicitStart = %v, want %v", opts.ExplicitStart, expected) case "explicit_end": expected, ok := expectedValue.(bool) if !ok { t.Fatalf("want.explicit_end should be bool, got %T", expectedValue) } assert.Equalf(t, expected, opts.ExplicitEnd, "ExplicitEnd = %v, want %v", opts.ExplicitEnd, expected) case "flow_simple_collections": expected, ok := expectedValue.(bool) if !ok { t.Fatalf("want.flow_simple_collections should be bool, got %T", expectedValue) } assert.Equalf(t, expected, opts.FlowSimpleCollections, "FlowSimpleCollections = %v, want %v", opts.FlowSimpleCollections, expected) case "quote_preference": expectedStr, ok := expectedValue.(string) if !ok { t.Fatalf("want.quote_preference should be string, got %T", expectedValue) } expected := parseQuoteStyle(t, expectedStr) assert.Equalf(t, expected, opts.QuotePreference, "QuotePreference = %v, want %v", opts.QuotePreference, expected) default: t.Fatalf("unknown want field: %s", key) } } } golang-go.yaml-yaml-v4-4.0.0~rc5/internal/libyaml/parser.go000066400000000000000000001421541521353103300235200ustar00rootroot00000000000000// Copyright 2006-2010 Kirill Simonov // Copyright 2011-2019 Canonical Ltd // Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 AND MIT // Parser stage: Transforms token stream into event stream. // Implements a recursive-descent parser (LL(1)) following the YAML grammar // specification. // // The parser implements the following grammar: // // stream ::= STREAM-START implicit_document? explicit_document* STREAM-END // implicit_document ::= block_node DOCUMENT-END* // explicit_document ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END* // block_node_or_indentless_sequence ::= // ALIAS // | properties (block_content | indentless_block_sequence)? // | block_content // | indentless_block_sequence // block_node ::= ALIAS // | properties block_content? // | block_content // flow_node ::= ALIAS // | properties flow_content? // | flow_content // properties ::= TAG ANCHOR? | ANCHOR TAG? // block_content ::= block_collection | flow_collection | SCALAR // flow_content ::= flow_collection | SCALAR // block_collection ::= block_sequence | block_mapping // flow_collection ::= flow_sequence | flow_mapping // block_sequence ::= BLOCK-SEQUENCE-START (BLOCK-ENTRY block_node?)* BLOCK-END // indentless_sequence ::= (BLOCK-ENTRY block_node?)+ // block_mapping ::= BLOCK-MAPPING_START // ((KEY block_node_or_indentless_sequence?)? // (VALUE block_node_or_indentless_sequence?)?)* // BLOCK-END // flow_sequence ::= FLOW-SEQUENCE-START // (flow_sequence_entry FLOW-ENTRY)* // flow_sequence_entry? // FLOW-SEQUENCE-END // flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? // flow_mapping ::= FLOW-MAPPING-START // (flow_mapping_entry FLOW-ENTRY)* // flow_mapping_entry? // FLOW-MAPPING-END // flow_mapping_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? package libyaml import ( "bytes" "io" "strings" ) // ReadHandler is called by the [Parser] when it needs to read more bytes // from the input source. The handler should fill the provided buffer with // up to len(buffer) bytes from the input source. // // The arguments are as follows: // // [in] parser The parser object. // [out] buffer The buffer for reading. // [out] size_read The actual number of bytes read from the source. // // On success, the handler should return 1. If the handler failed, // the returned value should be 0. On EOF, the handler should set the // size_read to 0 and return 1. type ReadHandler func(parser *Parser, buffer []byte) (n int, err error) // SimpleKey holds information about a potential simple key. type SimpleKey struct { flow_level int // What flow level is the key at? required bool // Is a simple key required? token_number int // The number of the token. mark Mark // The position mark. } // ParserState represents the state of the parser. type ParserState int // Parser state constants define the different states the parser can be in. const ( PARSE_STREAM_START_STATE ParserState = iota PARSE_IMPLICIT_DOCUMENT_START_STATE // Expect the beginning of an implicit document. PARSE_DOCUMENT_START_STATE // Expect DOCUMENT-START. PARSE_DOCUMENT_CONTENT_STATE // Expect the content of a document. PARSE_DOCUMENT_END_STATE // Expect DOCUMENT-END. PARSE_BLOCK_NODE_STATE // Expect a block node. PARSE_BLOCK_SEQUENCE_FIRST_ENTRY_STATE // Expect the first entry of a block sequence. PARSE_BLOCK_SEQUENCE_ENTRY_STATE // Expect an entry of a block sequence. PARSE_INDENTLESS_SEQUENCE_ENTRY_STATE // Expect an entry of an indentless sequence. PARSE_BLOCK_MAPPING_FIRST_KEY_STATE // Expect the first key of a block mapping. PARSE_BLOCK_MAPPING_KEY_STATE // Expect a block mapping key. PARSE_BLOCK_MAPPING_VALUE_STATE // Expect a block mapping value. PARSE_FLOW_SEQUENCE_FIRST_ENTRY_STATE // Expect the first entry of a flow sequence. PARSE_FLOW_SEQUENCE_ENTRY_STATE // Expect an entry of a flow sequence. PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_KEY_STATE // Expect a key of an ordered mapping. PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_VALUE_STATE // Expect a value of an ordered mapping. PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_END_STATE // Expect the and of an ordered mapping entry. PARSE_FLOW_MAPPING_FIRST_KEY_STATE // Expect the first key of a flow mapping. PARSE_FLOW_MAPPING_KEY_STATE // Expect a key of a flow mapping. PARSE_FLOW_MAPPING_VALUE_STATE // Expect a value of a flow mapping. PARSE_FLOW_MAPPING_EMPTY_VALUE_STATE // Expect an empty value of a flow mapping. PARSE_END_STATE // Expect nothing. ) // String returns a string representation of the parser state. func (ps ParserState) String() string { switch ps { case PARSE_STREAM_START_STATE: return "PARSE_STREAM_START_STATE" case PARSE_IMPLICIT_DOCUMENT_START_STATE: return "PARSE_IMPLICIT_DOCUMENT_START_STATE" case PARSE_DOCUMENT_START_STATE: return "PARSE_DOCUMENT_START_STATE" case PARSE_DOCUMENT_CONTENT_STATE: return "PARSE_DOCUMENT_CONTENT_STATE" case PARSE_DOCUMENT_END_STATE: return "PARSE_DOCUMENT_END_STATE" case PARSE_BLOCK_NODE_STATE: return "PARSE_BLOCK_NODE_STATE" case PARSE_BLOCK_SEQUENCE_FIRST_ENTRY_STATE: return "PARSE_BLOCK_SEQUENCE_FIRST_ENTRY_STATE" case PARSE_BLOCK_SEQUENCE_ENTRY_STATE: return "PARSE_BLOCK_SEQUENCE_ENTRY_STATE" case PARSE_INDENTLESS_SEQUENCE_ENTRY_STATE: return "PARSE_INDENTLESS_SEQUENCE_ENTRY_STATE" case PARSE_BLOCK_MAPPING_FIRST_KEY_STATE: return "PARSE_BLOCK_MAPPING_FIRST_KEY_STATE" case PARSE_BLOCK_MAPPING_KEY_STATE: return "PARSE_BLOCK_MAPPING_KEY_STATE" case PARSE_BLOCK_MAPPING_VALUE_STATE: return "PARSE_BLOCK_MAPPING_VALUE_STATE" case PARSE_FLOW_SEQUENCE_FIRST_ENTRY_STATE: return "PARSE_FLOW_SEQUENCE_FIRST_ENTRY_STATE" case PARSE_FLOW_SEQUENCE_ENTRY_STATE: return "PARSE_FLOW_SEQUENCE_ENTRY_STATE" case PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_KEY_STATE: return "PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_KEY_STATE" case PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_VALUE_STATE: return "PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_VALUE_STATE" case PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_END_STATE: return "PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_END_STATE" case PARSE_FLOW_MAPPING_FIRST_KEY_STATE: return "PARSE_FLOW_MAPPING_FIRST_KEY_STATE" case PARSE_FLOW_MAPPING_KEY_STATE: return "PARSE_FLOW_MAPPING_KEY_STATE" case PARSE_FLOW_MAPPING_VALUE_STATE: return "PARSE_FLOW_MAPPING_VALUE_STATE" case PARSE_FLOW_MAPPING_EMPTY_VALUE_STATE: return "PARSE_FLOW_MAPPING_EMPTY_VALUE_STATE" case PARSE_END_STATE: return "PARSE_END_STATE" } return "" } // AliasData holds information about aliases. type AliasData struct { anchor []byte // The anchor. index int // The node id. mark Mark // The anchor mark. } // Comment holds information about a comment in the YAML stream. type Comment struct { ScanMark Mark // Position where scanning for comments started TokenMark Mark // Position after which tokens will be associated with this comment StartMark Mark // Position of '#' comment mark EndMark Mark // Position where comment terminated Head []byte Line []byte Foot []byte } // Parser structure holds all information about the current // state of the parser. type Parser struct { lastError error // Reader stuff read_handler ReadHandler // Read handler. input_reader io.Reader // File input data. input []byte // String input data. input_pos int eof bool // EOF flag buffer []byte // The working buffer. buffer_pos int // The current position of the buffer. unread int // The number of unread characters in the buffer. newlines int // The number of line breaks since last non-break/non-blank character raw_buffer []byte // The raw buffer. raw_buffer_pos int // The current position of the buffer. encoding Encoding // The input encoding. offset int // The offset of the current position (in bytes). mark Mark // The mark of the current position. // Comments HeadComment []byte // The current head comments LineComment []byte // The current line comments FootComment []byte // The current foot comments tail_comment []byte // Foot comment that happens at the end of a block. stem_comment []byte // Comment in item preceding a nested structure (list inside list item, etc) comments []Comment // The folded comments for all parsed tokens comments_head int skip_comments bool // Skip comment scanning for performance // Scanner stuff stream_start_produced bool // Have we started to scan the input stream? stream_end_produced bool // Have we reached the end of the input stream? flow_level int // The number of unclosed '[' and '{' indicators. tokens []Token // The tokens queue. tokens_head int // The head of the tokens queue. tokens_parsed int // The number of tokens fetched from the queue. token_available bool // Does the tokens queue contain a token ready for dequeueing. indent int // The current indentation level. indents []int // The indentation levels stack. simple_key_allowed bool // May a simple key occur at the current position? simple_key_possible bool // Is the current simple key possible? simple_key SimpleKey // The current simple key. simple_key_stack []SimpleKey // The stack of simple keys. depthCheck func(int, *DepthContext) error // Depth limit check function // Parser stuff state ParserState // The current parser state. states []ParserState // The parser states stack. marks []Mark // The stack of marks. tag_directives []TagDirective // The list of TAG directives. // Representer stuff aliases []AliasData // The alias data. } // NewParser creates a new parser object. func NewParser() Parser { return Parser{ raw_buffer: make([]byte, 0, input_raw_buffer_size), buffer: make([]byte, 0, input_buffer_size), mark: Mark{Line: 1, Column: 1}, depthCheck: DefaultDepthCheck, } } // Parse gets the next event. func (parser *Parser) Parse(event *Event) error { // Erase the event object. *event = Event{} if parser.lastError != nil { return parser.lastError } // No events after the end of the stream or error. if parser.stream_end_produced || parser.state == PARSE_END_STATE { return io.EOF } // Generate the next event. if err := parser.stateMachine(event); err != nil { parser.lastError = err return err } return nil } // Delete a parser object. func (parser *Parser) Delete() { *parser = Parser{} } // String read handler. func yamlStringReadHandler(parser *Parser, buffer []byte) (n int, err error) { if parser.input_pos == len(parser.input) { return 0, io.EOF } n = copy(buffer, parser.input[parser.input_pos:]) parser.input_pos += n return n, nil } // Reader read handler. func yamlReaderReadHandler(parser *Parser, buffer []byte) (n int, err error) { return parser.input_reader.Read(buffer) } // SetInputString sets a string input. func (parser *Parser) SetInputString(input []byte) { if parser.read_handler != nil { panic("must set the input source only once") } parser.read_handler = yamlStringReadHandler parser.input = input parser.input_pos = 0 } // SetInputReader sets a file input. func (parser *Parser) SetInputReader(r io.Reader) { if parser.read_handler != nil { panic("must set the input source only once") } parser.read_handler = yamlReaderReadHandler parser.input_reader = r } // SetEncoding sets the source encoding. func (parser *Parser) SetEncoding(encoding Encoding) { if parser.encoding != ANY_ENCODING { panic("must set the encoding only once") } parser.encoding = encoding } // GetPendingComments returns the parser's comment queue for CLI access. func (parser *Parser) GetPendingComments() []Comment { return parser.comments } // GetCommentsHead returns the current position in the comment queue. func (parser *Parser) GetCommentsHead() int { return parser.comments_head } // SetSkipComments enables or disables comment scanning. // When enabled, the scanner skips comment tokens for better performance. func (parser *Parser) SetSkipComments(skip bool) { parser.skip_comments = skip } // default_tag_directives defines the standard tag directives (! and !!) // that are implicitly available in all YAML documents. var default_tag_directives = []TagDirective{ {[]byte("!"), []byte("!")}, {[]byte("!!"), []byte("tag:yaml.org,2002:")}, } // State dispatcher. func (parser *Parser) stateMachine(event *Event) error { // trace("yaml_parser_state_machine", "state:", parser.state.String()) switch parser.state { case PARSE_STREAM_START_STATE: return parser.parseStreamStart(event) case PARSE_IMPLICIT_DOCUMENT_START_STATE: return parser.parseDocumentStart(event, true) case PARSE_DOCUMENT_START_STATE: return parser.parseDocumentStart(event, false) case PARSE_DOCUMENT_CONTENT_STATE: return parser.parseDocumentContent(event) case PARSE_DOCUMENT_END_STATE: return parser.parseDocumentEnd(event) case PARSE_BLOCK_NODE_STATE: return parser.parseNode(event, true, false) case PARSE_BLOCK_SEQUENCE_FIRST_ENTRY_STATE: return parser.parseBlockSequenceEntry(event, true) case PARSE_BLOCK_SEQUENCE_ENTRY_STATE: return parser.parseBlockSequenceEntry(event, false) case PARSE_INDENTLESS_SEQUENCE_ENTRY_STATE: return parser.parseIndentlessSequenceEntry(event) case PARSE_BLOCK_MAPPING_FIRST_KEY_STATE: return parser.parseBlockMappingKey(event, true) case PARSE_BLOCK_MAPPING_KEY_STATE: return parser.parseBlockMappingKey(event, false) case PARSE_BLOCK_MAPPING_VALUE_STATE: return parser.parseBlockMappingValue(event) case PARSE_FLOW_SEQUENCE_FIRST_ENTRY_STATE: return parser.parseFlowSequenceEntry(event, true) case PARSE_FLOW_SEQUENCE_ENTRY_STATE: return parser.parseFlowSequenceEntry(event, false) case PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_KEY_STATE: return parser.parseFlowSequenceEntryMappingKey(event) case PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_VALUE_STATE: return parser.parseFlowSequenceEntryMappingValue(event) case PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_END_STATE: return parser.parseFlowSequenceEntryMappingEnd(event) case PARSE_FLOW_MAPPING_FIRST_KEY_STATE: return parser.parseFlowMappingKey(event, true) case PARSE_FLOW_MAPPING_KEY_STATE: return parser.parseFlowMappingKey(event, false) case PARSE_FLOW_MAPPING_VALUE_STATE: return parser.parseFlowMappingValue(event, false) case PARSE_FLOW_MAPPING_EMPTY_VALUE_STATE: return parser.parseFlowMappingValue(event, true) default: panic("invalid parser state") } } // Parse the production: // // stream ::= STREAM-START implicit_document? explicit_document* STREAM-END // ************ func (parser *Parser) parseStreamStart(event *Event) error { var token *Token if err := parser.peekToken(&token); err != nil { return err } if token.Type != STREAM_START_TOKEN { return formatParserError("did not find expected ", token.StartMark) } parser.state = PARSE_IMPLICIT_DOCUMENT_START_STATE *event = Event{ Type: STREAM_START_EVENT, StartMark: token.StartMark, EndMark: token.EndMark, encoding: token.encoding, } parser.skipToken() return nil } // Parse the productions: // // implicit_document ::= block_node DOCUMENT-END* // * // explicit_document ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END* // ************************* func (parser *Parser) parseDocumentStart(event *Event, implicit bool) error { var token *Token if err := parser.peekToken(&token); err != nil { return err } // Parse extra document end indicators. for token.Type == DOCUMENT_END_TOKEN { parser.skipToken() if err := parser.peekToken(&token); err != nil { return err } } if implicit && token.Type != VERSION_DIRECTIVE_TOKEN && token.Type != TAG_DIRECTIVE_TOKEN && token.Type != DOCUMENT_START_TOKEN && token.Type != STREAM_END_TOKEN { // Parse an implicit document. if err := parser.processDirectives(nil, nil); err != nil { return err } parser.states = append(parser.states, PARSE_DOCUMENT_END_STATE) parser.state = PARSE_BLOCK_NODE_STATE var head_comment []byte if len(parser.HeadComment) > 0 { // [Go] Scan the header comment backwards, and if an // empty line is found, break the header so the part // before the last empty line goes into the document // header, while the bottom of it goes into a follow up // event. for i := len(parser.HeadComment) - 1; i > 0; i-- { if parser.HeadComment[i] == '\n' { if i == len(parser.HeadComment)-1 { head_comment = parser.HeadComment[:i] parser.HeadComment = parser.HeadComment[i+1:] break } else if parser.HeadComment[i-1] == '\n' { head_comment = parser.HeadComment[:i-1] parser.HeadComment = parser.HeadComment[i+1:] break } } } } *event = Event{ Type: DOCUMENT_START_EVENT, StartMark: token.StartMark, EndMark: token.EndMark, Implicit: true, HeadComment: head_comment, } } else if token.Type != STREAM_END_TOKEN { // Parse an explicit document. var version_directive *VersionDirective var tag_directives []TagDirective start_mark := token.StartMark if err := parser.processDirectives(&version_directive, &tag_directives); err != nil { return err } if err := parser.peekToken(&token); err != nil { return err } if token.Type != DOCUMENT_START_TOKEN { return formatParserError( "did not find expected ", token.StartMark) } parser.states = append(parser.states, PARSE_DOCUMENT_END_STATE) parser.state = PARSE_DOCUMENT_CONTENT_STATE end_mark := token.EndMark *event = Event{ Type: DOCUMENT_START_EVENT, StartMark: start_mark, EndMark: end_mark, versionDirective: version_directive, tagDirectives: tag_directives, Implicit: false, } parser.skipToken() } else { // Parse the stream end. parser.state = PARSE_END_STATE *event = Event{ Type: STREAM_END_EVENT, StartMark: token.StartMark, EndMark: token.EndMark, } parser.skipToken() } return nil } // Parse the productions: // // explicit_document ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END* // *********** func (parser *Parser) parseDocumentContent(event *Event) error { var token *Token if err := parser.peekToken(&token); err != nil { return err } if token.Type == VERSION_DIRECTIVE_TOKEN || token.Type == TAG_DIRECTIVE_TOKEN || token.Type == DOCUMENT_START_TOKEN || token.Type == DOCUMENT_END_TOKEN || token.Type == STREAM_END_TOKEN { parser.state = parser.states[len(parser.states)-1] parser.states = parser.states[:len(parser.states)-1] return parser.processEmptyScalar(event, token.StartMark) } return parser.parseNode(event, true, false) } // Parse the productions: // // implicit_document ::= block_node DOCUMENT-END* // ************* // explicit_document ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END* func (parser *Parser) parseDocumentEnd(event *Event) error { var token *Token if err := parser.peekToken(&token); err != nil { return err } start_mark := token.StartMark end_mark := token.StartMark implicit := true if token.Type == DOCUMENT_END_TOKEN { end_mark = token.EndMark parser.skipToken() implicit = false } parser.tag_directives = parser.tag_directives[:0] parser.state = PARSE_DOCUMENT_START_STATE *event = Event{ Type: DOCUMENT_END_EVENT, StartMark: start_mark, EndMark: end_mark, Implicit: implicit, } parser.setEventComments(event) if len(event.HeadComment) > 0 && len(event.FootComment) == 0 { event.FootComment = event.HeadComment event.HeadComment = nil } return nil } // Parse directives. func (parser *Parser) processDirectives(version_directive_ref **VersionDirective, tag_directives_ref *[]TagDirective) error { var version_directive *VersionDirective var tag_directives []TagDirective var token *Token if err := parser.peekToken(&token); err != nil { return err } for token.Type == VERSION_DIRECTIVE_TOKEN || token.Type == TAG_DIRECTIVE_TOKEN { switch token.Type { case VERSION_DIRECTIVE_TOKEN: if version_directive != nil { return formatParserError( "found duplicate %YAML directive", token.StartMark) } if token.major != 1 || token.minor != 1 { return formatParserError( "found incompatible YAML document", token.StartMark) } version_directive = &VersionDirective{ major: token.major, minor: token.minor, } case TAG_DIRECTIVE_TOKEN: value := TagDirective{ handle: token.Value, prefix: token.prefix, } if err := parser.appendTagDirective(value, false, token.StartMark); err != nil { return err } tag_directives = append(tag_directives, value) } parser.skipToken() if err := parser.peekToken(&token); err != nil { return err } } for i := range default_tag_directives { if err := parser.appendTagDirective(default_tag_directives[i], true, token.StartMark); err != nil { return err } } if version_directive_ref != nil { *version_directive_ref = version_directive } if tag_directives_ref != nil { *tag_directives_ref = tag_directives } return nil } // Append a tag directive to the directives stack. func (parser *Parser) appendTagDirective(value TagDirective, allow_duplicates bool, mark Mark) error { for i := range parser.tag_directives { if bytes.Equal(value.handle, parser.tag_directives[i].handle) { if allow_duplicates { return nil } return formatParserError("found duplicate %TAG directive", mark) } } // [Go] I suspect the copy is unnecessary. This was likely done // because there was no way to track ownership of the data. value_copy := TagDirective{ handle: make([]byte, len(value.handle)), prefix: make([]byte, len(value.prefix)), } copy(value_copy.handle, value.handle) copy(value_copy.prefix, value.prefix) parser.tag_directives = append(parser.tag_directives, value_copy) return nil } // Parse the productions: // // block_node_or_indentless_sequence ::= // ALIAS // ***** // | properties (block_content | indentless_block_sequence)? // ********** * // | block_content | indentless_block_sequence // * // block_node ::= ALIAS // ***** // | properties block_content? // ********** * // | block_content // * // flow_node ::= ALIAS // ***** // | properties flow_content? // ********** * // | flow_content // * // properties ::= TAG ANCHOR? | ANCHOR TAG? // ************************* // block_content ::= block_collection | flow_collection | SCALAR // ****** // flow_content ::= flow_collection | SCALAR // ****** func (parser *Parser) parseNode(event *Event, block, indentless_sequence bool) error { // defer trace("yaml_parser_parse_node", "block:", block, "indentless_sequence:", indentless_sequence)() var token *Token if err := parser.peekToken(&token); err != nil { return err } if token.Type == ALIAS_TOKEN { parser.state = parser.states[len(parser.states)-1] parser.states = parser.states[:len(parser.states)-1] *event = Event{ Type: ALIAS_EVENT, StartMark: token.StartMark, EndMark: token.EndMark, Anchor: token.Value, } parser.setEventComments(event) parser.skipToken() return nil } start_mark := token.StartMark end_mark := token.StartMark var tag_token bool var tag_handle, tag_suffix, anchor []byte var tag_mark Mark switch token.Type { case ANCHOR_TOKEN: anchor = token.Value start_mark = token.StartMark end_mark = token.EndMark parser.skipToken() if err := parser.peekToken(&token); err != nil { return err } if token.Type == TAG_TOKEN { tag_token = true tag_handle = token.Value tag_suffix = token.suffix tag_mark = token.StartMark end_mark = token.EndMark parser.skipToken() if err := parser.peekToken(&token); err != nil { return err } } case TAG_TOKEN: tag_token = true tag_handle = token.Value tag_suffix = token.suffix start_mark = token.StartMark tag_mark = token.StartMark end_mark = token.EndMark parser.skipToken() if err := parser.peekToken(&token); err != nil { return err } if token.Type == ANCHOR_TOKEN { anchor = token.Value end_mark = token.EndMark parser.skipToken() if err := parser.peekToken(&token); err != nil { return err } } } var tag []byte if tag_token { if len(tag_handle) == 0 { tag = tag_suffix } else { for i := range parser.tag_directives { if bytes.Equal(parser.tag_directives[i].handle, tag_handle) { tag = append([]byte(nil), parser.tag_directives[i].prefix...) tag = append(tag, tag_suffix...) break } } if len(tag) == 0 { return formatParserErrorContext( "while parsing a node", start_mark, "found undefined tag handle", tag_mark) } } } implicit := len(tag) == 0 if indentless_sequence && token.Type == BLOCK_ENTRY_TOKEN { end_mark = token.EndMark parser.state = PARSE_INDENTLESS_SEQUENCE_ENTRY_STATE *event = Event{ Type: SEQUENCE_START_EVENT, StartMark: start_mark, EndMark: end_mark, Anchor: anchor, Tag: tag, Implicit: implicit, Style: Style(BLOCK_SEQUENCE_STYLE), } return nil } if token.Type == SCALAR_TOKEN { var plain_implicit, quoted_implicit bool end_mark = token.EndMark if (len(tag) == 0 && token.Style == PLAIN_SCALAR_STYLE) || (len(tag) == 1 && tag[0] == '!') { plain_implicit = true } else if len(tag) == 0 { quoted_implicit = true } parser.state = parser.states[len(parser.states)-1] parser.states = parser.states[:len(parser.states)-1] *event = Event{ Type: SCALAR_EVENT, StartMark: start_mark, EndMark: end_mark, Anchor: anchor, Tag: tag, Value: token.Value, Implicit: plain_implicit, quoted_implicit: quoted_implicit, Style: Style(token.Style), } parser.setEventComments(event) parser.skipToken() return nil } if token.Type == FLOW_SEQUENCE_START_TOKEN { // [Go] Some of the events below can be merged as they differ only on style. end_mark = token.EndMark parser.state = PARSE_FLOW_SEQUENCE_FIRST_ENTRY_STATE *event = Event{ Type: SEQUENCE_START_EVENT, StartMark: start_mark, EndMark: end_mark, Anchor: anchor, Tag: tag, Implicit: implicit, Style: Style(FLOW_SEQUENCE_STYLE), } parser.setEventComments(event) return nil } if token.Type == FLOW_MAPPING_START_TOKEN { end_mark = token.EndMark parser.state = PARSE_FLOW_MAPPING_FIRST_KEY_STATE *event = Event{ Type: MAPPING_START_EVENT, StartMark: start_mark, EndMark: end_mark, Anchor: anchor, Tag: tag, Implicit: implicit, Style: Style(FLOW_MAPPING_STYLE), } parser.setEventComments(event) return nil } if block && token.Type == BLOCK_SEQUENCE_START_TOKEN { end_mark = token.EndMark parser.state = PARSE_BLOCK_SEQUENCE_FIRST_ENTRY_STATE *event = Event{ Type: SEQUENCE_START_EVENT, StartMark: start_mark, EndMark: end_mark, Anchor: anchor, Tag: tag, Implicit: implicit, Style: Style(BLOCK_SEQUENCE_STYLE), } if parser.stem_comment != nil { event.HeadComment = parser.stem_comment parser.stem_comment = nil } return nil } if block && token.Type == BLOCK_MAPPING_START_TOKEN { end_mark = token.EndMark parser.state = PARSE_BLOCK_MAPPING_FIRST_KEY_STATE *event = Event{ Type: MAPPING_START_EVENT, StartMark: start_mark, EndMark: end_mark, Anchor: anchor, Tag: tag, Implicit: implicit, Style: Style(BLOCK_MAPPING_STYLE), } if parser.stem_comment != nil { event.HeadComment = parser.stem_comment parser.stem_comment = nil } return nil } if len(anchor) > 0 || len(tag) > 0 { parser.state = parser.states[len(parser.states)-1] parser.states = parser.states[:len(parser.states)-1] *event = Event{ Type: SCALAR_EVENT, StartMark: start_mark, EndMark: end_mark, Anchor: anchor, Tag: tag, Implicit: implicit, quoted_implicit: false, Style: Style(PLAIN_SCALAR_STYLE), } return nil } context := "while parsing a flow node" if block { context = "while parsing a block node" } return formatParserErrorContext(context, start_mark, "did not find expected node content", token.StartMark) } // Parse the productions: // // block_sequence ::= BLOCK-SEQUENCE-START (BLOCK-ENTRY block_node?)* BLOCK-END // ******************** *********** * ********* func (parser *Parser) parseBlockSequenceEntry(event *Event, first bool) error { if first { var token *Token if err := parser.peekToken(&token); err != nil { return err } parser.marks = append(parser.marks, token.StartMark) parser.skipToken() } var token *Token if err := parser.peekToken(&token); err != nil { return err } if token.Type == BLOCK_ENTRY_TOKEN { mark := token.EndMark prior_head_len := len(parser.HeadComment) parser.skipToken() if err := parser.splitStemComment(prior_head_len); err != nil { return err } if err := parser.peekToken(&token); err != nil { return err } if token.Type != BLOCK_ENTRY_TOKEN && token.Type != BLOCK_END_TOKEN { parser.states = append(parser.states, PARSE_BLOCK_SEQUENCE_ENTRY_STATE) return parser.parseNode(event, true, false) } else { parser.state = PARSE_BLOCK_SEQUENCE_ENTRY_STATE return parser.processEmptyScalar(event, mark) } } if token.Type == BLOCK_END_TOKEN { parser.state = parser.states[len(parser.states)-1] parser.states = parser.states[:len(parser.states)-1] parser.marks = parser.marks[:len(parser.marks)-1] *event = Event{ Type: SEQUENCE_END_EVENT, StartMark: token.StartMark, EndMark: token.EndMark, } parser.skipToken() return nil } context_mark := parser.marks[len(parser.marks)-1] parser.marks = parser.marks[:len(parser.marks)-1] return formatParserErrorContext( "while parsing a block collection", context_mark, "did not find expected '-' indicator", token.StartMark) } // Parse the productions: // // indentless_sequence ::= (BLOCK-ENTRY block_node?)+ // *********** * func (parser *Parser) parseIndentlessSequenceEntry(event *Event) error { var token *Token if err := parser.peekToken(&token); err != nil { return err } if token.Type == BLOCK_ENTRY_TOKEN { mark := token.EndMark prior_head_len := len(parser.HeadComment) parser.skipToken() if err := parser.splitStemComment(prior_head_len); err != nil { return err } if err := parser.peekToken(&token); err != nil { return err } if token.Type != BLOCK_ENTRY_TOKEN && token.Type != KEY_TOKEN && token.Type != VALUE_TOKEN && token.Type != BLOCK_END_TOKEN { parser.states = append(parser.states, PARSE_INDENTLESS_SEQUENCE_ENTRY_STATE) return parser.parseNode(event, true, false) } parser.state = PARSE_INDENTLESS_SEQUENCE_ENTRY_STATE return parser.processEmptyScalar(event, mark) } parser.state = parser.states[len(parser.states)-1] parser.states = parser.states[:len(parser.states)-1] *event = Event{ Type: SEQUENCE_END_EVENT, StartMark: token.StartMark, EndMark: token.StartMark, // [Go] Shouldn't this be token.end_mark? } return nil } // Split stem comment from head comment. // // When a sequence or map is found under a sequence entry, the former head // comment is assigned to the underlying sequence or map as a whole, not the // individual sequence or map entry as would be expected otherwise. // To handle this case the previous head comment is moved aside as the stem // comment. func (parser *Parser) splitStemComment(stem_len int) error { if stem_len == 0 { return nil } var token *Token if err := parser.peekToken(&token); err != nil { return err } if token.Type != BLOCK_SEQUENCE_START_TOKEN && token.Type != BLOCK_MAPPING_START_TOKEN { return nil } parser.stem_comment = parser.HeadComment[:stem_len] if len(parser.HeadComment) == stem_len { parser.HeadComment = nil } else { // Copy suffix to prevent very strange bugs if someone ever appends // further bytes to the prefix in the stem_comment slice above. parser.HeadComment = append([]byte(nil), parser.HeadComment[stem_len+1:]...) } return nil } // Parse the productions: // // block_mapping ::= BLOCK-MAPPING_START // ******************* // ((KEY block_node_or_indentless_sequence?)? // *** * // (VALUE block_node_or_indentless_sequence?)?)* // // BLOCK-END // ********* func (parser *Parser) parseBlockMappingKey(event *Event, first bool) error { if first { var token *Token if err := parser.peekToken(&token); err != nil { return err } parser.marks = append(parser.marks, token.StartMark) parser.skipToken() } var token *Token if err := parser.peekToken(&token); err != nil { return err } // [Go] A tail comment was left from the prior mapping value processed. // Emit an event as it needs to be processed with that value and not // the following key. if len(parser.tail_comment) > 0 { *event = Event{ Type: TAIL_COMMENT_EVENT, StartMark: token.StartMark, EndMark: token.EndMark, FootComment: parser.tail_comment, } parser.tail_comment = nil return nil } switch token.Type { case KEY_TOKEN: mark := token.EndMark parser.skipToken() if err := parser.peekToken(&token); err != nil { return err } if token.Type != KEY_TOKEN && token.Type != VALUE_TOKEN && token.Type != BLOCK_END_TOKEN { parser.states = append(parser.states, PARSE_BLOCK_MAPPING_VALUE_STATE) return parser.parseNode(event, true, true) } else { parser.state = PARSE_BLOCK_MAPPING_VALUE_STATE return parser.processEmptyScalar(event, mark) } case BLOCK_END_TOKEN: parser.state = parser.states[len(parser.states)-1] parser.states = parser.states[:len(parser.states)-1] parser.marks = parser.marks[:len(parser.marks)-1] *event = Event{ Type: MAPPING_END_EVENT, StartMark: token.StartMark, EndMark: token.EndMark, } parser.setEventComments(event) parser.skipToken() return nil } context_mark := parser.marks[len(parser.marks)-1] parser.marks = parser.marks[:len(parser.marks)-1] return formatParserErrorContext( "while parsing a block mapping", context_mark, "did not find expected key", token.StartMark) } // Parse the productions: // // block_mapping ::= BLOCK-MAPPING_START // // ((KEY block_node_or_indentless_sequence?)? // // (VALUE block_node_or_indentless_sequence?)?)* // ***** * // BLOCK-END func (parser *Parser) parseBlockMappingValue(event *Event) error { var token *Token if err := parser.peekToken(&token); err != nil { return err } if token.Type == VALUE_TOKEN { mark := token.EndMark parser.skipToken() if err := parser.peekToken(&token); err != nil { return err } if token.Type != KEY_TOKEN && token.Type != VALUE_TOKEN && token.Type != BLOCK_END_TOKEN { parser.states = append(parser.states, PARSE_BLOCK_MAPPING_KEY_STATE) return parser.parseNode(event, true, true) } parser.state = PARSE_BLOCK_MAPPING_KEY_STATE return parser.processEmptyScalar(event, mark) } parser.state = PARSE_BLOCK_MAPPING_KEY_STATE return parser.processEmptyScalar(event, token.StartMark) } // Parse the productions: // // flow_sequence ::= FLOW-SEQUENCE-START // ******************* // (flow_sequence_entry FLOW-ENTRY)* // * ********** // flow_sequence_entry? // * // FLOW-SEQUENCE-END // ***************** // flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? // * func (parser *Parser) parseFlowSequenceEntry(event *Event, first bool) error { if first { var token *Token if err := parser.peekToken(&token); err != nil { return err } parser.marks = append(parser.marks, token.StartMark) parser.skipToken() } var token *Token if err := parser.peekToken(&token); err != nil { return err } if token.Type != FLOW_SEQUENCE_END_TOKEN { if !first { if token.Type == FLOW_ENTRY_TOKEN { parser.skipToken() if err := parser.peekToken(&token); err != nil { return err } } else { context_mark := parser.marks[len(parser.marks)-1] parser.marks = parser.marks[:len(parser.marks)-1] return formatParserErrorContext( "while parsing a flow sequence", context_mark, "did not find expected ',' or ']'", token.StartMark) } } if token.Type == KEY_TOKEN { parser.state = PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_KEY_STATE *event = Event{ Type: MAPPING_START_EVENT, StartMark: token.StartMark, EndMark: token.EndMark, Implicit: true, Style: Style(FLOW_MAPPING_STYLE), } parser.skipToken() return nil } else if token.Type != FLOW_SEQUENCE_END_TOKEN { parser.states = append(parser.states, PARSE_FLOW_SEQUENCE_ENTRY_STATE) return parser.parseNode(event, false, false) } } parser.state = parser.states[len(parser.states)-1] parser.states = parser.states[:len(parser.states)-1] parser.marks = parser.marks[:len(parser.marks)-1] *event = Event{ Type: SEQUENCE_END_EVENT, StartMark: token.StartMark, EndMark: token.EndMark, } parser.setEventComments(event) parser.skipToken() return nil } // Parse the productions: // // flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? // *** * func (parser *Parser) parseFlowSequenceEntryMappingKey(event *Event) error { var token *Token if err := parser.peekToken(&token); err != nil { return err } if token.Type != VALUE_TOKEN && token.Type != FLOW_ENTRY_TOKEN && token.Type != FLOW_SEQUENCE_END_TOKEN { parser.states = append(parser.states, PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_VALUE_STATE) return parser.parseNode(event, false, false) } mark := token.EndMark parser.skipToken() parser.state = PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_VALUE_STATE return parser.processEmptyScalar(event, mark) } // Parse the productions: // // flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? // ***** * func (parser *Parser) parseFlowSequenceEntryMappingValue(event *Event) error { var token *Token if err := parser.peekToken(&token); err != nil { return err } if token.Type == VALUE_TOKEN { parser.skipToken() var token *Token if err := parser.peekToken(&token); err != nil { return err } if token.Type != FLOW_ENTRY_TOKEN && token.Type != FLOW_SEQUENCE_END_TOKEN { parser.states = append(parser.states, PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_END_STATE) return parser.parseNode(event, false, false) } } parser.state = PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_END_STATE return parser.processEmptyScalar(event, token.StartMark) } // Parse the productions: // // flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? // * func (parser *Parser) parseFlowSequenceEntryMappingEnd(event *Event) error { var token *Token if err := parser.peekToken(&token); err != nil { return err } parser.state = PARSE_FLOW_SEQUENCE_ENTRY_STATE *event = Event{ Type: MAPPING_END_EVENT, StartMark: token.StartMark, EndMark: token.StartMark, // [Go] Shouldn't this be end_mark? } return nil } // Parse the productions: // // flow_mapping ::= FLOW-MAPPING-START // ****************** // (flow_mapping_entry FLOW-ENTRY)* // * ********** // flow_mapping_entry? // ****************** // FLOW-MAPPING-END // **************** // flow_mapping_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? // * *** * func (parser *Parser) parseFlowMappingKey(event *Event, first bool) error { if first { var token *Token if err := parser.peekToken(&token); err != nil { return err } parser.marks = append(parser.marks, token.StartMark) parser.skipToken() } var token *Token if err := parser.peekToken(&token); err != nil { return err } if token.Type != FLOW_MAPPING_END_TOKEN { if !first { if token.Type == FLOW_ENTRY_TOKEN { parser.skipToken() if err := parser.peekToken(&token); err != nil { return err } } else { context_mark := parser.marks[len(parser.marks)-1] parser.marks = parser.marks[:len(parser.marks)-1] return formatParserErrorContext( "while parsing a flow mapping", context_mark, "did not find expected ',' or '}'", token.StartMark) } } if token.Type == KEY_TOKEN { parser.skipToken() if err := parser.peekToken(&token); err != nil { return err } if token.Type != VALUE_TOKEN && token.Type != FLOW_ENTRY_TOKEN && token.Type != FLOW_MAPPING_END_TOKEN { parser.states = append(parser.states, PARSE_FLOW_MAPPING_VALUE_STATE) return parser.parseNode(event, false, false) } else { parser.state = PARSE_FLOW_MAPPING_VALUE_STATE return parser.processEmptyScalar(event, token.StartMark) } } else if token.Type != FLOW_MAPPING_END_TOKEN { parser.states = append(parser.states, PARSE_FLOW_MAPPING_EMPTY_VALUE_STATE) return parser.parseNode(event, false, false) } } parser.state = parser.states[len(parser.states)-1] parser.states = parser.states[:len(parser.states)-1] parser.marks = parser.marks[:len(parser.marks)-1] *event = Event{ Type: MAPPING_END_EVENT, StartMark: token.StartMark, EndMark: token.EndMark, } parser.setEventComments(event) parser.skipToken() return nil } // Parse the productions: // // flow_mapping_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? // * ***** * func (parser *Parser) parseFlowMappingValue(event *Event, empty bool) error { var token *Token if err := parser.peekToken(&token); err != nil { return err } if empty { parser.state = PARSE_FLOW_MAPPING_KEY_STATE return parser.processEmptyScalar(event, token.StartMark) } if token.Type == VALUE_TOKEN { parser.skipToken() if err := parser.peekToken(&token); err != nil { return err } if token.Type != FLOW_ENTRY_TOKEN && token.Type != FLOW_MAPPING_END_TOKEN { parser.states = append(parser.states, PARSE_FLOW_MAPPING_KEY_STATE) return parser.parseNode(event, false, false) } } parser.state = PARSE_FLOW_MAPPING_KEY_STATE return parser.processEmptyScalar(event, token.StartMark) } // Peek the next token in the token queue. func (parser *Parser) peekToken(out **Token) error { if !parser.token_available { if err := parser.fetchMoreTokens(); err != nil { return err } } token := &parser.tokens[parser.tokens_head] parser.UnfoldComments(token) *out = token return nil } // UnfoldComments walks through the comments queue and joins all // comments behind the position of the provided token into the respective // top-level comment slices in the parser. func (parser *Parser) UnfoldComments(token *Token) { for parser.comments_head < len(parser.comments) && token.StartMark.Index >= parser.comments[parser.comments_head].TokenMark.Index { comment := &parser.comments[parser.comments_head] if len(comment.Head) > 0 { if token.Type == BLOCK_END_TOKEN { // No heads on ends, so keep comment.Head for a follow up token. break } if len(parser.HeadComment) > 0 { parser.HeadComment = append(parser.HeadComment, '\n') } parser.HeadComment = append(parser.HeadComment, comment.Head...) } if len(comment.Foot) > 0 { if len(parser.FootComment) > 0 { parser.FootComment = append(parser.FootComment, '\n') } parser.FootComment = append(parser.FootComment, comment.Foot...) } if len(comment.Line) > 0 { if len(parser.LineComment) > 0 { parser.LineComment = append(parser.LineComment, '\n') } parser.LineComment = append(parser.LineComment, comment.Line...) } *comment = Comment{} parser.comments_head++ } } // Remove the next token from the queue (must be called after peek_token). func (parser *Parser) skipToken() { parser.token_available = false parser.tokens_parsed++ parser.stream_end_produced = parser.tokens[parser.tokens_head].Type == STREAM_END_TOKEN parser.tokens_head++ } // formatParserError creates a LoadError with the given problem message // and mark position. func formatParserError(problem string, problemMark Mark) *LoadError { return &LoadError{ Stage: ParserStage, Mark: problemMark, Message: problem, } } // formatParserErrorContext creates a LoadError with both context and // problem information, each with their own mark positions. func formatParserErrorContext(context string, contextMark Mark, problem string, problemMark Mark) *LoadError { return &LoadError{ Stage: ParserStage, ContextMark: contextMark, ContextMsg: context, Mark: problemMark, Message: problem, } } // setEventComments transfers accumulated comments from the parser to the // event and clears the parser's comment state. func (parser *Parser) setEventComments(event *Event) { event.HeadComment = parser.HeadComment event.LineComment = parser.LineComment event.FootComment = parser.FootComment parser.HeadComment = nil parser.LineComment = nil parser.FootComment = nil parser.tail_comment = nil parser.stem_comment = nil } // Generate an empty scalar event. func (parser *Parser) processEmptyScalar(event *Event, mark Mark) error { *event = Event{ Type: SCALAR_EVENT, StartMark: mark, EndMark: mark, Value: nil, // Empty Implicit: true, Style: Style(PLAIN_SCALAR_STYLE), } return nil } // ParserGetEvents parses the YAML input and returns the generated event stream. func ParserGetEvents(in []byte) (string, error) { p := NewComposer(in, nil) defer p.Destroy() var events strings.Builder var event Event for { if err := p.Parser.Parse(&event); err != nil { return "", err } formatted := formatEvent(&event) events.WriteString(formatted) if event.Type == STREAM_END_EVENT { event.Delete() break } event.Delete() events.WriteByte('\n') } return events.String(), nil } // formatEvent formats an event as a human-readable string for debugging // and testing purposes. func formatEvent(e *Event) string { var b strings.Builder switch e.Type { case STREAM_START_EVENT: b.WriteString("+STR") case STREAM_END_EVENT: b.WriteString("-STR") case DOCUMENT_START_EVENT: b.WriteString("+DOC") if !e.Implicit { b.WriteString(" ---") } case DOCUMENT_END_EVENT: b.WriteString("-DOC") if !e.Implicit { b.WriteString(" ...") } case ALIAS_EVENT: b.WriteString("=ALI *") b.Write(e.Anchor) case SCALAR_EVENT: b.WriteString("=VAL") if len(e.Anchor) > 0 { b.WriteString(" &") b.Write(e.Anchor) } if len(e.Tag) > 0 { b.WriteString(" <") b.Write(e.Tag) b.WriteString(">") } switch e.ScalarStyle() { case PLAIN_SCALAR_STYLE: b.WriteString(" :") case LITERAL_SCALAR_STYLE: b.WriteString(" |") case FOLDED_SCALAR_STYLE: b.WriteString(" >") case SINGLE_QUOTED_SCALAR_STYLE: b.WriteString(" '") case DOUBLE_QUOTED_SCALAR_STYLE: b.WriteString(` "`) } // Escape special characters for consistent event output. val := strings.NewReplacer( `\`, `\\`, "\n", `\n`, "\t", `\t`, ).Replace(string(e.Value)) b.WriteString(val) case SEQUENCE_START_EVENT: b.WriteString("+SEQ") if len(e.Anchor) > 0 { b.WriteString(" &") b.Write(e.Anchor) } if len(e.Tag) > 0 { b.WriteString(" <") b.Write(e.Tag) b.WriteString(">") } if e.SequenceStyle() == FLOW_SEQUENCE_STYLE { b.WriteString(" []") } case SEQUENCE_END_EVENT: b.WriteString("-SEQ") case MAPPING_START_EVENT: b.WriteString("+MAP") if len(e.Anchor) > 0 { b.WriteString(" &") b.Write(e.Anchor) } if len(e.Tag) > 0 { b.WriteString(" <") b.Write(e.Tag) b.WriteString(">") } if e.MappingStyle() == FLOW_MAPPING_STYLE { b.WriteString(" {}") } case MAPPING_END_EVENT: b.WriteString("-MAP") } return b.String() } golang-go.yaml-yaml-v4-4.0.0~rc5/internal/libyaml/parser_test.go000066400000000000000000000115251521353103300245540ustar00rootroot00000000000000// Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Tests for the parser stage. // Verifies token stream to event stream transformation and grammar // implementation. package libyaml import ( "bytes" "testing" "go.yaml.in/yaml/v4/internal/testutil/assert" ) func TestParser(t *testing.T) { RunTestCases(t, "parser.yaml", map[string]TestHandler{ "parse-events": runParseEventsTest, "parse-events-detailed": runParseEventsDetailedTest, "parse-error": runParseErrorTest, "api-new": runAPINewTest, "api-method": runAPIMethodTest, "api-panic": runAPIPanicTest, "api-delete": runAPIDeleteTest, }) } // runParseEventsTest tests the parseEvents function. // //nolint:thelper // because this function is the real test func runParseEventsTest(t *testing.T, tc TestCase) { types, ok := parseEvents(tc.Yaml) assert.Truef(t, ok, "parseEvents() = %v, want true", ok) // Convert Want from interface{} to []string wantSlice, ok := tc.Want.([]any) assert.Truef(t, ok, "Want should be []interface{}") var expected []EventType for _, item := range wantSlice { eventStr, ok := item.(string) assert.Truef(t, ok, "Want item should be string") expected = append(expected, ParseEventType(t, eventStr)) } assert.Equalf(t, len(expected), len(types), "parseEvents() types length = %d, want %d", len(types), len(expected)) for i, et := range expected { assert.Equalf(t, et, types[i], "parseEvents() types[%d] = %v, want %v", i, types[i], et) } } // runParseEventsDetailedTest tests the parseEventsDetailed function. // //nolint:thelper // because this function is the real test func runParseEventsDetailedTest(t *testing.T, tc TestCase) { events, ok := parseEventsDetailed(tc.Yaml) assert.Truef(t, ok, "parseEventsDetailed() = %v, want true", ok) assert.Equalf(t, len(tc.WantSpecs), len(events), "parseEventsDetailed() events length = %d, want %d", len(events), len(tc.WantSpecs)) for i, wantSpec := range tc.WantSpecs { event := events[i] wantType := ParseEventType(t, wantSpec.Type) assert.Equalf(t, wantType, event.Type, "event[%d].Type = %v, want %v", i, event.Type, wantType) // Check specific event properties if specified if wantSpec.Anchor != "" { assert.Truef(t, bytes.Equal(event.Anchor, []byte(wantSpec.Anchor)), "event[%d].Anchor = %q, want %q", i, event.Anchor, wantSpec.Anchor) } if wantSpec.Tag != "" { assert.Truef(t, bytes.Equal(event.Tag, []byte(wantSpec.Tag)), "event[%d].Tag = %q, want %q", i, event.Tag, wantSpec.Tag) } if wantSpec.Value != "" { assert.Truef(t, bytes.Equal(event.Value, []byte(wantSpec.Value)), "event[%d].Value = %q, want %q", i, event.Value, wantSpec.Value) } if wantSpec.VersionDirective != nil { assert.NotNilf(t, event.versionDirective, "event[%d].versionDirective should not be nil", i) assert.Equalf(t, wantSpec.VersionDirective.Major, int(event.versionDirective.major), "event[%d].versionDirective.major = %d, want %d", i, event.versionDirective.major, wantSpec.VersionDirective.Major) assert.Equalf(t, wantSpec.VersionDirective.Minor, int(event.versionDirective.minor), "event[%d].versionDirective.minor = %d, want %d", i, event.versionDirective.minor, wantSpec.VersionDirective.Minor) } if len(wantSpec.TagDirectives) > 0 { assert.Equalf(t, len(wantSpec.TagDirectives), len(event.tagDirectives), "event[%d].tagDirectives length = %d, want %d", i, len(event.tagDirectives), len(wantSpec.TagDirectives)) for j, wantTd := range wantSpec.TagDirectives { assert.Truef(t, bytes.Equal(event.tagDirectives[j].handle, []byte(wantTd.Handle)), "event[%d].tagDirectives[%d].handle = %q, want %q", i, j, event.tagDirectives[j].handle, wantTd.Handle) assert.Truef(t, bytes.Equal(event.tagDirectives[j].prefix, []byte(wantTd.Prefix)), "event[%d].tagDirectives[%d].prefix = %q, want %q", i, j, event.tagDirectives[j].prefix, wantTd.Prefix) } } } } // runParseErrorTest tests the parser error handling. // //nolint:thelper // because this function is the real test func runParseErrorTest(t *testing.T, tc TestCase) { parser := NewParser() parser.SetInputString([]byte(tc.Yaml)) var event Event var parserErr error for parserErr == nil && event.Type != STREAM_END_EVENT { parserErr = parser.Parse(&event) } // Convert Want from interface{} to check for error // Want can be either a boolean or a single-element sequence // Defaults to true if not specified wantError := true if tc.Want != nil { switch v := tc.Want.(type) { case bool: wantError = v case []any: if len(v) > 0 { if boolVal, ok := v[0].(bool); ok { wantError = boolVal } } } } if wantError { assert.Truef(t, parserErr != nil, "Expected parser error, but got none") } else { assert.Truef(t, parserErr == nil, "Expected no parser error, but got %v", parserErr) } } golang-go.yaml-yaml-v4-4.0.0~rc5/internal/libyaml/reader.go000066400000000000000000000327321521353103300234660ustar00rootroot00000000000000// Copyright 2006-2010 Kirill Simonov // Copyright 2011-2019 Canonical Ltd // Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 AND MIT // Input reader with encoding detection and buffering. // Handles BOM detection, UTF-8/UTF-16 conversion, and provides buffered input // for the scanner. package libyaml import ( "errors" "fmt" "io" ) // Ensure that the buffer contains at least `length` characters. // Return true on success, false on failure. // // The length is supposed to be significantly less that the buffer size. func (parser *Parser) updateBuffer(length int) error { if parser.read_handler == nil { panic("read handler must be set") } // [Go] This function was changed to guarantee the requested length // size at EOF. // The fact we need to do this is pretty awful, but the description // above implies for that to be the case, and there are tests // If the EOF flag is set and the raw buffer is empty, do nothing. // //nolint:staticcheck // there is no problem with this empty branch as it's documentation. if parser.eof && parser.raw_buffer_pos == len(parser.raw_buffer) { // [Go] ACTUALLY! Read the documentation of this function above. // This is just broken. To return true, we need to have the // given length in the buffer. Not doing that means every single // check that calls this function to make sure the buffer has a // given length is Go) panicking; or C) accessing invalid memory. // return true } // Return if the buffer contains enough characters. if parser.unread >= length { return nil } // Determine the input encoding if it is not known yet. if parser.encoding == ANY_ENCODING { if err := parser.determineEncoding(); err != nil { return err } } // Move the unread characters to the beginning of the buffer. buffer_len := len(parser.buffer) if parser.buffer_pos > 0 && parser.buffer_pos < buffer_len { copy(parser.buffer, parser.buffer[parser.buffer_pos:]) buffer_len -= parser.buffer_pos parser.buffer_pos = 0 } else if parser.buffer_pos == buffer_len { buffer_len = 0 parser.buffer_pos = 0 } // Open the whole buffer for writing, and cut it before returning. parser.buffer = parser.buffer[:cap(parser.buffer)] // Fill the buffer until it has enough characters. first := true for parser.unread < length { // Fill the raw buffer if necessary. if !first || parser.raw_buffer_pos == len(parser.raw_buffer) { if err := parser.updateRawBuffer(); err != nil { parser.buffer = parser.buffer[:buffer_len] return err } } first = false // Decode the raw buffer. inner: for parser.raw_buffer_pos != len(parser.raw_buffer) { var value rune var width int raw_unread := len(parser.raw_buffer) - parser.raw_buffer_pos // Decode the next character. switch parser.encoding { case UTF8_ENCODING: // Decode a UTF-8 character. Check RFC 3629 // (http://www.ietf.org/rfc/rfc3629.txt) for more details. // // The following table (taken from the RFC) is used for // decoding. // // Char. number range | UTF-8 octet sequence // (hexadecimal) | (binary) // --------------------+------------------------------------ // 0000 0000-0000 007F | 0xxxxxxx // 0000 0080-0000 07FF | 110xxxxx 10xxxxxx // 0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx // 0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx // // Additionally, the characters in the range 0xD800-0xDFFF // are prohibited as they are reserved for use with UTF-16 // surrogate pairs. // Determine the length of the UTF-8 sequence. octet := parser.raw_buffer[parser.raw_buffer_pos] switch { case octet&0x80 == 0x00: width = 1 case octet&0xE0 == 0xC0: width = 2 case octet&0xF0 == 0xE0: width = 3 case octet&0xF8 == 0xF0: width = 4 default: // The leading octet is invalid. return formatReaderError( fmt.Sprintf("invalid leading UTF-8 octet (value: %d)", octet), Mark{Index: parser.offset}) } // Check if the raw buffer contains an incomplete character. if width > raw_unread { if parser.eof { return formatReaderError( "incomplete UTF-8 octet sequence", Mark{Index: parser.offset}) } break inner } // Decode the leading octet. switch { case octet&0x80 == 0x00: value = rune(octet & 0x7F) case octet&0xE0 == 0xC0: value = rune(octet & 0x1F) case octet&0xF0 == 0xE0: value = rune(octet & 0x0F) case octet&0xF8 == 0xF0: value = rune(octet & 0x07) default: value = 0 } // Check and decode the trailing octets. for k := 1; k < width; k++ { octet = parser.raw_buffer[parser.raw_buffer_pos+k] // Check if the octet is valid. if (octet & 0xC0) != 0x80 { return formatReaderError( fmt.Sprintf("invalid trailing UTF-8 octet (value: %d)", octet), Mark{Index: parser.offset + k}) } // Decode the octet. value = (value << 6) + rune(octet&0x3F) } // Check the length of the sequence against the value. switch { case width == 1: case width == 2 && value >= 0x80: case width == 3 && value >= 0x800: case width == 4 && value >= 0x10000: default: return formatReaderError( "invalid length of a UTF-8 sequence", Mark{Index: parser.offset}) } // Check the range of the value. if value >= 0xD800 && value <= 0xDFFF || value > 0x10FFFF { return formatReaderError( fmt.Sprintf("invalid Unicode character (value: %d)", value), Mark{Index: parser.offset}) } case UTF16LE_ENCODING, UTF16BE_ENCODING: var low, high int if parser.encoding == UTF16LE_ENCODING { low, high = 0, 1 } else { low, high = 1, 0 } // The UTF-16 encoding is not as simple as one might // naively think. Check RFC 2781 // (http://www.ietf.org/rfc/rfc2781.txt). // // Normally, two subsequent bytes describe a Unicode // character. However a special technique (called a // surrogate pair) is used for specifying character // values larger than 0xFFFF. // // A surrogate pair consists of two pseudo-characters: // high surrogate area (0xD800-0xDBFF) // low surrogate area (0xDC00-0xDFFF) // // The following formulas are used for decoding // and encoding characters using surrogate pairs: // // U = U' + 0x10000 (0x01 00 00 <= U <= 0x10 FF FF) // U' = yyyyyyyyyyxxxxxxxxxx (0 <= U' <= 0x0F FF FF) // W1 = 110110yyyyyyyyyy // W2 = 110111xxxxxxxxxx // // where U is the character value, W1 is the high surrogate // area, W2 is the low surrogate area. // Check for incomplete UTF-16 character. if raw_unread < 2 { if parser.eof { return formatReaderError( "incomplete UTF-16 character", Mark{Index: parser.offset}) } break inner } // Get the character. value = rune(parser.raw_buffer[parser.raw_buffer_pos+low]) + (rune(parser.raw_buffer[parser.raw_buffer_pos+high]) << 8) // Check for unexpected low surrogate area. if value&0xFC00 == 0xDC00 { return formatReaderError( fmt.Sprintf("unexpected low surrogate area (value: %d)", value), Mark{Index: parser.offset}) } // Check for a high surrogate area. if value&0xFC00 == 0xD800 { width = 4 // Check for incomplete surrogate pair. if raw_unread < 4 { if parser.eof { return formatReaderError( "incomplete UTF-16 surrogate pair", Mark{Index: parser.offset}) } break inner } // Get the next character. value2 := rune(parser.raw_buffer[parser.raw_buffer_pos+low+2]) + (rune(parser.raw_buffer[parser.raw_buffer_pos+high+2]) << 8) // Check for a low surrogate area. if value2&0xFC00 != 0xDC00 { return formatReaderError( fmt.Sprintf("expected low surrogate area (value: %d)", value2), Mark{Index: parser.offset + 2}) } // Generate the value of the surrogate pair. value = 0x10000 + ((value & 0x3FF) << 10) + (value2 & 0x3FF) } else { width = 2 } default: panic("impossible") } // YAML 1.2 compatible character sets // Check if the character is in the allowed range: // For JSON compatibility in quoted scalars, we must allow all // non-C0 characters. This includes ASCII DEL (0x7F) and the // C1 control block [#x80-#x9F]. // ref: https://yaml.org/spec/1.2.2/#51-character-set switch { // 8 bit set // Tab (\t) case value == 0x09: // Line feed (LF \n) case value == 0x0A: // Carriage Return (CR \r) case value == 0x0D: // 16 bit set // Printable ASCII case value >= 0x20 && value <= 0x7E: // DEL, C1 control // incompatible with YAML versions <= 1.1 case value >= 0x7F && value <= 0x9F: // and Basic Multilingual Plane (BMP), case value >= 0xA0 && value <= 0xD7FF: // Additional Unicode Areas case value >= 0xE000 && value <= 0xFFFD: // 32 bit set case value >= 0x10000 && value <= 0x10FFFF: default: return formatReaderError( fmt.Sprintf("control characters are not allowed (value: %d)", value), Mark{Index: parser.offset}) } // Move the raw pointers. parser.raw_buffer_pos += width parser.offset += width // Finally put the character into the buffer. if value <= 0x7F { // 0000 0000-0000 007F . 0xxxxxxx parser.buffer[buffer_len+0] = byte(value) buffer_len += 1 } else if value <= 0x7FF { // 0000 0080-0000 07FF . 110xxxxx 10xxxxxx parser.buffer[buffer_len+0] = byte(0xC0 + (value >> 6)) parser.buffer[buffer_len+1] = byte(0x80 + (value & 0x3F)) buffer_len += 2 } else if value <= 0xFFFF { // 0000 0800-0000 FFFF . 1110xxxx 10xxxxxx 10xxxxxx parser.buffer[buffer_len+0] = byte(0xE0 + (value >> 12)) parser.buffer[buffer_len+1] = byte(0x80 + ((value >> 6) & 0x3F)) parser.buffer[buffer_len+2] = byte(0x80 + (value & 0x3F)) buffer_len += 3 } else { // 0001 0000-0010 FFFF . 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx parser.buffer[buffer_len+0] = byte(0xF0 + (value >> 18)) parser.buffer[buffer_len+1] = byte(0x80 + ((value >> 12) & 0x3F)) parser.buffer[buffer_len+2] = byte(0x80 + ((value >> 6) & 0x3F)) parser.buffer[buffer_len+3] = byte(0x80 + (value & 0x3F)) buffer_len += 4 } parser.unread++ } // On EOF, put NUL into the buffer and return. if parser.eof { parser.buffer[buffer_len] = 0 buffer_len++ parser.unread++ break } } // [Go] Read the documentation of this function above. To return true, // we need to have the given length in the buffer. Not doing that means // every single check that calls this function to make sure the buffer // has a given length is Go) panicking; or C) accessing invalid memory. // This happens here due to the EOF above breaking early. for buffer_len < length { parser.buffer[buffer_len] = 0 buffer_len++ } parser.buffer = parser.buffer[:buffer_len] return nil } // Byte order marks for UTF-8, UTF-16LE, and UTF-16BE encodings. const ( bom_UTF8 = "\xef\xbb\xbf" bom_UTF16LE = "\xff\xfe" bom_UTF16BE = "\xfe\xff" ) // Determine the input stream encoding by checking the BOM symbol. // If no BOM is found, the UTF-8 encoding is assumed. // Return 1 on success, 0 on failure. func (parser *Parser) determineEncoding() error { // Ensure that we had enough bytes in the raw buffer. for !parser.eof && len(parser.raw_buffer)-parser.raw_buffer_pos < 3 { if err := parser.updateRawBuffer(); err != nil { return err } } // Determine the encoding. buf := parser.raw_buffer pos := parser.raw_buffer_pos avail := len(buf) - pos if avail >= 2 && buf[pos] == bom_UTF16LE[0] && buf[pos+1] == bom_UTF16LE[1] { parser.encoding = UTF16LE_ENCODING parser.raw_buffer_pos += 2 parser.offset += 2 } else if avail >= 2 && buf[pos] == bom_UTF16BE[0] && buf[pos+1] == bom_UTF16BE[1] { parser.encoding = UTF16BE_ENCODING parser.raw_buffer_pos += 2 parser.offset += 2 } else if avail >= 3 && buf[pos] == bom_UTF8[0] && buf[pos+1] == bom_UTF8[1] && buf[pos+2] == bom_UTF8[2] { parser.encoding = UTF8_ENCODING parser.raw_buffer_pos += 3 parser.offset += 3 } else { parser.encoding = UTF8_ENCODING } return nil } // Update the raw buffer. func (parser *Parser) updateRawBuffer() error { size_read := 0 // Return if the raw buffer is full. if parser.raw_buffer_pos == 0 && len(parser.raw_buffer) == cap(parser.raw_buffer) { return nil } // Return on EOF. if parser.eof { return nil } // Move the remaining bytes in the raw buffer to the beginning. if parser.raw_buffer_pos > 0 && parser.raw_buffer_pos < len(parser.raw_buffer) { copy(parser.raw_buffer, parser.raw_buffer[parser.raw_buffer_pos:]) } parser.raw_buffer = parser.raw_buffer[:len(parser.raw_buffer)-parser.raw_buffer_pos] parser.raw_buffer_pos = 0 // Call the read handler to fill the buffer. size_read, err := parser.read_handler(parser, parser.raw_buffer[len(parser.raw_buffer):cap(parser.raw_buffer)]) parser.raw_buffer = parser.raw_buffer[:len(parser.raw_buffer)+size_read] if err == io.EOF { parser.eof = true } else if err != nil { return &LoadError{ Stage: ReaderStage, Message: fmt.Sprintf("input error: %v", err), Mark: Mark{Index: parser.offset}, err: err, } } return nil } // formatReaderError creates a LoadError for reader-stage errors. func formatReaderError(message string, mark Mark) *LoadError { return &LoadError{ Stage: ReaderStage, Message: message, Mark: mark, err: errors.New(message), } } golang-go.yaml-yaml-v4-4.0.0~rc5/internal/libyaml/reader_test.go000066400000000000000000000111021521353103300245110ustar00rootroot00000000000000// Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Tests for the input reader. // Verifies BOM detection, encoding handling, and buffered input operations. package libyaml import ( "fmt" "testing" "go.yaml.in/yaml/v4/internal/testutil/assert" ) func TestReader(t *testing.T) { RunTestCases(t, "reader.yaml", map[string]TestHandler{ "reader-set-error": runReaderSetErrorTest, "reader-determine-encoding": runReaderDetermineEncodingTest, "reader-update-raw-buffer": runReaderUpdateRawBufferTest, "reader-update-buffer": runReaderUpdateBufferTest, "reader-panic": runReaderPanicTest, }) } func runReaderSetErrorTest(t *testing.T, tc TestCase) { t.Helper() parser := NewParser() // args should be [problem, offset, value] assert.Truef(t, len(tc.Args) >= 3, "Args should have at least 3 elements, got %d", len(tc.Args)) problem, ok := tc.Args[0].(string) assert.Truef(t, ok, "Args[0] should be string, got %T", tc.Args[0]) offset, ok := tc.Args[1].(int) assert.Truef(t, ok, "Args[1] should be int, got %T", tc.Args[1]) value, ok := tc.Args[2].(int) assert.Truef(t, ok, "Args[2] should be int, got %T", tc.Args[2]) // Format message with value if provided message := problem if value != -1 { message = fmt.Sprintf("%s (value: %d)", problem, value) } err := formatReaderError(message, Mark{Index: offset}) // Check return value: Want is a bool where true means success (no error). want, ok := tc.Want.(bool) assert.Truef(t, ok, "Want should be bool, got %T", tc.Want) if want { assert.NoErrorf(t, err, "setReaderError() returned error: %v", err) } else { assert.NotNilf(t, err, "setReaderError() = nil, want error") } // Run field checks runFieldChecks(t, &parser, tc.Checks) } func runReaderDetermineEncodingTest(t *testing.T, tc TestCase) { t.Helper() parser := NewParser() parser.SetInputString(tc.Input) err := parser.determineEncoding() // Check return value (defaults to true) want := WantBool(t, tc.Want, true) if want { assert.NoErrorf(t, err, "determineEncoding() returned error: %v", err) } else { assert.NotNilf(t, err, "determineEncoding() = nil, want error") } // Run field checks runFieldChecks(t, &parser, tc.Checks) } func runReaderUpdateRawBufferTest(t *testing.T, tc TestCase) { t.Helper() parser := NewParser() parser.SetInputString(tc.Input) // Apply any setup if tc.Setup != nil { applySetup(t, &parser, tc.Setup) } err := parser.updateRawBuffer() // Check return value (defaults to true) want := WantBool(t, tc.Want, true) if want { assert.NoErrorf(t, err, "updateRawBuffer() returned error: %v", err) } else { assert.NotNilf(t, err, "updateRawBuffer() = nil, want error") } // Run field checks runFieldChecks(t, &parser, tc.Checks) } func runReaderUpdateBufferTest(t *testing.T, tc TestCase) { t.Helper() parser := NewParser() parser.SetInputString(tc.Input) // Apply any setup if tc.Setup != nil { applySetup(t, &parser, tc.Setup) } // Get the length argument assert.Truef(t, len(tc.Args) >= 1, "Args should have at least 1 element, got %d", len(tc.Args)) length, ok := tc.Args[0].(int) assert.Truef(t, ok, "Args[0] should be int, got %T", tc.Args[0]) err := parser.updateBuffer(length) // Check return value (defaults to true) want := WantBool(t, tc.Want, true) if want { assert.NoErrorf(t, err, "updateBuffer(%d) returned error: %v", length, err) } else { assert.NotNilf(t, err, "updateBuffer(%d) = nil, want error", length) } // Run field checks runFieldChecks(t, &parser, tc.Checks) } func runReaderPanicTest(t *testing.T, tc TestCase) { t.Helper() parser := NewParser() wantMsg, ok := tc.Want.(string) assert.Truef(t, ok, "Want should be string, got %T", tc.Want) assert.PanicMatchesf(t, wantMsg, func() { switch tc.Function { case "updateBuffer": assert.Truef(t, len(tc.Args) >= 1, "Args should have at least 1 element, got %d", len(tc.Args)) length, ok := tc.Args[0].(int) assert.Truef(t, ok, "Args[0] should be int, got %T", tc.Args[0]) parser.updateBuffer(length) default: t.Fatalf("unknown function: %s", tc.Function) } }, "Expected panic: %s", wantMsg) } func applySetup(t *testing.T, parser *Parser, setup any) { t.Helper() if setup == nil { return } setupMap, ok := setup.(map[string]any) if !ok { t.Fatalf("setup must be a map, got %T", setup) } for key, value := range setupMap { switch key { case "eof": boolVal, ok := value.(bool) assert.Truef(t, ok, "setup.eof should be bool, got %T", value) parser.eof = boolVal default: t.Fatalf("unknown setup key: %s", key) } } } golang-go.yaml-yaml-v4-4.0.0~rc5/internal/libyaml/representer.go000066400000000000000000000343101521353103300245540ustar00rootroot00000000000000// Copyright 2011-2019 Canonical Ltd // Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Representer stage: Converts Go values to YAML nodes. // Handles representing from Go types to the intermediate node representation. package libyaml import ( "encoding" "reflect" "regexp" "sort" "strconv" "strings" "time" "unicode" "unicode/utf8" ) // keyList is a sortable slice of reflect.Values used for sorting map keys // in a natural order (numeric, then lexicographic). type keyList []reflect.Value // Representer converts Go values to YAML node trees with configurable // formatting options. type Representer struct { flow bool Indent int lineWidth int explicitStart bool explicitEnd bool flowSimpleCollections bool quotePreference QuoteStyle } // NewRepresenter creates a new YAML representer with the given options. func NewRepresenter(opts *Options) *Representer { return &Representer{ Indent: opts.Indent, lineWidth: opts.LineWidth, explicitStart: opts.ExplicitStart, explicitEnd: opts.ExplicitEnd, flowSimpleCollections: opts.FlowSimpleCollections, quotePreference: opts.QuotePreference, } } // Represent converts a Go value to a YAML node tree. // This is the primary method for the Representer stage in the dump pipeline. func (r *Representer) Represent(tag string, in reflect.Value) *Node { var node *Node if in.IsValid() { node, _ = in.Interface().(*Node) } if node != nil && node.Kind == DocumentNode { // Already a document node, return as-is return node } else { // Wrap the represented value in a document node contentNode := r.represent(tag, in) return &Node{ Kind: DocumentNode, Content: []*Node{contentNode}, } } } // From http://yaml.org/type/float.html, except the regular expression there // is bogus. In practice parsers do not enforce the "\.[0-9_]*" suffix. var base60float = regexp.MustCompile(`^[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+(?:\.[0-9_]*)?$`) // represent is the core conversion method that handles the actual // type-specific conversion from Go values to YAML nodes. func (r *Representer) represent(tag string, in reflect.Value) *Node { tag = shortTag(tag) if !in.IsValid() || in.Kind() == reflect.Pointer && in.IsNil() { return r.nilv() } iface := in.Interface() switch value := iface.(type) { case *Node: return r.nodev(in) case Node: if !in.CanAddr() { n := reflect.New(in.Type()).Elem() n.Set(in) in = n } return r.nodev(in.Addr()) case time.Time: return r.timev(tag, in) case *time.Time: return r.timev(tag, in.Elem()) case time.Duration: return r.stringv(tag, reflect.ValueOf(value.String())) case Marshaler: v, err := value.MarshalYAML() if err != nil { failDump(RepresenterStage, err) } if v == nil { return r.nilv() } return r.represent(tag, reflect.ValueOf(v)) case encoding.TextMarshaler: text, err := value.MarshalText() if err != nil { failDump(RepresenterStage, err) } in = reflect.ValueOf(string(text)) case nil: return r.nilv() } switch in.Kind() { case reflect.Interface: return r.represent(tag, in.Elem()) case reflect.Map: return r.mapv(tag, in) case reflect.Pointer: return r.represent(tag, in.Elem()) case reflect.Struct: return r.structv(tag, in) case reflect.Slice, reflect.Array: return r.slicev(tag, in) case reflect.String: return r.stringv(tag, in) case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return r.intv(tag, in) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: return r.uintv(tag, in) case reflect.Float32, reflect.Float64: return r.floatv(tag, in) case reflect.Bool: return r.boolv(tag, in) default: failDumpf(RepresenterStage, "cannot represent type: %s", in.Type().String()) return nil // unreachable; failDumpf always panics } } // mapv converts a Go map to a YAML mapping node with sorted keys. func (r *Representer) mapv(tag string, in reflect.Value) *Node { if tag == "" { tag = mapTag } var style Style if r.flow { r.flow = false style = FlowStyle } keys := keyList(in.MapKeys()) sort.Sort(keys) content := make([]*Node, 0, len(keys)*2) for _, k := range keys { content = append(content, r.represent("", k)) content = append(content, r.represent("", in.MapIndex(k))) } return &Node{ Kind: MappingNode, Tag: tag, Content: content, Style: style, } } // structv converts a Go struct to a YAML mapping node, handling field tags, // omitempty, inline fields, and inline maps. func (r *Representer) structv(tag string, in reflect.Value) *Node { sinfo, err := getStructInfo(in.Type()) if err != nil { failDump(RepresenterStage, err) } if tag == "" { tag = mapTag } var style Style if r.flow { r.flow = false style = FlowStyle } content := make([]*Node, 0) for _, info := range sinfo.FieldsList { var value reflect.Value if info.Inline == nil { value = in.Field(info.Num) } else { value = r.fieldByIndex(in, info.Inline) if !value.IsValid() { continue } } if info.OmitEmpty && isZero(value) { continue } content = append(content, r.represent("", reflect.ValueOf(info.Key))) r.flow = info.Flow content = append(content, r.represent("", value)) } if sinfo.InlineMap >= 0 { m := in.Field(sinfo.InlineMap) if m.Len() > 0 { r.flow = false keys := keyList(m.MapKeys()) sort.Sort(keys) for _, k := range keys { if _, found := sinfo.FieldsMap[k.String()]; found { failDumpf(RepresenterStage, "cannot have key %q in inlined map: conflicts with struct field", k.String()) } content = append(content, r.represent("", k)) r.flow = false content = append(content, r.represent("", m.MapIndex(k))) } } } return &Node{ Kind: MappingNode, Tag: tag, Content: content, Style: style, } } // slicev converts a Go slice or array to a YAML sequence node. func (r *Representer) slicev(tag string, in reflect.Value) *Node { if tag == "" { tag = seqTag } var style Style if r.flow { r.flow = false style = FlowStyle } n := in.Len() content := make([]*Node, n) for i := 0; i < n; i++ { content[i] = r.represent("", in.Index(i)) } return &Node{ Kind: SequenceNode, Tag: tag, Content: content, Style: style, } } // stringv converts a Go string to a YAML scalar node, handling quoting, // binary data (base64 encoding), and special string values. func (r *Representer) stringv(tag string, in reflect.Value) *Node { var style Style s := in.String() needsQuoting := false switch { case !utf8.ValidString(s): if tag == binaryTag { failDumpf(RepresenterStage, "explicitly tagged !!binary data must be base64-encoded") } if tag != "" { failDumpf(RepresenterStage, "cannot represent invalid UTF-8 data as %s", shortTag(tag)) } // It can't be represented directly as YAML so use a binary tag // and represent it as base64. tag = binaryTag s = encodeBase64(s) case tag == "": tag = strTag // Check if this string needs quoting for compatibility // even though it would resolve as !!str needsQuoting = isBase60Float(s) || isOldBool(s) || looksLikeMerge(s) } // Set the style based on content switch { case strings.Contains(s, "\n"): if r.flow || !shouldUseLiteralStyle(s) { style = DoubleQuotedStyle } else { style = LiteralStyle } case needsQuoting: // Force quoting for YAML 1.1 compatibility values style = SingleQuotedStyle default: // Plain style by default - Desolver will add quotes if type mismatch style = 0 } return &Node{ Kind: ScalarNode, Tag: tag, Value: s, Style: style, } } // boolv converts a Go bool to a YAML scalar node. func (r *Representer) boolv(tag string, in reflect.Value) *Node { var s string if in.Bool() { s = "true" } else { s = "false" } if tag == "" { tag = boolTag } return &Node{ Kind: ScalarNode, Tag: tag, Value: s, } } // intv converts a Go signed integer to a YAML scalar node. func (r *Representer) intv(tag string, in reflect.Value) *Node { s := strconv.FormatInt(in.Int(), 10) if tag == "" { tag = intTag } return &Node{ Kind: ScalarNode, Tag: tag, Value: s, } } // uintv converts a Go unsigned integer to a YAML scalar node. func (r *Representer) uintv(tag string, in reflect.Value) *Node { s := strconv.FormatUint(in.Uint(), 10) if tag == "" { tag = intTag } return &Node{ Kind: ScalarNode, Tag: tag, Value: s, } } // timev converts a Go [time.Time] to a YAML scalar node in RFC3339Nano format. func (r *Representer) timev(tag string, in reflect.Value) *Node { t := in.Interface().(time.Time) s := t.Format(time.RFC3339Nano) if tag == "" { tag = timestampTag } return &Node{ Kind: ScalarNode, Tag: tag, Value: s, } } // floatv converts a Go float to a YAML scalar node, handling special values // like infinity and NaN. func (r *Representer) floatv(tag string, in reflect.Value) *Node { // Issue #352: When formatting, use the precision of the underlying value precision := 64 if in.Kind() == reflect.Float32 { precision = 32 } s := strconv.FormatFloat(in.Float(), 'g', -1, precision) switch s { case "+Inf": s = ".inf" case "-Inf": s = "-.inf" case "NaN": s = ".nan" } if tag == "" { tag = floatTag } return &Node{ Kind: ScalarNode, Tag: tag, Value: s, } } // nilv creates a YAML null node. func (r *Representer) nilv() *Node { return &Node{ Kind: ScalarNode, Tag: nullTag, Value: "null", } } // nodev returns a node value as-is without conversion. func (r *Representer) nodev(in reflect.Value) *Node { // Return the node as-is - no conversion needed return in.Interface().(*Node) } // Len returns the number of keys in the list. func (l keyList) Len() int { return len(l) } // Swap exchanges the positions of two keys in the list. func (l keyList) Swap(i, j int) { l[i], l[j] = l[j], l[i] } // Less implements a natural sort order for map keys: numeric values sort // numerically, strings sort with natural number ordering, and mixed types // sort by kind. func (l keyList) Less(i, j int) bool { a := l[i] b := l[j] ak := a.Kind() bk := b.Kind() for (ak == reflect.Interface || ak == reflect.Pointer) && !a.IsNil() { a = a.Elem() ak = a.Kind() } for (bk == reflect.Interface || bk == reflect.Pointer) && !b.IsNil() { b = b.Elem() bk = b.Kind() } af, aok := keyFloat(a) bf, bok := keyFloat(b) if aok && bok { if af != bf { return af < bf } if ak != bk { return ak < bk } return numLess(a, b) } if ak != reflect.String || bk != reflect.String { return ak < bk } ar, br := []rune(a.String()), []rune(b.String()) digits := false for i := 0; i < len(ar) && i < len(br); i++ { if ar[i] == br[i] { digits = unicode.IsDigit(ar[i]) continue } al := unicode.IsLetter(ar[i]) bl := unicode.IsLetter(br[i]) if al && bl { return ar[i] < br[i] } if al || bl { if digits { return al } else { return bl } } var ai, bi int var an, bn int64 if ar[i] == '0' || br[i] == '0' { for j := i - 1; j >= 0 && unicode.IsDigit(ar[j]); j-- { if ar[j] != '0' { an = 1 bn = 1 break } } } for ai = i; ai < len(ar) && unicode.IsDigit(ar[ai]); ai++ { an = an*10 + int64(ar[ai]-'0') } for bi = i; bi < len(br) && unicode.IsDigit(br[bi]); bi++ { bn = bn*10 + int64(br[bi]-'0') } if an != bn { return an < bn } if ai != bi { return ai < bi } return ar[i] < br[i] } return len(ar) < len(br) } // keyFloat returns a float value for v if it is a number/bool // and whether it is a number/bool or not. func keyFloat(v reflect.Value) (f float64, ok bool) { switch v.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return float64(v.Int()), true case reflect.Float32, reflect.Float64: return v.Float(), true case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: return float64(v.Uint()), true case reflect.Bool: if v.Bool() { return 1, true } return 0, true } return 0, false } // numLess returns whether a < b. // a and b must necessarily have the same kind. func numLess(a, b reflect.Value) bool { switch a.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return a.Int() < b.Int() case reflect.Float32, reflect.Float64: return a.Float() < b.Float() case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: return a.Uint() < b.Uint() case reflect.Bool: return !a.Bool() && b.Bool() } panic("not a number") } // fieldByIndex navigates through struct fields using the given index path, // dereferencing pointers as needed. func (r *Representer) fieldByIndex(v reflect.Value, index []int) (field reflect.Value) { for _, num := range index { for { if v.Kind() == reflect.Pointer { if v.IsNil() { return reflect.Value{} } v = v.Elem() continue } break } v = v.Field(num) } return v } // isBase60 returns whether s is in base 60 notation as defined in YAML 1.1. // // The base 60 float notation in YAML 1.1 is a terrible idea and is unsupported // in YAML 1.2 and by this package, but these should be represented quoted for // the time being for compatibility with other parsers. func isBase60Float(s string) (result bool) { // Fast path. if s == "" { return false } c := s[0] if !(c == '+' || c == '-' || c >= '0' && c <= '9') || strings.IndexByte(s, ':') < 0 { return false } // Do the full match. return base60float.MatchString(s) } // isOldBool returns whether s is bool notation as defined in YAML 1.1. // // We continue to force strings that YAML 1.1 would interpret as booleans to be // rendered as quotes strings so that the represented output valid for YAML 1.1 // parsing. func isOldBool(s string) (result bool) { switch s { case "y", "Y", "yes", "Yes", "YES", "on", "On", "ON", "n", "N", "no", "No", "NO", "off", "Off", "OFF": return true default: return false } } // looksLikeMerge returns true if the given string is the merge indicator "<<". // // When encoding a scalar with this exact value, it must be quoted to prevent it // from being interpreted as a merge indicator during decoding. func looksLikeMerge(s string) (result bool) { return s == "<<" } golang-go.yaml-yaml-v4-4.0.0~rc5/internal/libyaml/representer_test.go000066400000000000000000000050251521353103300256140ustar00rootroot00000000000000// Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Tests for the Representer stage package libyaml import ( "fmt" "reflect" "testing" ) // checkNode recursively validates a node against expected structure func checkNode(t *testing.T, node *Node, wantMap map[string]any, path string) { t.Helper() // Check kind if kindStr, ok := wantMap["kind"].(string); ok { var expectedKind Kind switch kindStr { case "Scalar": expectedKind = ScalarNode case "Sequence": expectedKind = SequenceNode case "Mapping": expectedKind = MappingNode } if node.Kind != expectedKind { t.Fatalf("%s: got kind %v; want %v", path, node.Kind, expectedKind) } } // Check tag if wantTag, ok := wantMap["tag"].(string); ok { if node.Tag != wantTag { t.Fatalf("%s: got tag %q; want %q", path, node.Tag, wantTag) } } // Check value (for scalars) if wantValue, ok := wantMap["value"].(string); ok { if node.Value != wantValue { t.Fatalf("%s: got value %q; want %q", path, node.Value, wantValue) } } // Check content (for collections) if wantContent, ok := wantMap["content"].([]any); ok { if len(node.Content) != len(wantContent) { t.Fatalf("%s: got %d children; want %d", path, len(node.Content), len(wantContent)) } for i, wantChild := range wantContent { if wantChildMap, ok := wantChild.(map[string]any); ok { childPath := fmt.Sprintf("%s[%d]", path, i) checkNode(t, node.Content[i], wantChildMap, childPath) } } } } func TestRepresenter(t *testing.T) { RunTestCases(t, "representer.yaml", map[string]TestHandler{ "represent-scalar": func(t *testing.T, tc TestCase) { t.Helper() r := NewRepresenter(DefaultOptions) doc := r.Represent("", reflect.ValueOf(tc.From)) if doc == nil || doc.Kind != DocumentNode { t.Fatal("expected DocumentNode") } if len(doc.Content) == 0 { t.Fatal("expected content in document") } node := doc.Content[0] // Check node against want spec wantMap := tc.Want.(map[string]any) checkNode(t, node, wantMap, "root") }, "represent-collection": func(t *testing.T, tc TestCase) { t.Helper() r := NewRepresenter(DefaultOptions) doc := r.Represent("", reflect.ValueOf(tc.From)) if doc == nil || doc.Kind != DocumentNode { t.Fatal("expected DocumentNode") } if len(doc.Content) == 0 { t.Fatal("expected content in document") } node := doc.Content[0] // Check node against want spec wantMap := tc.Want.(map[string]any) checkNode(t, node, wantMap, "root") }, }) } golang-go.yaml-yaml-v4-4.0.0~rc5/internal/libyaml/resolver.go000066400000000000000000000215051521353103300240610ustar00rootroot00000000000000// Copyright 2011-2019 Canonical Ltd // Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Tag resolution for YAML scalars. // Determines implicit types (int, float, bool, null, timestamp) from untagged // scalar values. package libyaml import ( "encoding/base64" "fmt" "math" "regexp" "strconv" "strings" "time" ) // resolveMapItem holds a resolved value and its YAML tag for exact string // matches in the resolution table. type resolveMapItem struct { value any tag string } // Resolver handles tag resolution for YAML nodes. type Resolver struct { opts *Options } // NewResolver creates a new Resolver with the given options. func NewResolver(opts *Options) *Resolver { return &Resolver{opts: opts} } // Resolve walks the node tree and resolves tags for untagged nodes. // This is called after composition to: // - Default quoted scalars to !!str // - Default sequences to !!seq // - Default mappings to !!map // - Resolve plain scalars to implicit types (int, float, bool, null, timestamp) func (r *Resolver) Resolve(n *Node) { if n == nil { return } switch n.Kind { case ScalarNode: if n.Tag == "" { if n.Style&(SingleQuotedStyle|DoubleQuotedStyle|LiteralStyle|FoldedStyle) != 0 { // Quoted scalars default to !!str without value resolution n.Tag = strTag } else { // Plain scalars: resolve type from value n.Tag, _ = resolve("", n.Value) } } case SequenceNode: if n.Tag == "" { n.Tag = seqTag } for _, child := range n.Content { r.Resolve(child) } case MappingNode: if n.Tag == "" { n.Tag = mapTag } for _, child := range n.Content { r.Resolve(child) } case DocumentNode: for _, child := range n.Content { r.Resolve(child) } case AliasNode: // Alias nodes point to already-resolved nodes } } // resolve determines the YAML tag and Go value for a scalar string. // It takes a tag hint and the scalar string value, and returns the resolved // tag and the corresponding Go value (int, float, bool, [time.Time], etc.). // If the tag is already specified and non-resolvable, it returns the input // unchanged. func resolve(tag string, in string) (rtag string, out any) { tag = shortTag(tag) if !resolvableTag(tag) { return tag, in } defer func() { switch tag { case "", rtag, strTag, binaryTag: return case floatTag: if rtag == intTag { switch v := out.(type) { case int64: rtag = floatTag out = float64(v) return case int: rtag = floatTag out = float64(v) return } } } Fail(formatResolverError( fmt.Sprintf("cannot construct %s `%s` as a %s", shortTag(rtag), in, shortTag(tag)), Mark{}, )) }() // Any data is accepted as a !!str or !!binary. // Otherwise, the prefix is enough of a hint about what it might be. hint := byte('N') if in != "" { hint = resolveTable[in[0]] } if hint != 0 && tag != strTag && tag != binaryTag { // Handle things we can lookup in a map. if item, ok := resolveMap[in]; ok { return item.tag, item.value } // Base 60 floats are a bad idea, were dropped in YAML 1.2, and // are purposefully unsupported here. They're still quoted on // the way out for compatibility with other parser, though. switch hint { case 'M': // We've already checked the map above. case '.': // Not in the map, so maybe a normal float. floatv, err := strconv.ParseFloat(in, 64) if err == nil { return floatTag, floatv } case 'D', 'S': // Int, float, or timestamp. // Only try values as a timestamp if the value is // unquoted or there's an explicit !!timestamp tag. if tag == "" || tag == timestampTag { t, ok := parseTimestamp(in) if ok { return timestampTag, t } } plain := strings.ReplaceAll(in, "_", "") intv, err := strconv.ParseInt(plain, 0, 64) if err == nil { if intv == int64(int(intv)) { return intTag, int(intv) } else { return intTag, intv } } uintv, err := strconv.ParseUint(plain, 0, 64) if err == nil { return intTag, uintv } if yamlStyleFloat.MatchString(plain) { floatv, err := strconv.ParseFloat(plain, 64) if err == nil { return floatTag, floatv } } default: panic("internal error: missing handler for resolver table: " + string(rune(hint)) + " (with " + in + ")") } } return strTag, in } // resolveTable provides a fast lookup table for initial character-based // classification during tag resolution. // resolveMap maps specific scalar strings to their resolved values and tags. var ( resolveTable = make([]byte, 256) resolveMap = make(map[string]resolveMapItem) ) // negativeZero represents -0.0 for YAML encoding/decoding // this is needed because Go constants cannot express -0.0 // https://staticcheck.dev/docs/checks/#SA4026 var negativeZero = math.Copysign(0.0, -1.0) // yamlStyleFloat matches floating-point numbers in YAML style (including // scientific notation and numbers starting with a dot). var yamlStyleFloat = regexp.MustCompile(`^[-+]?(?:\.[0-9]+|[0-9]+(?:\.[0-9]*)?)(?:[eE][-+]?[0-9]+)?$`) // allowedTimestampFormats lists the timestamp formats supported by the // resolver. // This is a subset of the formats allowed by the regular expression // defined at http://yaml.org/type/timestamp.html. var allowedTimestampFormats = []string{ "2006-1-2T15:4:5.999999999Z07:00", // RCF3339Nano with short date fields. "2006-1-2t15:4:5.999999999Z07:00", // RFC3339Nano with short date fields and lower-case "t". "2006-1-2 15:4:5.999999999", // space separated with no time zone "2006-1-2", // date only // Notable exception: time.Parse cannot handle: "2001-12-14 21:59:43.10 -5" // from the set of examples. } // init initializes the resolveTable with character class mappings for tag resolution. func init() { t := resolveTable t[int('+')] = 'S' // Sign t[int('-')] = 'S' for _, c := range "0123456789" { t[int(c)] = 'D' // Digit } for _, c := range "yYnNtTfFoO~<" { // < for merge key << t[int(c)] = 'M' // In map } t[int('.')] = '.' // Float (potentially in map) resolveMapList := []struct { v any tag string l []string }{ {true, boolTag, []string{"true", "True", "TRUE"}}, {false, boolTag, []string{"false", "False", "FALSE"}}, {nil, nullTag, []string{"", "~", "null", "Null", "NULL"}}, {math.NaN(), floatTag, []string{".nan", ".NaN", ".NAN"}}, {math.Inf(+1), floatTag, []string{".inf", ".Inf", ".INF"}}, {math.Inf(+1), floatTag, []string{"+.inf", "+.Inf", "+.INF"}}, {math.Inf(-1), floatTag, []string{"-.inf", "-.Inf", "-.INF"}}, {negativeZero, floatTag, []string{"-0", "-0.0"}}, {"<<", mergeTag, []string{"<<"}}, } m := resolveMap for _, item := range resolveMapList { for _, s := range item.l { m[s] = resolveMapItem{item.v, item.tag} } } } // resolvableTag checks if a tag can be automatically resolved from a scalar // value. func resolvableTag(tag string) bool { switch tag { case "", strTag, boolTag, intTag, floatTag, nullTag, timestampTag: return true } return false } // encodeBase64 encodes s as base64 that is broken up into multiple lines // as appropriate for the resulting length. func encodeBase64(s string) string { const lineLen = 70 encLen := base64.StdEncoding.EncodedLen(len(s)) lines := encLen/lineLen + 1 buf := make([]byte, encLen*2+lines) in := buf[0:encLen] out := buf[encLen:] base64.StdEncoding.Encode(in, []byte(s)) k := 0 for i := 0; i < len(in); i += lineLen { j := i + lineLen if j > len(in) { j = len(in) } k += copy(out[k:], in[i:j]) if lines > 1 { out[k] = '\n' k++ } } return string(out[:k]) } // parseTimestamp parses s as a timestamp string and // returns the timestamp and reports whether it succeeded. // Timestamp formats are defined at http://yaml.org/type/timestamp.html func parseTimestamp(s string) (time.Time, bool) { // TODO write code to check all the formats supported by // http://yaml.org/type/timestamp.html instead of using time.Parse. // Quick check: all date formats start with YYYY-. i := 0 for ; i < len(s); i++ { if c := s[i]; c < '0' || c > '9' { break } } if i != 4 || i == len(s) || s[i] != '-' { return time.Time{}, false } for _, format := range allowedTimestampFormats { if t, err := time.Parse(format, s); err == nil { return t, true } } return time.Time{}, false } // formatResolverError creates a LoadError for resolver-stage errors. func formatResolverError(message string, mark Mark) *LoadError { return &LoadError{ Stage: ResolverStage, Mark: mark, Message: message, } } // formatResolverErrorContext creates a LoadError with both context and // problem information for resolver-stage errors. func formatResolverErrorContext(context string, contextMark Mark, message string, mark Mark) *LoadError { return &LoadError{ Stage: ResolverStage, ContextMark: contextMark, ContextMsg: context, Mark: mark, Message: message, } } golang-go.yaml-yaml-v4-4.0.0~rc5/internal/libyaml/resolver_test.go000066400000000000000000000044241521353103300251210ustar00rootroot00000000000000// Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Tests for the Resolver stage package libyaml import ( "testing" ) func TestResolver(t *testing.T) { RunTestCases(t, "resolver.yaml", map[string]TestHandler{ "resolve-default": func(t *testing.T, tc TestCase) { t.Helper() // Create node from test case node := &Node{ Tag: tc.Node.Tag, Value: tc.Node.Value, } // Parse kind switch tc.Node.Kind { case "Scalar": node.Kind = ScalarNode case "Sequence": node.Kind = SequenceNode case "Mapping": node.Kind = MappingNode default: node.Kind = ScalarNode // default } // Parse style switch tc.Node.Style { case "Single": node.Style = SingleQuotedStyle case "Double": node.Style = DoubleQuotedStyle case "Literal": node.Style = LiteralStyle case "Folded": node.Style = FoldedStyle case "Flow": node.Style = FlowStyle case "Tagged": node.Style = TaggedStyle } // Resolve the node r := NewResolver(nil) r.Resolve(node) // Extract want fields wantMap := tc.Want.(map[string]any) wantTag := wantMap["tag"].(string) // Check tag if node.Tag != wantTag { t.Fatalf("got tag %q; want %q", node.Tag, wantTag) } }, "resolve-infer": func(t *testing.T, tc TestCase) { t.Helper() // Create scalar node from test case node := &Node{ Kind: ScalarNode, Tag: tc.Node.Tag, Value: tc.Node.Value, } // Resolve the node r := NewResolver(nil) r.Resolve(node) // Extract want fields wantMap := tc.Want.(map[string]any) wantTag := wantMap["tag"].(string) // Check tag if node.Tag != wantTag { t.Fatalf("got tag %q; want %q", node.Tag, wantTag) } }, "resolve-preserve": func(t *testing.T, tc TestCase) { t.Helper() // Create scalar node with pre-existing tag node := &Node{ Kind: ScalarNode, Tag: tc.Node.Tag, Value: tc.Node.Value, } // Resolve the node r := NewResolver(nil) r.Resolve(node) // Extract want fields wantMap := tc.Want.(map[string]any) wantTag := wantMap["tag"].(string) // Check tag is preserved if node.Tag != wantTag { t.Fatalf("got tag %q; want %q (tag should be preserved)", node.Tag, wantTag) } }, }) } golang-go.yaml-yaml-v4-4.0.0~rc5/internal/libyaml/scanner.go000066400000000000000000002707171521353103300236640ustar00rootroot00000000000000// Copyright 2006-2010 Kirill Simonov // Copyright 2011-2019 Canonical Ltd // Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 AND MIT // Scanner stage: Transforms input stream into token sequence. // The Scanner is the most complex stage, handling indentation, simple keys, // and block collection detection. package libyaml import ( "bytes" "fmt" "io" ) // Introduction // ************ // // The following notes assume that you are familiar with the YAML specification // (http://yaml.org/spec/1.2/spec.html). We mostly follow it, although in // some cases we are less restrictive that it requires. // // The process of transforming a YAML stream into a sequence of events is // divided on two steps: Scanning and Parsing. // // The Scanner transforms the input stream into a sequence of tokens, while the // parser transform the sequence of tokens produced by the Scanner into a // sequence of parsing events. // // The Scanner is rather clever and complicated. The Parser, on the contrary, // is a straightforward implementation of a recursive-descendant parser (or, // LL(1) parser, as it is usually called). // // Actually there are two issues of Scanning that might be called "clever", the // rest is quite straightforward. The issues are "block collection start" and // "simple keys". Both issues are explained below in details. // // Here the Scanning step is explained and implemented. We start with the list // of all the tokens produced by the Scanner together with short descriptions. // // Now, tokens: // // STREAM-START(encoding) # The stream start. // STREAM-END # The stream end. // VERSION-DIRECTIVE(major,minor) # The '%YAML' directive. // TAG-DIRECTIVE(handle,prefix) # The '%TAG' directive. // DOCUMENT-START # '---' // DOCUMENT-END # '...' // BLOCK-SEQUENCE-START # Indentation increase denoting a block // BLOCK-MAPPING-START # sequence or a block mapping. // BLOCK-END # Indentation decrease. // FLOW-SEQUENCE-START # '[' // FLOW-SEQUENCE-END # ']' // BLOCK-SEQUENCE-START # '{' // BLOCK-SEQUENCE-END # '}' // BLOCK-ENTRY # '-' // FLOW-ENTRY # ',' // KEY # '?' or nothing (simple keys). // VALUE # ':' // ALIAS(anchor) # '*anchor' // ANCHOR(anchor) # '&anchor' // TAG(handle,suffix) # '!handle!suffix' // SCALAR(value,style) # A scalar. // // The following two tokens are "virtual" tokens denoting the beginning and the // end of the stream: // // STREAM-START(encoding) // STREAM-END // // We pass the information about the input stream encoding with the // STREAM-START token. // // The next two tokens are responsible for tags: // // VERSION-DIRECTIVE(major,minor) // TAG-DIRECTIVE(handle,prefix) // // Example: // // %YAML 1.1 // %TAG ! !foo // %TAG !yaml! tag:yaml.org,2002: // --- // // The corresponding sequence of tokens: // // STREAM-START(utf-8) // VERSION-DIRECTIVE(1,1) // TAG-DIRECTIVE("!","!foo") // TAG-DIRECTIVE("!yaml","tag:yaml.org,2002:") // DOCUMENT-START // STREAM-END // // Note that the VERSION-DIRECTIVE and TAG-DIRECTIVE tokens occupy a whole // line. // // The document start and end indicators are represented by: // // DOCUMENT-START // DOCUMENT-END // // Note that if a YAML stream contains an implicit document (without '---' // and '...' indicators), no DOCUMENT-START and DOCUMENT-END tokens will be // produced. // // In the following examples, we present whole documents together with the // produced tokens. // // 1. An implicit document: // // 'a scalar' // // Tokens: // // STREAM-START(utf-8) // SCALAR("a scalar",single-quoted) // STREAM-END // // 2. An explicit document: // // --- // 'a scalar' // ... // // Tokens: // // STREAM-START(utf-8) // DOCUMENT-START // SCALAR("a scalar",single-quoted) // DOCUMENT-END // STREAM-END // // 3. Several documents in a stream: // // 'a scalar' // --- // 'another scalar' // --- // 'yet another scalar' // // Tokens: // // STREAM-START(utf-8) // SCALAR("a scalar",single-quoted) // DOCUMENT-START // SCALAR("another scalar",single-quoted) // DOCUMENT-START // SCALAR("yet another scalar",single-quoted) // STREAM-END // // We have already introduced the SCALAR token above. The following tokens are // used to describe aliases, anchors, tag, and scalars: // // ALIAS(anchor) // ANCHOR(anchor) // TAG(handle,suffix) // SCALAR(value,style) // // The following series of examples illustrate the usage of these tokens: // // 1. A recursive sequence: // // &A [ *A ] // // Tokens: // // STREAM-START(utf-8) // ANCHOR("A") // FLOW-SEQUENCE-START // ALIAS("A") // FLOW-SEQUENCE-END // STREAM-END // // 2. A tagged scalar: // // !!float "3.14" # A good approximation. // // Tokens: // // STREAM-START(utf-8) // TAG("!!","float") // SCALAR("3.14",double-quoted) // STREAM-END // // 3. Various scalar styles: // // --- # Implicit empty plain scalars do not produce tokens. // --- a plain scalar // --- 'a single-quoted scalar' // --- "a double-quoted scalar" // --- |- // a literal scalar // --- >- // a folded // scalar // // Tokens: // // STREAM-START(utf-8) // DOCUMENT-START // DOCUMENT-START // SCALAR("a plain scalar",plain) // DOCUMENT-START // SCALAR("a single-quoted scalar",single-quoted) // DOCUMENT-START // SCALAR("a double-quoted scalar",double-quoted) // DOCUMENT-START // SCALAR("a literal scalar",literal) // DOCUMENT-START // SCALAR("a folded scalar",folded) // STREAM-END // // Now it's time to review collection-related tokens. We will start with // flow collections: // // FLOW-SEQUENCE-START // FLOW-SEQUENCE-END // FLOW-MAPPING-START // FLOW-MAPPING-END // FLOW-ENTRY // KEY // VALUE // // The tokens FLOW-SEQUENCE-START, FLOW-SEQUENCE-END, FLOW-MAPPING-START, and // FLOW-MAPPING-END represent the indicators '[', ']', '{', and '}' // correspondingly. FLOW-ENTRY represent the ',' indicator. Finally the // indicators '?' and ':', which are used for denoting mapping keys and values, // are represented by the KEY and VALUE tokens. // // The following examples show flow collections: // // 1. A flow sequence: // // [item 1, item 2, item 3] // // Tokens: // // STREAM-START(utf-8) // FLOW-SEQUENCE-START // SCALAR("item 1",plain) // FLOW-ENTRY // SCALAR("item 2",plain) // FLOW-ENTRY // SCALAR("item 3",plain) // FLOW-SEQUENCE-END // STREAM-END // // 2. A flow mapping: // // { // a simple key: a value, # Note that the KEY token is produced. // ? a complex key: another value, // } // // Tokens: // // STREAM-START(utf-8) // FLOW-MAPPING-START // KEY // SCALAR("a simple key",plain) // VALUE // SCALAR("a value",plain) // FLOW-ENTRY // KEY // SCALAR("a complex key",plain) // VALUE // SCALAR("another value",plain) // FLOW-ENTRY // FLOW-MAPPING-END // STREAM-END // // A simple key is a key which is not denoted by the '?' indicator. Note that // the Scanner still produce the KEY token whenever it encounters a simple key. // // For scanning block collections, the following tokens are used (note that we // repeat KEY and VALUE here): // // BLOCK-SEQUENCE-START // BLOCK-MAPPING-START // BLOCK-END // BLOCK-ENTRY // KEY // VALUE // // The tokens BLOCK-SEQUENCE-START and BLOCK-MAPPING-START denote indentation // increase that precedes a block collection (cf. the INDENT token in Python). // The token BLOCK-END denote indentation decrease that ends a block collection // (cf. the DEDENT token in Python). However YAML has some syntax peculiarities // that makes detections of these tokens more complex. // // The tokens BLOCK-ENTRY, KEY, and VALUE are used to represent the indicators // '-', '?', and ':' correspondingly. // // The following examples show how the tokens BLOCK-SEQUENCE-START, // BLOCK-MAPPING-START, and BLOCK-END are emitted by the Scanner: // // 1. Block sequences: // // - item 1 // - item 2 // - // - item 3.1 // - item 3.2 // - // key 1: value 1 // key 2: value 2 // // Tokens: // // STREAM-START(utf-8) // BLOCK-SEQUENCE-START // BLOCK-ENTRY // SCALAR("item 1",plain) // BLOCK-ENTRY // SCALAR("item 2",plain) // BLOCK-ENTRY // BLOCK-SEQUENCE-START // BLOCK-ENTRY // SCALAR("item 3.1",plain) // BLOCK-ENTRY // SCALAR("item 3.2",plain) // BLOCK-END // BLOCK-ENTRY // BLOCK-MAPPING-START // KEY // SCALAR("key 1",plain) // VALUE // SCALAR("value 1",plain) // KEY // SCALAR("key 2",plain) // VALUE // SCALAR("value 2",plain) // BLOCK-END // BLOCK-END // STREAM-END // // 2. Block mappings: // // a simple key: a value # The KEY token is produced here. // ? a complex key // : another value // a mapping: // key 1: value 1 // key 2: value 2 // a sequence: // - item 1 // - item 2 // // Tokens: // // STREAM-START(utf-8) // BLOCK-MAPPING-START // KEY // SCALAR("a simple key",plain) // VALUE // SCALAR("a value",plain) // KEY // SCALAR("a complex key",plain) // VALUE // SCALAR("another value",plain) // KEY // SCALAR("a mapping",plain) // BLOCK-MAPPING-START // KEY // SCALAR("key 1",plain) // VALUE // SCALAR("value 1",plain) // KEY // SCALAR("key 2",plain) // VALUE // SCALAR("value 2",plain) // BLOCK-END // KEY // SCALAR("a sequence",plain) // VALUE // BLOCK-SEQUENCE-START // BLOCK-ENTRY // SCALAR("item 1",plain) // BLOCK-ENTRY // SCALAR("item 2",plain) // BLOCK-END // BLOCK-END // STREAM-END // // YAML does not always require to start a new block collection from a new // line. If the current line contains only '-', '?', and ':' indicators, a new // block collection may start at the current line. The following examples // illustrate this case: // // 1. Collections in a sequence: // // - - item 1 // - item 2 // - key 1: value 1 // key 2: value 2 // - ? complex key // : complex value // // Tokens: // // STREAM-START(utf-8) // BLOCK-SEQUENCE-START // BLOCK-ENTRY // BLOCK-SEQUENCE-START // BLOCK-ENTRY // SCALAR("item 1",plain) // BLOCK-ENTRY // SCALAR("item 2",plain) // BLOCK-END // BLOCK-ENTRY // BLOCK-MAPPING-START // KEY // SCALAR("key 1",plain) // VALUE // SCALAR("value 1",plain) // KEY // SCALAR("key 2",plain) // VALUE // SCALAR("value 2",plain) // BLOCK-END // BLOCK-ENTRY // BLOCK-MAPPING-START // KEY // SCALAR("complex key") // VALUE // SCALAR("complex value") // BLOCK-END // BLOCK-END // STREAM-END // // 2. Collections in a mapping: // // ? a sequence // : - item 1 // - item 2 // ? a mapping // : key 1: value 1 // key 2: value 2 // // Tokens: // // STREAM-START(utf-8) // BLOCK-MAPPING-START // KEY // SCALAR("a sequence",plain) // VALUE // BLOCK-SEQUENCE-START // BLOCK-ENTRY // SCALAR("item 1",plain) // BLOCK-ENTRY // SCALAR("item 2",plain) // BLOCK-END // KEY // SCALAR("a mapping",plain) // VALUE // BLOCK-MAPPING-START // KEY // SCALAR("key 1",plain) // VALUE // SCALAR("value 1",plain) // KEY // SCALAR("key 2",plain) // VALUE // SCALAR("value 2",plain) // BLOCK-END // BLOCK-END // STREAM-END // // YAML also permits non-indented sequences if they are included into a block // mapping. In this case, the token BLOCK-SEQUENCE-START is not produced: // // key: // - item 1 # BLOCK-SEQUENCE-START is NOT produced here. // - item 2 // // Tokens: // // STREAM-START(utf-8) // BLOCK-MAPPING-START // KEY // SCALAR("key",plain) // VALUE // BLOCK-ENTRY // SCALAR("item 1",plain) // BLOCK-ENTRY // SCALAR("item 2",plain) // BLOCK-END // // Buffer sizes and internal constants const ( // The size of the input raw buffer. input_raw_buffer_size = 512 // The size of the input buffer. // It should be possible to decode the whole raw buffer. input_buffer_size = input_raw_buffer_size * 3 // The size of the output buffer. output_buffer_size = 128 // The size of other stacks and queues. initial_stack_size = 16 initial_queue_size = 16 initial_string_size = 16 ) // Character classification functions // Check if the character at the specified position is an alphabetical // character, a digit, '_', or '-'. func isAlpha(b []byte, i int) bool { return b[i] >= '0' && b[i] <= '9' || b[i] >= 'A' && b[i] <= 'Z' || b[i] >= 'a' && b[i] <= 'z' || b[i] == '_' || b[i] == '-' } // Check if the character at the specified position is a flow indicator as // defined by spec production [23] c-flow-indicator ::= // c-collect-entry | c-sequence-start | c-sequence-end | // c-mapping-start | c-mapping-end func isFlowIndicator(b []byte, i int) bool { return b[i] == '[' || b[i] == ']' || b[i] == '{' || b[i] == '}' || b[i] == ',' } // Check if the character at the specified position is valid for anchor names // as defined by spec production [102] ns-anchor-char ::= ns-char - // c-flow-indicator. // This includes all printable characters except: CR, LF, BOM, space, tab, '[', // ']', '{', '}', ','. // We further limit it to ascii chars only, which is a subset of the spec // production but is usually what most people expect. func isAnchorChar(b []byte, i int) bool { if isColon(b, i) { // [Go] we exclude colons from anchor/alias names. // // A colon is a valid anchor character according to the YAML 1.2 specification, // but it can lead to ambiguity. // https://github.com/yaml/go-yaml/issues/109 // // Also, it would have been a breaking change to support it, as go.yaml.in/yaml/v3 ignores it. // Supporting it could lead to unexpected behavior. return false } return isPrintable(b, i) && !isLineBreak(b, i) && !isBlank(b, i) && !isBOM(b, i) && !isFlowIndicator(b, i) && isASCII(b, i) } // isColon checks whether the character at the specified position is a colon. func isColon(b []byte, i int) bool { return b[i] == ':' } // Check if the character at the specified position is valid in a tag URI. // // The set of valid characters is: // // '0'-'9', 'A'-'Z', 'a'-'z', '_', '-', ';', '/', '?', ':', '@', '&', // '=', '+', '$', '.', '!', '~', '*', '\'', '(', ')', '%'. // // If verbatim is true, flow indicators (',', '[', ']', '{', '}') are also // allowed. func isTagURIChar(b []byte, i int, verbatim bool) bool { c := b[i] // isAlpha covers: 0-9, A-Z, a-z, _, - if isAlpha(b, i) { return true } // Check special URI characters switch c { case ';', '/', '?', ':', '@', '&', '=', '+', '$', '.', '!', '~', '*', '\'', '(', ')', '%': return true case ',', '[', ']', '{', '}': return verbatim } return false } // Check if the character at the specified position is a digit. func isDigit(b []byte, i int) bool { return b[i] >= '0' && b[i] <= '9' } // Get the value of a digit. func asDigit(b []byte, i int) int { return int(b[i]) - '0' } // Check if the character at the specified position is a hex-digit. func isHex(b []byte, i int) bool { return b[i] >= '0' && b[i] <= '9' || b[i] >= 'A' && b[i] <= 'F' || b[i] >= 'a' && b[i] <= 'f' } // Get the value of a hex-digit. func asHex(b []byte, i int) int { bi := b[i] if bi >= 'A' && bi <= 'F' { return int(bi) - 'A' + 10 } if bi >= 'a' && bi <= 'f' { return int(bi) - 'a' + 10 } return int(bi) - '0' } // Check if the character is ASCII. func isASCII(b []byte, i int) bool { return b[i] <= 0x7F } // Check if the character at the start of the buffer can be printed unescaped. func isPrintable(b []byte, i int) bool { return ((b[i] == 0x0A) || // . == #x0A (b[i] >= 0x20 && b[i] <= 0x7E) || // #x20 <= . <= #x7E (b[i] == 0xC2 && b[i+1] >= 0xA0) || // #0xA0 <= . <= #xD7FF (b[i] > 0xC2 && b[i] < 0xED) || (b[i] == 0xED && b[i+1] < 0xA0) || (b[i] == 0xEE) || (b[i] == 0xEF && // #xE000 <= . <= #xFFFD !(b[i+1] == 0xBB && b[i+2] == 0xBF) && // && . != #xFEFF !(b[i+1] == 0xBF && (b[i+2] == 0xBE || b[i+2] == 0xBF)))) } // Check if the character at the specified position is NUL. func isZeroChar(b []byte, i int) bool { return b[i] == 0x00 } // Check if the beginning of the buffer is a BOM. func isBOM(b []byte, i int) bool { return b[0] == 0xEF && b[1] == 0xBB && b[2] == 0xBF } // Check if the character at the specified position is space. func isSpace(b []byte, i int) bool { return b[i] == ' ' } // Check if the character at the specified position is tab. func isTab(b []byte, i int) bool { return b[i] == '\t' } // Check if the character at the specified position is blank (space or tab). func isBlank(b []byte, i int) bool { // return isSpace(b, i) || isTab(b, i) return b[i] == ' ' || b[i] == '\t' } // Check if the character at the specified position is a line break. func isLineBreak(b []byte, i int) bool { return (b[i] == '\r' || // CR (#xD) b[i] == '\n' || // LF (#xA) b[i] == 0xC2 && b[i+1] == 0x85 || // NEL (#x85) b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA8 || // LS (#x2028) b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA9) // PS (#x2029) } // isCRLF checks if the position contains a CR LF sequence. func isCRLF(b []byte, i int) bool { return b[i] == '\r' && b[i+1] == '\n' } // Check if the character is a line break or NUL. func isBreakOrZero(b []byte, i int) bool { // return isLineBreak(b, i) || isZeroChar(b, i) return ( // isBreak: b[i] == '\r' || // CR (#xD) b[i] == '\n' || // LF (#xA) b[i] == 0xC2 && b[i+1] == 0x85 || // NEL (#x85) b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA8 || // LS (#x2028) b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA9 || // PS (#x2029) // isZeroChar: b[i] == 0) } // Check if the character is a line break, space, or NUL. func isSpaceOrZero(b []byte, i int) bool { // return isSpace(b, i) || isBreakOrZero(b, i) return ( // isSpace: b[i] == ' ' || // isBreakOrZero: b[i] == '\r' || // CR (#xD) b[i] == '\n' || // LF (#xA) b[i] == 0xC2 && b[i+1] == 0x85 || // NEL (#x85) b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA8 || // LS (#x2028) b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA9 || // PS (#x2029) b[i] == 0) } // Check if the character is a line break, space, tab, or NUL. func isBlankOrZero(b []byte, i int) bool { // return isBlank(b, i) || isBreakOrZero(b, i) return ( // isBlank: b[i] == ' ' || b[i] == '\t' || // isBreakOrZero: b[i] == '\r' || // CR (#xD) b[i] == '\n' || // LF (#xA) b[i] == 0xC2 && b[i+1] == 0x85 || // NEL (#x85) b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA8 || // LS (#x2028) b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA9 || // PS (#x2029) b[i] == 0) } func isEndOfScalarInFlowContentChar(b []byte, i int) bool { switch b[i] { // ",", "[", "]", "{" and "}" case ',', '[', ']', '{', '}': return true // "? " case '?': return isBlankOrZero(b, i+1) // ": ", ":,", ":]" and ":}" case ':': return b[i+1] == ' ' || b[i+1] == ',' || b[i+1] == ']' || b[i+1] == '}' default: return false } } // Determine the width of the character. func width(b byte) int { // Don't replace these by a switch without first // confirming that it is being inlined. if b&0x80 == 0x00 { return 1 } if b&0xE0 == 0xC0 { return 2 } if b&0xF0 == 0xE0 { return 3 } if b&0xF8 == 0xF0 { return 4 } return 0 } // Scan advances the buffer pointer and reads the next token. func (parser *Parser) Scan(token *Token) error { // Erase the token object. *token = Token{} // [Go] Is this necessary? if parser.lastError != nil { return parser.lastError } // No tokens after STREAM-END or error. if parser.stream_end_produced { return io.EOF } // Ensure that the tokens queue contains enough tokens. if !parser.token_available { if err := parser.fetchMoreTokens(); err != nil { parser.lastError = err return err } } // Fetch the next token from the queue. *token = parser.tokens[parser.tokens_head] parser.tokens_head++ parser.tokens_parsed++ parser.token_available = false if token.Type == STREAM_END_TOKEN { parser.stream_end_produced = true } return nil } // formatScannerError creates a ScannerError with the given problem message // and mark position. // max_number_length is the maximum length of a number suffix in a scalar tag. const max_number_length = 2 // fetchMoreTokens ensures the token queue has at least one token for lookahead. func (parser *Parser) fetchMoreTokens() error { // While we need more tokens to fetch, do it. for { // [Go] The comment parsing logic requires a lookahead of two tokens // so that foot comments may be parsed in time of associating them // with the tokens that are parsed before them, and also for line // comments to be transformed into head comments in some edge cases. if parser.tokens_head < len(parser.tokens)-2 { // If a potential simple key is at the head position, we need to fetch // the next token to disambiguate it. var first_key int found_potential_key := false if len(parser.simple_key_stack) > 0 { // Found a simple key on the stack first_key = parser.simple_key_stack[0].token_number found_potential_key = true } else if parser.simple_key_possible { // Found a 'current' simple key (which was not pushed to the stack yet) first_key = parser.simple_key.token_number found_potential_key = true } if !found_potential_key { // We don't have any potential simple keys break } else if parser.tokens_parsed != first_key { // We have not reached the potential simple key yet. break } } // Fetch the next token. if err := parser.fetchNextToken(); err != nil { return err } } parser.token_available = true return nil } // The dispatcher for token fetchers. func (parser *Parser) fetchNextToken() (err error) { // Ensure that the buffer is initialized. if parser.unread < 1 { if err := parser.updateBuffer(1); err != nil { return err } } // Check if we just started scanning. Fetch STREAM-START then. if !parser.stream_start_produced { return parser.fetchStreamStart() } scan_mark := parser.mark // Eat whitespaces and comments until we reach the next token. if err := parser.scanToNextToken(); err != nil { return err } // [Go] While unrolling indents, transform the head comments of prior // indentation levels observed after scan_start into foot comments at // the respective indexes. // Check the indentation level against the current column. if err := parser.unrollIndent(parser.mark.Column, scan_mark); err != nil { return err } // Ensure that the buffer contains at least 4 characters. 4 is the length // of the longest indicators ('--- ' and '... '). if parser.unread < 4 { if err := parser.updateBuffer(4); err != nil { return err } } // Is it the end of the stream? if isZeroChar(parser.buffer, parser.buffer_pos) { return parser.fetchStreamEnd() } // Is it a directive? if parser.mark.Column == 1 && parser.buffer[parser.buffer_pos] == '%' { return parser.fetchDirective() } buf := parser.buffer pos := parser.buffer_pos // Is it the document start indicator? if parser.mark.Column == 1 && buf[pos] == '-' && buf[pos+1] == '-' && buf[pos+2] == '-' && isBlankOrZero(buf, pos+3) { return parser.fetchDocumentIndicator(DOCUMENT_START_TOKEN) } // Is it the document end indicator? if parser.mark.Column == 1 && buf[pos] == '.' && buf[pos+1] == '.' && buf[pos+2] == '.' && isBlankOrZero(buf, pos+3) { return parser.fetchDocumentIndicator(DOCUMENT_END_TOKEN) } comment_mark := parser.mark if len(parser.tokens) > 0 && (parser.flow_level == 0 && buf[pos] == ':' || parser.flow_level > 0 && buf[pos] == ',') { // Associate any following comments with the prior token. comment_mark = parser.tokens[len(parser.tokens)-1].StartMark } defer func() { if err != nil { return } if parser.skip_comments { return } if len(parser.tokens) > 0 && parser.tokens[len(parser.tokens)-1].Type == BLOCK_ENTRY_TOKEN { // Sequence indicators alone have no line comments. It becomes // a head comment for whatever follows. return } err = parser.scanLineComment(comment_mark) }() // Is it the flow sequence start indicator? if buf[pos] == '[' { return parser.fetchFlowCollectionStart(FLOW_SEQUENCE_START_TOKEN) } // Is it the flow mapping start indicator? if parser.buffer[parser.buffer_pos] == '{' { return parser.fetchFlowCollectionStart(FLOW_MAPPING_START_TOKEN) } // Is it the flow sequence end indicator? if parser.buffer[parser.buffer_pos] == ']' { return parser.fetchFlowCollectionEnd( FLOW_SEQUENCE_END_TOKEN) } // Is it the flow mapping end indicator? if parser.buffer[parser.buffer_pos] == '}' { return parser.fetchFlowCollectionEnd( FLOW_MAPPING_END_TOKEN) } // Is it the flow entry indicator? if parser.buffer[parser.buffer_pos] == ',' { return parser.fetchFlowEntry() } // Is it the block entry indicator? if parser.buffer[parser.buffer_pos] == '-' && isBlankOrZero(parser.buffer, parser.buffer_pos+1) { return parser.fetchBlockEntry() } // Is it the key indicator? if parser.buffer[parser.buffer_pos] == '?' && isBlankOrZero(parser.buffer, parser.buffer_pos+1) { return parser.fetchKey() } // Is it the value indicator? if parser.buffer[parser.buffer_pos] == ':' && (parser.flow_level > 0 && !parser.isFlowSequence() || isBlankOrZero(parser.buffer, parser.buffer_pos+1)) { return parser.fetchValue() } // Is it an alias? if parser.buffer[parser.buffer_pos] == '*' { return parser.fetchAnchor(ALIAS_TOKEN) } // Is it an anchor? if parser.buffer[parser.buffer_pos] == '&' { return parser.fetchAnchor(ANCHOR_TOKEN) } // Is it a tag? if parser.buffer[parser.buffer_pos] == '!' { return parser.fetchTag() } // Is it a literal scalar? if parser.buffer[parser.buffer_pos] == '|' && parser.flow_level == 0 { return parser.fetchBlockScalar(true) } // Is it a folded scalar? if parser.buffer[parser.buffer_pos] == '>' && parser.flow_level == 0 { return parser.fetchBlockScalar(false) } // Is it a single-quoted scalar? if parser.buffer[parser.buffer_pos] == '\'' { return parser.fetchFlowScalar(true) } // Is it a double-quoted scalar? if parser.buffer[parser.buffer_pos] == '"' { return parser.fetchFlowScalar(false) } // Is it a plain scalar? // // A plain scalar may start with any non-blank characters except // // '-', '?', ':', ',', '[', ']', '{', '}', // '#', '&', '*', '!', '|', '>', '\'', '\"', // '%', '@', '`'. // // In the block context (and, for the '-' indicator, in the flow context // too), it may also start with the characters // // '-', '?', ':' // // if it is followed by a non-space character. // // The last rule is more restrictive than the specification requires. // [Go] TODO Make this logic more reasonable. //switch parser.buffer[parser.buffer_pos] { //case '-', '?', ':', ',', '?', '-', ',', ':', ']', '[', '}', '{', '&', '#', '!', '*', '>', '|', '"', '\'', '@', '%', '-', '`': //} if !(isBlankOrZero(parser.buffer, parser.buffer_pos) || parser.buffer[parser.buffer_pos] == '-' || parser.buffer[parser.buffer_pos] == '?' || parser.buffer[parser.buffer_pos] == ':' || parser.buffer[parser.buffer_pos] == ',' || parser.buffer[parser.buffer_pos] == '[' || parser.buffer[parser.buffer_pos] == ']' || parser.buffer[parser.buffer_pos] == '{' || parser.buffer[parser.buffer_pos] == '}' || parser.buffer[parser.buffer_pos] == '#' || parser.buffer[parser.buffer_pos] == '&' || parser.buffer[parser.buffer_pos] == '*' || parser.buffer[parser.buffer_pos] == '!' || parser.buffer[parser.buffer_pos] == '|' || parser.buffer[parser.buffer_pos] == '>' || parser.buffer[parser.buffer_pos] == '\'' || parser.buffer[parser.buffer_pos] == '"' || parser.buffer[parser.buffer_pos] == '%' || parser.buffer[parser.buffer_pos] == '@' || parser.buffer[parser.buffer_pos] == '`') || (parser.buffer[parser.buffer_pos] == '-' && !isBlank(parser.buffer, parser.buffer_pos+1)) || ((parser.buffer[parser.buffer_pos] == '?' || parser.buffer[parser.buffer_pos] == ':') && !isBlankOrZero(parser.buffer, parser.buffer_pos+1)) { return parser.fetchPlainScalar() } // If we don't determine the token type so far, it is an error. return formatScannerErrorContext( "while scanning for the next token", parser.mark, "found character that cannot start any token", parser.mark) } // isFlowSequence checks if the previous token indicates we're in a flow // sequence context. func (parser *Parser) fetchAnchor(typ TokenType) error { // An anchor or an alias could be a simple key. if err := parser.saveSimpleKey(); err != nil { return err } // A simple key cannot follow an anchor or an alias. parser.simple_key_allowed = false // Create the ALIAS or ANCHOR token and append it to the queue. var token Token if err := parser.scanAnchor(&token, typ); err != nil { return err } parser.insertToken(-1, &token) return nil } // Produce the TAG token. func (parser *Parser) fetchBlockEntry() error { // Check if the scanner is in the block context. if parser.flow_level == 0 { // Check if we are allowed to start a new entry. if !parser.simple_key_allowed { return formatScannerError("block sequence entries are not allowed in this context", parser.mark) } // Add the BLOCK-SEQUENCE-START token if needed. if err := parser.rollIndent(parser.mark.Column, -1, BLOCK_SEQUENCE_START_TOKEN, parser.mark); err != nil { return err } } else { //nolint:staticcheck // there is no problem with this empty branch as it's documentation. // It is an error for the '-' indicator to occur in the flow context, // but we let the Parser detect and report about it because the Parser // is able to point to the context. } // Reset any potential simple keys on the current flow level. if err := parser.removeSimpleKey(); err != nil { return err } // Simple keys are allowed after '-'. parser.simple_key_allowed = true // Consume the token. start_mark := parser.mark parser.skip() end_mark := parser.mark // Create the BLOCK-ENTRY token and append it to the queue. token := Token{ Type: BLOCK_ENTRY_TOKEN, StartMark: start_mark, EndMark: end_mark, } parser.insertToken(-1, &token) return nil } // Produce the KEY token. func (parser *Parser) fetchBlockScalar(literal bool) error { // Remove any potential simple keys. if err := parser.removeSimpleKey(); err != nil { return err } // A simple key may follow a block scalar. parser.simple_key_allowed = true // Create the SCALAR token and append it to the queue. var token Token if err := parser.scanBlockScalar(&token, literal); err != nil { return err } parser.insertToken(-1, &token) return nil } // Produce the SCALAR(...,single-quoted) or SCALAR(...,double-quoted) tokens. func (parser *Parser) fetchDirective() error { // Reset the indentation level. if err := parser.unrollIndent(-1, parser.mark); err != nil { return err } // Reset simple keys. if err := parser.removeSimpleKey(); err != nil { return err } parser.simple_key_allowed = false // Create the YAML-DIRECTIVE or TAG-DIRECTIVE token. token := Token{} if err := parser.scanDirective(&token); err != nil { return err } // Append the token to the queue. parser.insertToken(-1, &token) return nil } // Produce the DOCUMENT-START or DOCUMENT-END token. func (parser *Parser) fetchDocumentIndicator(typ TokenType) error { // Reset the indentation level. if err := parser.unrollIndent(-1, parser.mark); err != nil { return err } // Reset simple keys. if err := parser.removeSimpleKey(); err != nil { return err } parser.simple_key_allowed = false // Consume the token. start_mark := parser.mark parser.skip() parser.skip() parser.skip() end_mark := parser.mark // Create the DOCUMENT-START or DOCUMENT-END token. token := Token{ Type: typ, StartMark: start_mark, EndMark: end_mark, } // Append the token to the queue. parser.insertToken(-1, &token) return nil } // Produce the FLOW-SEQUENCE-START or FLOW-MAPPING-START token. func (parser *Parser) fetchFlowCollectionEnd(typ TokenType) error { // Reset any potential simple key on the current flow level. if err := parser.removeSimpleKey(); err != nil { return err } // Decrease the flow level. if err := parser.decreaseFlowLevel(); err != nil { return err } // No simple keys after the indicators ']' and '}'. parser.simple_key_allowed = false // Consume the token. start_mark := parser.mark parser.skip() end_mark := parser.mark // Create the FLOW-SEQUENCE-END of FLOW-MAPPING-END token. token := Token{ Type: typ, StartMark: start_mark, EndMark: end_mark, } // Append the token to the queue. parser.insertToken(-1, &token) return nil } // Produce the FLOW-ENTRY token. func (parser *Parser) fetchFlowCollectionStart(typ TokenType) error { // The indicators '[' and '{' may start a simple key. if err := parser.saveSimpleKey(); err != nil { return err } // Increase the flow level. if err := parser.increaseFlowLevel(); err != nil { return err } // A simple key may follow the indicators '[' and '{'. parser.simple_key_allowed = true // Consume the token. start_mark := parser.mark parser.skip() end_mark := parser.mark // Create the FLOW-SEQUENCE-START of FLOW-MAPPING-START token. token := Token{ Type: typ, StartMark: start_mark, EndMark: end_mark, } // Append the token to the queue. parser.insertToken(-1, &token) return nil } // Produce the FLOW-SEQUENCE-END or FLOW-MAPPING-END token. func (parser *Parser) fetchFlowEntry() error { // Reset any potential simple keys on the current flow level. if err := parser.removeSimpleKey(); err != nil { return err } // Simple keys are allowed after ','. parser.simple_key_allowed = true // Consume the token. start_mark := parser.mark parser.skip() end_mark := parser.mark // Create the FLOW-ENTRY token and append it to the queue. token := Token{ Type: FLOW_ENTRY_TOKEN, StartMark: start_mark, EndMark: end_mark, } parser.insertToken(-1, &token) return nil } // Produce the BLOCK-ENTRY token. func (parser *Parser) fetchFlowScalar(single bool) error { // A plain scalar could be a simple key. if err := parser.saveSimpleKey(); err != nil { return err } // A simple key cannot follow a flow scalar. parser.simple_key_allowed = false // Create the SCALAR token and append it to the queue. var token Token if err := parser.scanFlowScalar(&token, single); err != nil { return err } parser.insertToken(-1, &token) return nil } // Produce the SCALAR(...,plain) token. func (parser *Parser) fetchKey() error { // In the block context, additional checks are required. if parser.flow_level == 0 { // Check if we are allowed to start a new key (not necessary simple). if !parser.simple_key_allowed { return formatScannerError("mapping keys are not allowed in this context", parser.mark) } // Add the BLOCK-MAPPING-START token if needed. if err := parser.rollIndent(parser.mark.Column, -1, BLOCK_MAPPING_START_TOKEN, parser.mark); err != nil { return err } } // Reset any potential simple keys on the current flow level. if err := parser.removeSimpleKey(); err != nil { return err } // Simple keys are allowed after '?' in the block context. parser.simple_key_allowed = parser.flow_level == 0 // Consume the token. start_mark := parser.mark parser.skip() end_mark := parser.mark // Create the KEY token and append it to the queue. token := Token{ Type: KEY_TOKEN, StartMark: start_mark, EndMark: end_mark, } parser.insertToken(-1, &token) return nil } // Produce the VALUE token. func (parser *Parser) fetchPlainScalar() error { // A plain scalar could be a simple key. if err := parser.saveSimpleKey(); err != nil { return err } // A simple key cannot follow a flow scalar. parser.simple_key_allowed = false // Create the SCALAR token and append it to the queue. var token Token if err := parser.scanPlainScalar(&token); err != nil { return err } parser.insertToken(-1, &token) return nil } // Eat whitespaces and comments until the next token is found. func (parser *Parser) fetchStreamEnd() error { // Force new line. if parser.mark.Column != 1 { parser.mark.Column = 1 parser.mark.Line++ } // Reset the indentation level. if err := parser.unrollIndent(-1, parser.mark); err != nil { return err } // Reset simple keys. if err := parser.removeSimpleKey(); err != nil { return err } parser.simple_key = SimpleKey{} parser.simple_key_stack = []SimpleKey{} parser.simple_key_allowed = false // Create the STREAM-END token and append it to the queue. token := Token{ Type: STREAM_END_TOKEN, StartMark: parser.mark, EndMark: parser.mark, } parser.insertToken(-1, &token) return nil } // Produce a VERSION-DIRECTIVE or TAG-DIRECTIVE token. func (parser *Parser) fetchStreamStart() error { // Set the initial indentation. parser.indent = -1 // Initialize the simple key stack. parser.simple_key = SimpleKey{} parser.simple_key_stack = []SimpleKey{} // A simple key is allowed at the beginning of the stream. parser.simple_key_allowed = true // We have started. parser.stream_start_produced = true // Create the STREAM-START token and append it to the queue. token := Token{ Type: STREAM_START_TOKEN, StartMark: parser.mark, EndMark: parser.mark, encoding: parser.encoding, } parser.insertToken(-1, &token) return nil } // Produce the STREAM-END token and shut down the scanner. func (parser *Parser) fetchTag() error { // A tag could be a simple key. if err := parser.saveSimpleKey(); err != nil { return err } // A simple key cannot follow a tag. parser.simple_key_allowed = false // Create the TAG token and append it to the queue. var token Token if err := parser.scanTag(&token); err != nil { return err } parser.insertToken(-1, &token) return nil } // Produce the SCALAR(...,literal) or SCALAR(...,folded) tokens. func (parser *Parser) fetchValue() error { simple_key := &parser.simple_key // Have we found a simple key? if parser.simple_key_possible && simple_key.mark.Line == parser.mark.Line { // Create the KEY token and insert it into the queue. token := Token{ Type: KEY_TOKEN, StartMark: simple_key.mark, EndMark: simple_key.mark, } parser.insertToken(simple_key.token_number-parser.tokens_parsed, &token) // In the block context, we may need to add the BLOCK-MAPPING-START token. if err := parser.rollIndent(simple_key.mark.Column, simple_key.token_number, BLOCK_MAPPING_START_TOKEN, simple_key.mark); err != nil { return err } // Remove the simple key. parser.simple_key_possible = false simple_key.required = false // A simple key cannot follow another simple key. parser.simple_key_allowed = false } else { // The ':' indicator follows a complex key. // In the block context, extra checks are required. if parser.flow_level == 0 { // Check if we are allowed to start a complex value. if !parser.simple_key_allowed { return formatScannerError("mapping values are not allowed in this context", parser.mark) } // Add the BLOCK-MAPPING-START token if needed. if err := parser.rollIndent(parser.mark.Column, -1, BLOCK_MAPPING_START_TOKEN, parser.mark); err != nil { return err } } // Simple keys after ':' are allowed in the block context. parser.simple_key_allowed = parser.flow_level == 0 } // Consume the token. start_mark := parser.mark parser.skip() end_mark := parser.mark // Create the VALUE token and append it to the queue. token := Token{ Type: VALUE_TOKEN, StartMark: start_mark, EndMark: end_mark, } parser.insertToken(-1, &token) return nil } // Produce the ALIAS or ANCHOR token. func (parser *Parser) scanAnchor(token *Token, typ TokenType) error { var s []byte // Eat the indicator character. start_mark := parser.mark parser.skip() // Consume the value. if parser.unread < 1 { if err := parser.updateBuffer(1); err != nil { return err } } for isAnchorChar(parser.buffer, parser.buffer_pos) { s = parser.read(s) if parser.unread < 1 { if err := parser.updateBuffer(1); err != nil { return err } } } end_mark := parser.mark /* * Check if length of the anchor is greater than 0 and it is followed by * a whitespace character or one of the indicators: * * '?', ':', ',', ']', '}', '%', '@', '`'. */ if len(s) == 0 || !(isBlankOrZero(parser.buffer, parser.buffer_pos) || parser.buffer[parser.buffer_pos] == '?' || parser.buffer[parser.buffer_pos] == ':' || parser.buffer[parser.buffer_pos] == ',' || parser.buffer[parser.buffer_pos] == ']' || parser.buffer[parser.buffer_pos] == '}' || parser.buffer[parser.buffer_pos] == '%' || parser.buffer[parser.buffer_pos] == '@' || parser.buffer[parser.buffer_pos] == '`') { context := "while scanning an alias" if typ == ANCHOR_TOKEN { context = "while scanning an anchor" } return formatScannerErrorContext(context, start_mark, "did not find expected alphabetic or numeric character", parser.mark) } // Create a token. *token = Token{ Type: typ, StartMark: start_mark, EndMark: end_mark, Value: s, } return nil } // scanTag scans a TAG token. func (parser *Parser) scanBlockScalar(token *Token, literal bool) error { // Eat the indicator '|' or '>'. start_mark := parser.mark parser.skip() // Scan the additional block scalar indicators. if parser.unread < 1 { if err := parser.updateBuffer(1); err != nil { return err } } // Check for a chomping indicator. var chomping, increment int if parser.buffer[parser.buffer_pos] == '+' || parser.buffer[parser.buffer_pos] == '-' { // Set the chomping method and eat the indicator. if parser.buffer[parser.buffer_pos] == '+' { chomping = +1 } else { chomping = -1 } parser.skip() // Check for an indentation indicator. if parser.unread < 1 { if err := parser.updateBuffer(1); err != nil { return err } } if isDigit(parser.buffer, parser.buffer_pos) { // Check that the indentation is greater than 0. if parser.buffer[parser.buffer_pos] == '0' { return formatScannerErrorContext("while scanning a block scalar", start_mark, "found an indentation indicator equal to 0", parser.mark) } // Get the indentation level and eat the indicator. increment = asDigit(parser.buffer, parser.buffer_pos) parser.skip() } } else if isDigit(parser.buffer, parser.buffer_pos) { // Do the same as above, but in the opposite order. if parser.buffer[parser.buffer_pos] == '0' { return formatScannerErrorContext("while scanning a block scalar", start_mark, "found an indentation indicator equal to 0", parser.mark) } increment = asDigit(parser.buffer, parser.buffer_pos) parser.skip() if parser.unread < 1 { if err := parser.updateBuffer(1); err != nil { return err } } if parser.buffer[parser.buffer_pos] == '+' || parser.buffer[parser.buffer_pos] == '-' { if parser.buffer[parser.buffer_pos] == '+' { chomping = +1 } else { chomping = -1 } parser.skip() } } // Eat whitespaces and comments to the end of the line. if parser.unread < 1 { if err := parser.updateBuffer(1); err != nil { return err } } for isBlank(parser.buffer, parser.buffer_pos) { parser.skip() if parser.unread < 1 { if err := parser.updateBuffer(1); err != nil { return err } } } if parser.buffer[parser.buffer_pos] == '#' { if err := parser.scanLineComment(start_mark); err != nil { return err } for !isBreakOrZero(parser.buffer, parser.buffer_pos) { parser.skip() if parser.unread < 1 { if err := parser.updateBuffer(1); err != nil { return err } } } } // Check if we are at the end of the line. if !isBreakOrZero(parser.buffer, parser.buffer_pos) { return formatScannerErrorContext("while scanning a block scalar", start_mark, "did not find expected comment or line break", parser.mark) } // Eat a line break. if isLineBreak(parser.buffer, parser.buffer_pos) { if parser.unread < 2 { if err := parser.updateBuffer(2); err != nil { return err } } parser.skipLine() } end_mark := parser.mark // Set the indentation level if it was specified. var indent int if increment > 0 { if parser.indent >= 0 { indent = parser.indent + increment } else { // With 1-based columns, the line start is column 1, so an // explicit indent indicator of n means column n+1. indent = increment + 1 } } // Scan the leading line breaks and determine the indentation level if // needed. var s, leading_break, trailing_breaks []byte if err := parser.scanBlockScalarBreaks(&indent, &trailing_breaks, start_mark, &end_mark); err != nil { return err } // Scan the block scalar content. if parser.unread < 1 { if err := parser.updateBuffer(1); err != nil { return err } } var leading_blank, trailing_blank bool for parser.mark.Column == indent && !isZeroChar(parser.buffer, parser.buffer_pos) { // We are at the beginning of a non-empty line. // Is it a trailing whitespace? trailing_blank = isBlank(parser.buffer, parser.buffer_pos) // Check if we need to fold the leading line break. if !literal && !leading_blank && !trailing_blank && len(leading_break) > 0 && leading_break[0] == '\n' { // Do we need to join the lines by space? if len(trailing_breaks) == 0 { s = append(s, ' ') } } else { s = append(s, leading_break...) } leading_break = leading_break[:0] // Append the remaining line breaks. s = append(s, trailing_breaks...) trailing_breaks = trailing_breaks[:0] // Is it a leading whitespace? leading_blank = isBlank(parser.buffer, parser.buffer_pos) // Consume the current line. for !isBreakOrZero(parser.buffer, parser.buffer_pos) { s = parser.read(s) if parser.unread < 1 { if err := parser.updateBuffer(1); err != nil { return err } } } // Consume the line break. if parser.unread < 2 { if err := parser.updateBuffer(2); err != nil { return err } } leading_break = parser.readLine(leading_break) // Eat the following indentation spaces and line breaks. if err := parser.scanBlockScalarBreaks(&indent, &trailing_breaks, start_mark, &end_mark); err != nil { return err } } // Chomp the tail. if chomping != -1 { s = append(s, leading_break...) } if chomping == 1 { s = append(s, trailing_breaks...) } // Create a token. *token = Token{ Type: SCALAR_TOKEN, StartMark: start_mark, EndMark: end_mark, Value: s, Style: LITERAL_SCALAR_STYLE, } if !literal { token.Style = FOLDED_SCALAR_STYLE } return nil } // Scan indentation spaces and line breaks for a block scalar. // Determine the indentation level if needed. func (parser *Parser) scanBlockScalarBreaks(indent *int, breaks *[]byte, start_mark Mark, end_mark *Mark) error { *end_mark = parser.mark // Eat the indentation spaces and line breaks. max_indent := 0 for { // Eat the indentation spaces. if parser.unread < 1 { if err := parser.updateBuffer(1); err != nil { return err } } for (*indent == 0 || parser.mark.Column < *indent) && isSpace(parser.buffer, parser.buffer_pos) { parser.skip() if parser.unread < 1 { if err := parser.updateBuffer(1); err != nil { return err } } } if parser.mark.Column > max_indent { max_indent = parser.mark.Column } // Check for a tab character messing the indentation. if (*indent == 0 || parser.mark.Column < *indent) && isTab(parser.buffer, parser.buffer_pos) { return formatScannerErrorContext("while scanning a block scalar", start_mark, "found a tab character where an indentation space is expected", parser.mark) } // Have we found a non-empty line? if !isLineBreak(parser.buffer, parser.buffer_pos) { break } // Consume the line break. if parser.unread < 2 { if err := parser.updateBuffer(2); err != nil { return err } } // [Go] Should really be returning breaks instead. *breaks = parser.readLine(*breaks) *end_mark = parser.mark } // Determine the indentation level if needed. if *indent == 0 { *indent = max_indent if *indent < parser.indent+1 { *indent = parser.indent + 1 } if *indent < 1 { *indent = 1 } } return nil } // Scan a quoted scalar. func (parser *Parser) scanComments(scan_mark Mark) error { token := parser.tokens[len(parser.tokens)-1] if token.Type == FLOW_ENTRY_TOKEN && len(parser.tokens) > 1 { token = parser.tokens[len(parser.tokens)-2] } token_mark := token.StartMark var start_mark Mark next_indent := parser.indent if next_indent < 0 { next_indent = 0 } recent_empty := false first_empty := parser.newlines <= 1 line := parser.mark.Line column := parser.mark.Column var text []byte // The foot line is the place where a comment must start to // still be considered as a foot of the prior content. // If there's some content in the currently parsed line, then // the foot is the line below it. foot_line := -1 if scan_mark.Line > 0 { foot_line = parser.mark.Line - parser.newlines + 1 if parser.newlines == 0 && parser.mark.Column > 1 { foot_line++ } } peek := 0 for ; peek < 512; peek++ { if parser.unread < peek+1 { if parser.updateBuffer(peek+1) != nil { break } } column++ if isBlank(parser.buffer, parser.buffer_pos+peek) { continue } c := parser.buffer[parser.buffer_pos+peek] close_flow := parser.flow_level > 0 && (c == ']' || c == '}') if close_flow || isBreakOrZero(parser.buffer, parser.buffer_pos+peek) { // Got line break or terminator. if close_flow || !recent_empty { if close_flow || first_empty && (start_mark.Line == foot_line && token.Type != VALUE_TOKEN || start_mark.Column-1 < next_indent) { // This is the first empty line and there were no empty lines before, // so this initial part of the comment is a foot of the prior token // instead of being a head for the following one. Split it up. // Alternatively, this might also be the last comment inside a flow // scope, so it must be a footer. if len(text) > 0 { if start_mark.Column-1 < next_indent { // If dedented it's unrelated to the prior token. token_mark = start_mark } parser.comments = append(parser.comments, Comment{ ScanMark: scan_mark, TokenMark: token_mark, StartMark: start_mark, EndMark: Mark{parser.mark.Index + peek, line, column}, Foot: text, }) scan_mark = Mark{parser.mark.Index + peek, line, column} token_mark = scan_mark text = nil } } else { if len(text) > 0 && parser.buffer[parser.buffer_pos+peek] != 0 { text = append(text, '\n') } } } if !isLineBreak(parser.buffer, parser.buffer_pos+peek) { break } first_empty = false recent_empty = true column = 1 line++ continue } if len(text) > 0 && (close_flow || column-1 < next_indent && column != start_mark.Column) { // The comment at the different indentation is a foot of the // preceding data rather than a head of the upcoming one. parser.comments = append(parser.comments, Comment{ ScanMark: scan_mark, TokenMark: token_mark, StartMark: start_mark, EndMark: Mark{parser.mark.Index + peek, line, column}, Foot: text, }) scan_mark = Mark{parser.mark.Index + peek, line, column} token_mark = scan_mark text = nil } if parser.buffer[parser.buffer_pos+peek] != '#' { break } if len(text) == 0 { start_mark = Mark{parser.mark.Index + peek, line, column} } else { text = append(text, '\n') } recent_empty = false // Consume until after the consumed comment line. seen := parser.mark.Index + peek for { if parser.unread < 1 { if err := parser.updateBuffer(1); err != nil { return err } } if isBreakOrZero(parser.buffer, parser.buffer_pos) { if parser.mark.Index >= seen { break } if parser.unread < 2 { if err := parser.updateBuffer(2); err != nil { return err } } parser.skipLine() } else if parser.mark.Index >= seen { text = parser.read(text) } else { parser.skip() } } peek = 0 column = 1 line = parser.mark.Line next_indent = parser.indent if next_indent < 0 { next_indent = 0 } } if len(text) > 0 { parser.comments = append(parser.comments, Comment{ ScanMark: scan_mark, TokenMark: start_mark, StartMark: start_mark, EndMark: Mark{parser.mark.Index + peek - 1, line, column}, Head: text, }) } return nil } // scanDirective scans a directive (%YAML or %TAG). func (parser *Parser) scanDirective(token *Token) error { // Eat '%'. start_mark := parser.mark parser.skip() // Scan the directive name. var name []byte if err := parser.scanDirectiveName(start_mark, &name); err != nil { return err } // Is it a YAML directive? if bytes.Equal(name, []byte("YAML")) { // Scan the VERSION directive value. var major, minor int8 if err := parser.scanVersionDirectiveValue(start_mark, &major, &minor); err != nil { return err } end_mark := parser.mark // Create a VERSION-DIRECTIVE token. *token = Token{ Type: VERSION_DIRECTIVE_TOKEN, StartMark: start_mark, EndMark: end_mark, major: major, minor: minor, } // Is it a TAG directive? } else if bytes.Equal(name, []byte("TAG")) { // Scan the TAG directive value. var handle, prefix []byte if err := parser.scanTagDirectiveValue(start_mark, &handle, &prefix); err != nil { return err } end_mark := parser.mark // Create a TAG-DIRECTIVE token. *token = Token{ Type: TAG_DIRECTIVE_TOKEN, StartMark: start_mark, EndMark: end_mark, Value: handle, prefix: prefix, } // Unknown directive. } else { return formatScannerErrorContext("while scanning a directive", start_mark, "found unknown directive name", parser.mark) } // Eat the rest of the line including any comments. if parser.unread < 1 { if err := parser.updateBuffer(1); err != nil { return err } } for isBlank(parser.buffer, parser.buffer_pos) { parser.skip() if parser.unread < 1 { if err := parser.updateBuffer(1); err != nil { return err } } } if parser.buffer[parser.buffer_pos] == '#' { // [Go] Discard this inline comment for the time being. //if !parser.ScanLineComment(start_mark) { // return false //} for !isBreakOrZero(parser.buffer, parser.buffer_pos) { parser.skip() if parser.unread < 1 { if err := parser.updateBuffer(1); err != nil { return err } } } } // Check if we are at the end of the line. if !isBreakOrZero(parser.buffer, parser.buffer_pos) { return formatScannerErrorContext("while scanning a directive", start_mark, "did not find expected comment or line break", parser.mark) } // Eat a line break. if isLineBreak(parser.buffer, parser.buffer_pos) { if parser.unread < 2 { if err := parser.updateBuffer(2); err != nil { return err } } parser.skipLine() } return nil } // Scan the directive name. // // Scope: // // %YAML 1.1 # a comment \n // ^^^^ // %TAG !yaml! tag:yaml.org,2002: \n // ^^^ func (parser *Parser) scanDirectiveName(start_mark Mark, name *[]byte) error { // Consume the directive name. if parser.unread < 1 { if err := parser.updateBuffer(1); err != nil { return err } } var s []byte for isAlpha(parser.buffer, parser.buffer_pos) { s = parser.read(s) if parser.unread < 1 { if err := parser.updateBuffer(1); err != nil { return err } } } // Check if the name is empty. if len(s) == 0 { return formatScannerErrorContext("while scanning a directive", start_mark, "could not find expected directive name", parser.mark) } // Check for an blank character after the name. if !isBlankOrZero(parser.buffer, parser.buffer_pos) { return formatScannerErrorContext("while scanning a directive", start_mark, "found unexpected non-alphabetical character", parser.mark) } *name = s return nil } // Scan the value of VERSION-DIRECTIVE. // // Scope: // // %YAML 1.1 # a comment \n // ^^^^^^ func (parser *Parser) scanFlowScalar(token *Token, single bool) error { // Eat the left quote. start_mark := parser.mark parser.skip() // Consume the content of the quoted scalar. var s, leading_break, trailing_breaks, whitespaces []byte for { // Check that there are no document indicators at the beginning // of the line. if parser.unread < 4 { if err := parser.updateBuffer(4); err != nil { return err } } if parser.mark.Column == 1 && ((parser.buffer[parser.buffer_pos+0] == '-' && parser.buffer[parser.buffer_pos+1] == '-' && parser.buffer[parser.buffer_pos+2] == '-') || (parser.buffer[parser.buffer_pos+0] == '.' && parser.buffer[parser.buffer_pos+1] == '.' && parser.buffer[parser.buffer_pos+2] == '.')) && isBlankOrZero(parser.buffer, parser.buffer_pos+3) { return formatScannerErrorContext("while scanning a quoted scalar", start_mark, "found unexpected document indicator", parser.mark) } // Check for EOF. if isZeroChar(parser.buffer, parser.buffer_pos) { return formatScannerErrorContext("while scanning a quoted scalar", start_mark, "found unexpected end of stream", parser.mark) } // Consume non-blank characters. leading_blanks := false for !isBlankOrZero(parser.buffer, parser.buffer_pos) { if single && parser.buffer[parser.buffer_pos] == '\'' && parser.buffer[parser.buffer_pos+1] == '\'' { // Is is an escaped single quote. s = append(s, '\'') parser.skip() parser.skip() } else if single && parser.buffer[parser.buffer_pos] == '\'' { // It is a right single quote. break } else if !single && parser.buffer[parser.buffer_pos] == '"' { // It is a right double quote. break } else if !single && parser.buffer[parser.buffer_pos] == '\\' && isLineBreak(parser.buffer, parser.buffer_pos+1) { // It is an escaped line break. if parser.unread < 3 { if err := parser.updateBuffer(3); err != nil { return err } } parser.skip() parser.skipLine() leading_blanks = true break } else if !single && parser.buffer[parser.buffer_pos] == '\\' { // It is an escape sequence. code_length := 0 // Check the escape character. switch parser.buffer[parser.buffer_pos+1] { case '0': s = append(s, 0) case 'a': s = append(s, '\x07') case 'b': s = append(s, '\x08') case 't', '\t': s = append(s, '\x09') case 'n': s = append(s, '\x0A') case 'v': s = append(s, '\x0B') case 'f': s = append(s, '\x0C') case 'r': s = append(s, '\x0D') case 'e': s = append(s, '\x1B') case ' ': s = append(s, '\x20') case '"': s = append(s, '"') case '\'': s = append(s, '\'') case '\\': s = append(s, '\\') case 'N': // NEL (#x85) s = append(s, '\xC2') s = append(s, '\x85') case '_': // #xA0 s = append(s, '\xC2') s = append(s, '\xA0') case 'L': // LS (#x2028) s = append(s, '\xE2') s = append(s, '\x80') s = append(s, '\xA8') case 'P': // PS (#x2029) s = append(s, '\xE2') s = append(s, '\x80') s = append(s, '\xA9') case 'x': code_length = 2 case 'u': code_length = 4 case 'U': code_length = 8 default: return formatScannerErrorContext("while scanning a quoted scalar", start_mark, "found unknown escape character", parser.mark) } parser.skip() parser.skip() // Consume an arbitrary escape code. if code_length > 0 { var value int // Scan the character value. if parser.unread < code_length { if err := parser.updateBuffer(code_length); err != nil { return err } } for k := 0; k < code_length; k++ { if !isHex(parser.buffer, parser.buffer_pos+k) { return formatScannerErrorContext("while scanning a quoted scalar", start_mark, "did not find expected hexadecimal number", parser.mark) } value = (value << 4) + asHex(parser.buffer, parser.buffer_pos+k) } // Check the value and write the character. if (value >= 0xD800 && value <= 0xDFFF) || value > 0x10FFFF { return formatScannerErrorContext("while scanning a quoted scalar", start_mark, "found invalid Unicode character escape code", parser.mark) } if value <= 0x7F { s = append(s, byte(value)) } else if value <= 0x7FF { s = append(s, byte(0xC0+(value>>6))) s = append(s, byte(0x80+(value&0x3F))) } else if value <= 0xFFFF { s = append(s, byte(0xE0+(value>>12))) s = append(s, byte(0x80+((value>>6)&0x3F))) s = append(s, byte(0x80+(value&0x3F))) } else { s = append(s, byte(0xF0+(value>>18))) s = append(s, byte(0x80+((value>>12)&0x3F))) s = append(s, byte(0x80+((value>>6)&0x3F))) s = append(s, byte(0x80+(value&0x3F))) } // Advance the pointer. for k := 0; k < code_length; k++ { parser.skip() } } } else { // It is a non-escaped non-blank character. s = parser.read(s) } if parser.unread < 2 { if err := parser.updateBuffer(2); err != nil { return err } } } if parser.unread < 1 { if err := parser.updateBuffer(1); err != nil { return err } } // Check if we are at the end of the scalar. if single { if parser.buffer[parser.buffer_pos] == '\'' { break } } else { if parser.buffer[parser.buffer_pos] == '"' { break } } // Consume blank characters. for isBlank(parser.buffer, parser.buffer_pos) || isLineBreak(parser.buffer, parser.buffer_pos) { if isBlank(parser.buffer, parser.buffer_pos) { // Consume a space or a tab character. if !leading_blanks { whitespaces = parser.read(whitespaces) } else { parser.skip() } } else { if parser.unread < 2 { if err := parser.updateBuffer(2); err != nil { return err } } // Check if it is a first line break. if !leading_blanks { whitespaces = whitespaces[:0] leading_break = parser.readLine(leading_break) leading_blanks = true } else { trailing_breaks = parser.readLine(trailing_breaks) } } if parser.unread < 1 { if err := parser.updateBuffer(1); err != nil { return err } } } // Join the whitespaces or fold line breaks. if leading_blanks { // Do we need to fold line breaks? if len(leading_break) > 0 && leading_break[0] == '\n' { if len(trailing_breaks) == 0 { s = append(s, ' ') } else { s = append(s, trailing_breaks...) } } else { s = append(s, leading_break...) s = append(s, trailing_breaks...) } trailing_breaks = trailing_breaks[:0] leading_break = leading_break[:0] } else { s = append(s, whitespaces...) whitespaces = whitespaces[:0] } } // Eat the right quote. parser.skip() end_mark := parser.mark // Create a token. *token = Token{ Type: SCALAR_TOKEN, StartMark: start_mark, EndMark: end_mark, Value: s, Style: SINGLE_QUOTED_SCALAR_STYLE, } if !single { token.Style = DOUBLE_QUOTED_SCALAR_STYLE } return nil } // Scan a plain scalar. func (parser *Parser) scanLineComment(token_mark Mark) error { if parser.newlines > 0 { return nil } var start_mark Mark var text []byte for peek := 0; peek < 512; peek++ { if parser.unread < peek+1 { if parser.updateBuffer(peek+1) != nil { break } } if isBlank(parser.buffer, parser.buffer_pos+peek) { continue } if parser.buffer[parser.buffer_pos+peek] == '#' { seen := parser.mark.Index + peek for { if parser.unread < 1 { if err := parser.updateBuffer(1); err != nil { return err } } if isBreakOrZero(parser.buffer, parser.buffer_pos) { if parser.mark.Index >= seen { break } if parser.unread < 2 { if err := parser.updateBuffer(2); err != nil { return err } } parser.skipLine() } else if parser.mark.Index >= seen { if len(text) == 0 { start_mark = parser.mark } text = parser.read(text) } else { parser.skip() } } } break } if len(text) > 0 { parser.comments = append(parser.comments, Comment{ ScanMark: token_mark, TokenMark: token_mark, StartMark: start_mark, EndMark: parser.mark, Line: text, }) } return nil } // scanComments scans and associates comments with tokens, handling head, line, // and foot comments based on their position relative to tokens. func (parser *Parser) scanPlainScalar(token *Token) error { var s, leading_break, trailing_breaks, whitespaces []byte var leading_blanks bool indent := parser.indent + 1 start_mark := parser.mark end_mark := parser.mark // Consume the content of the plain scalar. for { // Check for a document indicator. if parser.unread < 4 { if err := parser.updateBuffer(4); err != nil { return err } } if parser.mark.Column == 1 && ((parser.buffer[parser.buffer_pos+0] == '-' && parser.buffer[parser.buffer_pos+1] == '-' && parser.buffer[parser.buffer_pos+2] == '-') || (parser.buffer[parser.buffer_pos+0] == '.' && parser.buffer[parser.buffer_pos+1] == '.' && parser.buffer[parser.buffer_pos+2] == '.')) && isBlankOrZero(parser.buffer, parser.buffer_pos+3) { break } // Check for a comment. if parser.buffer[parser.buffer_pos] == '#' { break } // Consume non-blank characters. for !isBlankOrZero(parser.buffer, parser.buffer_pos) { // Check for indicators that may end a plain scalar. // Check for ": " or isEndOfScalarInFlowContentChar in flow context. if (parser.buffer[parser.buffer_pos] == ':' && isBlankOrZero(parser.buffer, parser.buffer_pos+1)) || (parser.flow_level > 0 && isEndOfScalarInFlowContentChar(parser.buffer, parser.buffer_pos)) { break } // Check if we need to join whitespaces and breaks. if leading_blanks || len(whitespaces) > 0 { if leading_blanks { // Do we need to fold line breaks? if leading_break[0] == '\n' { if len(trailing_breaks) == 0 { s = append(s, ' ') } else { s = append(s, trailing_breaks...) } } else { s = append(s, leading_break...) s = append(s, trailing_breaks...) } trailing_breaks = trailing_breaks[:0] leading_break = leading_break[:0] leading_blanks = false } else { s = append(s, whitespaces...) whitespaces = whitespaces[:0] } } // Copy the character. s = parser.read(s) end_mark = parser.mark if parser.unread < 2 { if err := parser.updateBuffer(2); err != nil { return err } } } // Is it the end? if !(isBlank(parser.buffer, parser.buffer_pos) || isLineBreak(parser.buffer, parser.buffer_pos)) { break } // Consume blank characters. if parser.unread < 1 { if err := parser.updateBuffer(1); err != nil { return err } } for isBlank(parser.buffer, parser.buffer_pos) || isLineBreak(parser.buffer, parser.buffer_pos) { if isBlank(parser.buffer, parser.buffer_pos) { // Check for tab characters that abuse indentation. if leading_blanks && parser.mark.Column < indent && isTab(parser.buffer, parser.buffer_pos) { return formatScannerErrorContext("while scanning a plain scalar", start_mark, "found a tab character that violates indentation", parser.mark) } // Consume a space or a tab character. if !leading_blanks { whitespaces = parser.read(whitespaces) } else { parser.skip() } } else { if parser.unread < 2 { if err := parser.updateBuffer(2); err != nil { return err } } // Check if it is a first line break. if !leading_blanks { whitespaces = whitespaces[:0] leading_break = parser.readLine(leading_break) leading_blanks = true } else { trailing_breaks = parser.readLine(trailing_breaks) } } if parser.unread < 1 { if err := parser.updateBuffer(1); err != nil { return err } } } // Check indentation level. if parser.flow_level == 0 && parser.mark.Column < indent { break } } // Create a token. *token = Token{ Type: SCALAR_TOKEN, StartMark: start_mark, EndMark: end_mark, Value: s, Style: PLAIN_SCALAR_STYLE, } // Note that we change the 'simple_key_allowed' flag. if leading_blanks { parser.simple_key_allowed = true } return nil } // scanLineComment scans a comment on the same line as a token. func (parser *Parser) scanTag(token *Token) error { var handle, suffix []byte start_mark := parser.mark // Check if the tag is in the canonical form. if parser.unread < 2 { if err := parser.updateBuffer(2); err != nil { return err } } if parser.buffer[parser.buffer_pos+1] == '<' { // Keep the handle as '' // Eat '!<' parser.skip() parser.skip() // Consume the tag value (verbatim tag - flow indicators allowed). if err := parser.scanTagURI(false, true, nil, start_mark, &suffix); err != nil { return err } // Check for '>' and eat it. if parser.buffer[parser.buffer_pos] != '>' { return formatScannerErrorContext("while scanning a tag", start_mark, "did not find the expected '>'", parser.mark) } parser.skip() } else { // The tag has either the '!suffix' or the '!handle!suffix' form. // First, try to scan a handle. if err := parser.scanTagHandle(false, start_mark, &handle); err != nil { return err } // Check if it is, indeed, handle. if handle[0] == '!' && len(handle) > 1 && handle[len(handle)-1] == '!' { // Scan the suffix now (short form - flow indicators // not allowed). if err := parser.scanTagURI(false, false, nil, start_mark, &suffix); err != nil { return err } } else { // It wasn't a handle after all. Scan the rest of the // tag (short form). if err := parser.scanTagURI(false, false, handle, start_mark, &suffix); err != nil { return err } // Set the handle to '!'. handle = []byte{'!'} // A special case: the '!' tag. Set the handle to '' // and the suffix to '!'. if len(suffix) == 0 { handle, suffix = suffix, handle } } } // Check the character which ends the tag. if parser.unread < 1 { if err := parser.updateBuffer(1); err != nil { return err } } if !isBlankOrZero(parser.buffer, parser.buffer_pos) { return formatScannerErrorContext("while scanning a tag", start_mark, "did not find expected whitespace or line break", parser.mark) } end_mark := parser.mark // Create a token. *token = Token{ Type: TAG_TOKEN, StartMark: start_mark, EndMark: end_mark, Value: handle, suffix: suffix, } return nil } // Scan a tag handle. func (parser *Parser) scanTagDirectiveValue(start_mark Mark, handle, prefix *[]byte) error { var handle_value, prefix_value []byte // Eat whitespaces. if parser.unread < 1 { if err := parser.updateBuffer(1); err != nil { return err } } for isBlank(parser.buffer, parser.buffer_pos) { parser.skip() if parser.unread < 1 { if err := parser.updateBuffer(1); err != nil { return err } } } // Scan a handle. if err := parser.scanTagHandle(true, start_mark, &handle_value); err != nil { return err } // Expect a whitespace. if parser.unread < 1 { if err := parser.updateBuffer(1); err != nil { return err } } if !isBlank(parser.buffer, parser.buffer_pos) { return formatScannerErrorContext("while scanning a %TAG directive", start_mark, "did not find expected whitespace", parser.mark) } // Eat whitespaces. for isBlank(parser.buffer, parser.buffer_pos) { parser.skip() if parser.unread < 1 { if err := parser.updateBuffer(1); err != nil { return err } } } // Scan a prefix (TAG directive URI - flow indicators allowed). if err := parser.scanTagURI(true, true, nil, start_mark, &prefix_value); err != nil { return err } // Expect a whitespace or line break. if parser.unread < 1 { if err := parser.updateBuffer(1); err != nil { return err } } if !isBlankOrZero(parser.buffer, parser.buffer_pos) { return formatScannerErrorContext("while scanning a %TAG directive", start_mark, "did not find expected whitespace or line break", parser.mark) } *handle = handle_value *prefix = prefix_value return nil } // scanAnchor scans an ANCHOR or ALIAS token. func (parser *Parser) scanTagHandle(directive bool, start_mark Mark, handle *[]byte) error { // Check the initial '!' character. if parser.unread < 1 { if err := parser.updateBuffer(1); err != nil { return err } } if parser.buffer[parser.buffer_pos] != '!' { return parser.setScannerTagError(directive, start_mark, "did not find expected '!'") } var s []byte // Copy the '!' character. s = parser.read(s) // Copy all subsequent alphabetical and numerical characters. if parser.unread < 1 { if err := parser.updateBuffer(1); err != nil { return err } } for isAlpha(parser.buffer, parser.buffer_pos) { s = parser.read(s) if parser.unread < 1 { if err := parser.updateBuffer(1); err != nil { return err } } } // Check if the trailing character is '!' and copy it. if parser.buffer[parser.buffer_pos] == '!' { s = parser.read(s) } else { // It's either the '!' tag or not really a tag handle. // If it's a %TAG directive, it's an error. // If it's a tag token, it must be a part of URI. if directive && string(s) != "!" { return parser.setScannerTagError(directive, start_mark, "did not find expected '!'") } } *handle = s return nil } // Scan a tag URI. // directive: true if scanning a %TAG directive URI // verbatim: true if scanning a verbatim tag !<...> or TAG directive (flow indicators allowed) func (parser *Parser) scanTagURI(directive bool, verbatim bool, head []byte, start_mark Mark, uri *[]byte) error { // size_t length = head ? strlen((char *)head) : 0 var s []byte hasTag := len(head) > 0 // Copy the head if needed. // // Note that we don't copy the leading '!' character. if len(head) > 1 { s = append(s, head[1:]...) } // Scan the tag. if parser.unread < 1 { if err := parser.updateBuffer(1); err != nil { return err } } // The set of characters that may appear in URI is as follows: // // '0'-'9', 'A'-'Z', 'a'-'z', '_', '-', ';', '/', '?', ':', '@', // '&', '=', '+', '$', '.', '!', '~', '*', '\'', '(', ')', '%'. // // Note: Flow indicators (',', '[', ']', '{', '}') are only allowed in // verbatim tags. for isTagURIChar(parser.buffer, parser.buffer_pos, verbatim) { // Check if it is a URI-escape sequence. if parser.buffer[parser.buffer_pos] == '%' { if err := parser.scanURIEscapes(directive, start_mark, &s); err != nil { return err } } else { s = parser.read(s) } if parser.unread < 1 { if err := parser.updateBuffer(1); err != nil { return err } } hasTag = true } // Check for characters which are not allowed in tags. // For non-verbatim tags, if we stopped at a printable character that // isn't whitespace, it's an invalid tag character - give a specific // error. // For verbatim tags, the caller will check for the expected '>' // delimiter. if !verbatim { c := parser.buffer[parser.buffer_pos] if !isBlankOrZero(parser.buffer, parser.buffer_pos) && c >= 0x20 && c <= 0x7E { return parser.setScannerTagError(directive, start_mark, fmt.Sprintf("found character '%c' that is not allowed in a YAML tag", c)) } } if !hasTag { return parser.setScannerTagError(directive, start_mark, "did not find expected tag URI") } *uri = s return nil } // Decode an URI-escape sequence corresponding to a single UTF-8 character. func (parser *Parser) scanToNextToken() error { scan_mark := parser.mark // Until the next token is not found. for { // Allow the BOM mark to start a line. if parser.unread < 1 { if err := parser.updateBuffer(1); err != nil { return err } } if parser.mark.Column == 1 && isBOM(parser.buffer, parser.buffer_pos) { parser.skip() } // Eat whitespaces. // Tabs are allowed: // - in the flow context // - in the block context, but not at the beginning of the line or // after '-', '?', or ':' (complex value). if parser.unread < 1 { if err := parser.updateBuffer(1); err != nil { return err } } for parser.buffer[parser.buffer_pos] == ' ' || ((parser.flow_level > 0 || !parser.simple_key_allowed) && parser.buffer[parser.buffer_pos] == '\t') { parser.skip() if parser.unread < 1 { if err := parser.updateBuffer(1); err != nil { return err } } } // Check if we just had a line comment under a sequence entry that // looks more like a header to the following content. Similar to this: // // - # The comment // - Some data // // If so, transform the line comment to a head comment and reposition. if len(parser.comments) > 0 && len(parser.tokens) > 1 { tokenA := parser.tokens[len(parser.tokens)-2] tokenB := parser.tokens[len(parser.tokens)-1] comment := &parser.comments[len(parser.comments)-1] if tokenA.Type == BLOCK_SEQUENCE_START_TOKEN && tokenB.Type == BLOCK_ENTRY_TOKEN && len(comment.Line) > 0 && !isLineBreak(parser.buffer, parser.buffer_pos) { // If it was in the prior line, reposition so it becomes a // header of the follow up token. Otherwise, keep it in place // so it becomes a header of the former. comment.Head = comment.Line comment.Line = nil if comment.StartMark.Line == parser.mark.Line-1 { comment.TokenMark = parser.mark } } } // Eat a comment until a line break. if parser.buffer[parser.buffer_pos] == '#' { if parser.skip_comments { // Skip comment without storing it for !isLineBreak(parser.buffer, parser.buffer_pos) && !isZeroChar(parser.buffer, parser.buffer_pos) { parser.skip() if parser.unread < 1 { if err := parser.updateBuffer(1); err != nil { return err } } } } else { if err := parser.scanComments(scan_mark); err != nil { return err } } } // If it is a line break, eat it. if isLineBreak(parser.buffer, parser.buffer_pos) { if parser.unread < 2 { if err := parser.updateBuffer(2); err != nil { return err } } parser.skipLine() // In the block context, a new line may start a simple key. if parser.flow_level == 0 { parser.simple_key_allowed = true } } else { break // We have found a token. } } return nil } // Scan a YAML-DIRECTIVE or TAG-DIRECTIVE token. // // Scope: // // %YAML 1.1 # a comment \n // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ // %TAG !yaml! tag:yaml.org,2002: \n // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ func (parser *Parser) scanURIEscapes(directive bool, start_mark Mark, s *[]byte) error { // Decode the required number of characters. w := 1024 for w > 0 { // Check for a URI-escaped octet. if parser.unread < 3 { if err := parser.updateBuffer(3); err != nil { return err } } if !(parser.buffer[parser.buffer_pos] == '%' && isHex(parser.buffer, parser.buffer_pos+1) && isHex(parser.buffer, parser.buffer_pos+2)) { return parser.setScannerTagError(directive, start_mark, "did not find URI escaped octet") } // Get the octet. octet := byte((asHex(parser.buffer, parser.buffer_pos+1) << 4) + asHex(parser.buffer, parser.buffer_pos+2)) // If it is the leading octet, determine the length of the // UTF-8 sequence. if w == 1024 { w = width(octet) if w == 0 { return parser.setScannerTagError(directive, start_mark, "found an incorrect leading UTF-8 octet") } } else { // Check if the trailing octet is correct. if octet&0xC0 != 0x80 { return parser.setScannerTagError(directive, start_mark, "found an incorrect trailing UTF-8 octet") } } // Copy the octet and move the pointers. *s = append(*s, octet) parser.skip() parser.skip() parser.skip() w-- } return nil } // Scan a block scalar. func (parser *Parser) scanVersionDirectiveNumber(start_mark Mark, number *int8) error { // Repeat while the next character is digit. if parser.unread < 1 { if err := parser.updateBuffer(1); err != nil { return err } } var value, length int8 for isDigit(parser.buffer, parser.buffer_pos) { // Check if the number is too long. length++ if length > max_number_length { return formatScannerErrorContext("while scanning a %YAML directive", start_mark, "found extremely long version number", parser.mark) } value = value*10 + int8(asDigit(parser.buffer, parser.buffer_pos)) parser.skip() if parser.unread < 1 { if err := parser.updateBuffer(1); err != nil { return err } } } // Check if the number was present. if length == 0 { return formatScannerErrorContext("while scanning a %YAML directive", start_mark, "did not find expected version number", parser.mark) } *number = value return nil } // Scan the value of a TAG-DIRECTIVE token. // // Scope: // // %TAG !yaml! tag:yaml.org,2002: \n // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ func (parser *Parser) scanVersionDirectiveValue(start_mark Mark, major, minor *int8) error { // Eat whitespaces. if parser.unread < 1 { if err := parser.updateBuffer(1); err != nil { return err } } for isBlank(parser.buffer, parser.buffer_pos) { parser.skip() if parser.unread < 1 { if err := parser.updateBuffer(1); err != nil { return err } } } // Consume the major version number. if err := parser.scanVersionDirectiveNumber(start_mark, major); err != nil { return err } // Eat '.'. if parser.buffer[parser.buffer_pos] != '.' { return formatScannerErrorContext("while scanning a %YAML directive", start_mark, "did not find expected digit or '.' character", parser.mark) } parser.skip() // Consume the minor version number. if err := parser.scanVersionDirectiveNumber(start_mark, minor); err != nil { return err } return nil } // max_number_length limits the length of version number components in // %YAML directives. func (parser *Parser) saveSimpleKey() error { // A simple key is required at the current position if the scanner is in // the block context and the current column coincides with the indentation // level. required := parser.flow_level == 0 && parser.indent == parser.mark.Column // // If the current position may start a simple key, save it. // if parser.simple_key_allowed { if err := parser.removeSimpleKey(); err != nil { return err } parser.simple_key_possible = true parser.simple_key = SimpleKey{ required: required, flow_level: parser.flow_level, token_number: parser.tokens_parsed + (len(parser.tokens) - parser.tokens_head), mark: parser.mark, } } return nil } // Remove a potential simple key at the current flow level. func (parser *Parser) removeSimpleKey() error { // If the key is required, it is an error. if parser.simple_key.required { return formatScannerErrorContext( "while scanning a simple key", parser.simple_key.mark, "could not find expected ':'", parser.mark) } parser.simple_key_possible = false // disable the key return nil } func (parser *Parser) isFlowSequence() bool { if len(parser.tokens) == 0 { return false } previousToken := parser.tokens[len(parser.tokens)-1] return previousToken.Type == FLOW_ENTRY_TOKEN || previousToken.Type == FLOW_SEQUENCE_START_TOKEN } // Check if a simple key may start at the current position and add it if // needed. func (parser *Parser) increaseFlowLevel() error { // Increase the flow level. parser.flow_level++ if err := parser.depthCheck(parser.flow_level, &DepthContext{Kind: DepthKindFlow}); err != nil { return formatScannerErrorContext( "while increasing flow level", parser.simple_key.mark, err.Error(), parser.mark) } // If a simple key was possible, push it to the stack before resetting the key. if parser.simple_key_possible { parser.simple_key_stack = append(parser.simple_key_stack, parser.simple_key) } // Reset the simple key for the new flow level. parser.simple_key = SimpleKey{} return nil } // Decrease the flow level. func (parser *Parser) decreaseFlowLevel() error { if parser.flow_level > 0 { parser.flow_level-- if len(parser.simple_key_stack) == 0 { return nil } last := len(parser.simple_key_stack) - 1 if parser.simple_key_stack[last].flow_level == parser.flow_level { parser.simple_key = parser.simple_key_stack[last] // use last item parser.simple_key_stack = parser.simple_key_stack[:last] // remove last item parser.simple_key_possible = true // enable the key } } return nil } func (parser *Parser) rollIndent(column, number int, typ TokenType, mark Mark) error { // In the flow context, do nothing. if parser.flow_level > 0 { return nil } if parser.indent < column { // Push the current indentation level to the stack and set the new // indentation level. parser.indents = append(parser.indents, parser.indent) parser.indent = column if err := parser.depthCheck(len(parser.indents), &DepthContext{Kind: DepthKindBlock}); err != nil { return formatScannerErrorContext( "while increasing indent level", parser.simple_key.mark, err.Error(), parser.mark) } // Create a token and insert it into the queue. token := Token{ Type: typ, StartMark: mark, EndMark: mark, } if number > -1 { number -= parser.tokens_parsed } parser.insertToken(number, &token) } return nil } // Pop indentation levels from the indents stack until the current level // becomes less or equal to the column. For each indentation level, append // the BLOCK-END token. func (parser *Parser) unrollIndent(column int, scan_mark Mark) error { // In the flow context, do nothing. if parser.flow_level > 0 { return nil } block_mark := scan_mark block_mark.Index-- // Loop through the indentation levels in the stack. for parser.indent > column { // [Go] Reposition the end token before potential following // foot comments of parent blocks. For that, search // backwards for recent comments that were at the same // indent as the block that is ending now. stop_index := block_mark.Index for i := len(parser.comments) - 1; i >= 0; i-- { comment := &parser.comments[i] if comment.EndMark.Index < stop_index { // Don't go back beyond the start of the // comment/whitespace scan, unless column < 0. // If requested indent column is < 0, then the // document is over and everything else is a // foot anyway. break } if comment.StartMark.Column == parser.indent+1 { // This is a good match. // But maybe there's a former comment at that // same indent level, so keep searching. block_mark = comment.StartMark } // While the end of the former comment matches with // the start of the following one, we know there's // nothing in between and scanning is still safe. stop_index = comment.ScanMark.Index } // Create a token and append it to the queue. token := Token{ Type: BLOCK_END_TOKEN, StartMark: block_mark, EndMark: block_mark, } parser.insertToken(-1, &token) // Pop the indentation level. parser.indent = parser.indents[len(parser.indents)-1] parser.indents = parser.indents[:len(parser.indents)-1] } return nil } // insertToken inserts a token into the queue at the specified position. func (parser *Parser) insertToken(pos int, token *Token) { // fmt.Println("yaml_insert_token", "pos:", pos, "typ:", token.typ, "head:", parser.tokens_head, "len:", len(parser.tokens)) // Check if we can move the queue at the beginning of the buffer. if parser.tokens_head > 0 && len(parser.tokens) == cap(parser.tokens) { if parser.tokens_head != len(parser.tokens) { copy(parser.tokens, parser.tokens[parser.tokens_head:]) } parser.tokens = parser.tokens[:len(parser.tokens)-parser.tokens_head] parser.tokens_head = 0 } parser.tokens = append(parser.tokens, *token) if pos < 0 { return } copy(parser.tokens[parser.tokens_head+pos+1:], parser.tokens[parser.tokens_head+pos:]) parser.tokens[parser.tokens_head+pos] = *token } // Initialize the scanner and produce the STREAM-START token. func (parser *Parser) skip() { if !isBlank(parser.buffer, parser.buffer_pos) { parser.newlines = 0 } parser.mark.Index++ parser.mark.Column++ parser.unread-- parser.buffer_pos += width(parser.buffer[parser.buffer_pos]) } // skipLine advances the parser position past the current line break. func (parser *Parser) skipLine() { if isCRLF(parser.buffer, parser.buffer_pos) { parser.mark.Index += 2 parser.mark.Column = 1 parser.mark.Line++ parser.unread -= 2 parser.buffer_pos += 2 parser.newlines++ } else if isLineBreak(parser.buffer, parser.buffer_pos) { parser.mark.Index++ parser.mark.Column = 1 parser.mark.Line++ parser.unread-- parser.buffer_pos += width(parser.buffer[parser.buffer_pos]) parser.newlines++ } } // Copy a character to a string buffer and advance pointers. func (parser *Parser) read(s []byte) []byte { if !isBlank(parser.buffer, parser.buffer_pos) { parser.newlines = 0 } w := width(parser.buffer[parser.buffer_pos]) if w == 0 { panic("invalid character sequence") } if len(s) == 0 { s = make([]byte, 0, 32) } if w == 1 && len(s)+w <= cap(s) { s = s[:len(s)+1] s[len(s)-1] = parser.buffer[parser.buffer_pos] parser.buffer_pos++ } else { s = append(s, parser.buffer[parser.buffer_pos:parser.buffer_pos+w]...) parser.buffer_pos += w } parser.mark.Index++ parser.mark.Column++ parser.unread-- return s } // Copy a line break character to a string buffer and advance pointers. func (parser *Parser) readLine(s []byte) []byte { buf := parser.buffer pos := parser.buffer_pos switch { case buf[pos] == '\r' && buf[pos+1] == '\n': // CR LF . LF s = append(s, '\n') parser.buffer_pos += 2 parser.mark.Index++ parser.unread-- case buf[pos] == '\r' || buf[pos] == '\n': // CR|LF . LF s = append(s, '\n') parser.buffer_pos += 1 case buf[pos] == '\xC2' && buf[pos+1] == '\x85': // NEL . LF s = append(s, '\n') parser.buffer_pos += 2 case buf[pos] == '\xE2' && buf[pos+1] == '\x80' && (buf[pos+2] == '\xA8' || buf[pos+2] == '\xA9'): // LS|PS . LS|PS s = append(s, buf[parser.buffer_pos:pos+3]...) parser.buffer_pos += 3 default: return s } parser.mark.Index++ parser.mark.Column = 1 parser.mark.Line++ parser.unread-- parser.newlines++ return s } // formatScannerError creates a LoadError for scanner-stage errors. func formatScannerError(problem string, problemMark Mark) *LoadError { return &LoadError{ Stage: ScannerStage, Mark: problemMark, Message: problem, } } // formatScannerErrorContext creates a LoadError with both context and // problem information, each with their own mark positions. func formatScannerErrorContext(context string, contextMark Mark, problem string, problemMark Mark) *LoadError { return &LoadError{ Stage: ScannerStage, ContextMark: contextMark, ContextMsg: context, Mark: problemMark, Message: problem, } } // setScannerTagError creates a tag-related scanner error with appropriate // context based on whether it's from a directive or tag parsing. func (parser *Parser) setScannerTagError(directive bool, contextMark Mark, problem string) error { context := "while parsing a tag" if directive { context = "while parsing a %TAG directive" } return formatScannerErrorContext(context, contextMark, problem, parser.mark) } // trace is a debug utility that prints entry/exit messages for function calls. func trace(args ...any) func() { pargs := append([]any{"+++"}, args...) fmt.Println(pargs...) pargs = append([]any{"---"}, args...) return func() { fmt.Println(pargs...) } } // Ensure that the tokens queue contains at least one token which can be // returned to the Parser. golang-go.yaml-yaml-v4-4.0.0~rc5/internal/libyaml/scanner_test.go000066400000000000000000000133101521353103300247030ustar00rootroot00000000000000// Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Tests for the scanner stage. // Verifies input stream to token stream transformation, indentation handling, // and simple keys. package libyaml import ( "bytes" "testing" "go.yaml.in/yaml/v4/internal/testutil/assert" ) func TestScanner(t *testing.T) { RunTestCases(t, "scanner.yaml", map[string]TestHandler{ "scan-tokens": runScanTokensTest, "scan-tokens-detailed": runScanTokensDetailedTest, "scan-error": runScanErrorTest, "char-predicate": runCharPredicateTest, "char-convert": runCharConvertTest, }) } // runScanTokensTest tests the scanTokens function. // //nolint:thelper // because this function is the real test func runScanTokensTest(t *testing.T, tc TestCase) { types, ok := scanTokens(tc.Yaml) assert.Truef(t, ok, "scanTokens() failed") // Convert Want from interface{} to []string wantSlice, ok := tc.Want.([]any) assert.Truef(t, ok, "Want should be []interface{}") var expected []TokenType for _, item := range wantSlice { tokenStr, ok := item.(string) assert.Truef(t, ok, "Want item should be string") expected = append(expected, ParseTokenType(t, tokenStr)) } assert.Equalf(t, len(expected), len(types), "scanTokens() got %d tokens, want %d", len(types), len(expected)) for i, tt := range expected { assert.Equalf(t, tt, types[i], "token[%d] = %v, want %v", i, types[i], tt) } } // runScanTokensDetailedTest tests the scanTokensDetailed function. // //nolint:thelper // because this function is the real test func runScanTokensDetailedTest(t *testing.T, tc TestCase) { tokens, ok := scanTokensDetailed(tc.Yaml) assert.Truef(t, ok, "scanTokensDetailed() failed") assert.Equalf(t, len(tc.WantTokens), len(tokens), "scanTokensDetailed() got %d tokens, want %d", len(tokens), len(tc.WantTokens)) for i, wantSpec := range tc.WantTokens { token := tokens[i] wantType := ParseTokenType(t, wantSpec.Type) assert.Equalf(t, wantType, token.Type, "token[%d].Type = %v, want %v", i, token.Type, wantType) // Check specific token properties if specified if wantSpec.Value != "" { assert.Truef(t, bytes.Equal(token.Value, []byte(wantSpec.Value)), "token[%d].Value = %q, want %q", i, token.Value, wantSpec.Value) } if wantSpec.Style != "" { wantStyle := ParseScalarStyle(t, wantSpec.Style) assert.Equalf(t, wantStyle, token.Style, "token[%d].Style = %v, want %v", i, token.Style, wantStyle) } } } // runScanErrorTest tests the scanner error handling. // //nolint:thelper // because this function is the real test func runScanErrorTest(t *testing.T, tc TestCase) { parser := NewParser() parser.SetInputString([]byte(tc.Yaml)) var token Token var scanErr error for scanErr == nil && token.Type != STREAM_END_TOKEN { scanErr = parser.Scan(&token) } // Convert Want from interface{} to check for error // Want can be either a boolean or a single-element sequence // Defaults to true if not specified wantError := true if tc.Want != nil { switch v := tc.Want.(type) { case bool: wantError = v case []any: if len(v) > 0 { if boolVal, ok := v[0].(bool); ok { wantError = boolVal } } } } if wantError { assert.Truef(t, scanErr != nil, "Expected scanner error, but got none") // Check error message against regex pattern if provided if tc.Like != "" { assert.ErrorMatchesf(t, tc.Like, scanErr, "") } } else { assert.Truef(t, scanErr == nil, "Expected no scanner error, but got %v", scanErr) } } // Character classification and conversion tests // These tests are now part of scanner.yaml and run via TestScanner func runCharPredicateTest(t *testing.T, tc TestCase) { t.Helper() input := tc.Input index := tc.Index // Default want to true if not specified want := WantBool(t, tc.Want, true) var got bool switch tc.Function { case "isAlpha": got = isAlpha(input, index) case "isFlowIndicator": got = isFlowIndicator(input, index) case "isAnchorChar": got = isAnchorChar(input, index) case "isColon": got = isColon(input, index) case "isTagURIChar": // Default verbatim to false if not specified verbatim := false if len(tc.Args) >= 1 { v, ok := tc.Args[0].(bool) assert.Truef(t, ok, "Args[0] should be bool, got %T", tc.Args[0]) verbatim = v } got = isTagURIChar(input, index, verbatim) case "isDigit": got = isDigit(input, index) case "isHex": got = isHex(input, index) case "isASCII": got = isASCII(input, index) case "isPrintable": got = isPrintable(input, index) case "isZeroChar": got = isZeroChar(input, index) case "isBOM": got = isBOM(input, index) case "isSpace": got = isSpace(input, index) case "isTab": got = isTab(input, index) case "isBlank": got = isBlank(input, index) case "isLineBreak": got = isLineBreak(input, index) case "isCRLF": got = isCRLF(input, index) case "isBreakOrZero": got = isBreakOrZero(input, index) case "isSpaceOrZero": got = isSpaceOrZero(input, index) case "isBlankOrZero": got = isBlankOrZero(input, index) default: t.Fatalf("unknown function: %s", tc.Function) } assert.Equalf(t, want, got, "%s(%q, %d) = %v, want %v", tc.Function, input, index, got, want) } func runCharConvertTest(t *testing.T, tc TestCase) { t.Helper() input := tc.Input index := tc.Index want, ok := tc.Want.(int) assert.Truef(t, ok, "Want should be int, got %T", tc.Want) var got int switch tc.Function { case "asDigit": got = asDigit(input, index) case "asHex": got = asHex(input, index) case "width": // width takes a single byte, not a byte array and index got = width(input[index]) default: t.Fatalf("unknown function: %s", tc.Function) } assert.Equalf(t, want, got, "%s(%q, %d) = %d, want %d", tc.Function, input, index, got, want) } golang-go.yaml-yaml-v4-4.0.0~rc5/internal/libyaml/serializer.go000066400000000000000000000271221521353103300243720ustar00rootroot00000000000000// Copyright 2011-2019 Canonical Ltd // Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Serializer stage: Converts representation tree (Nodes) to event stream. // Walks the node tree and produces events for the emitter. package libyaml import ( "errors" "fmt" "io" "strings" "unicode/utf8" ) // Serializer handles serialization of YAML nodes to event stream. type Serializer struct { Emitter Emitter out []byte lineWidth int explicitStart bool explicitEnd bool flowSimpleCollections bool quotePreference QuoteStyle doneInit bool } // NewSerializer creates a new Serializer with the given options. func NewSerializer(w io.Writer, opts *Options) *Serializer { emitter := NewEmitter() emitter.CompactSequenceIndent = opts.CompactSeqIndent emitter.quotePreference = opts.QuotePreference emitter.SetWidth(opts.LineWidth) emitter.SetUnicode(opts.Unicode) emitter.SetCanonical(opts.Canonical) emitter.SetLineBreak(opts.LineBreak) // Set indentation (defaults to 2 if not specified) indent := opts.Indent if indent == 0 { indent = 2 } emitter.BestIndent = indent if w != nil { emitter.SetOutputWriter(w) } return &Serializer{ Emitter: emitter, lineWidth: opts.LineWidth, explicitStart: opts.ExplicitStart, explicitEnd: opts.ExplicitEnd, flowSimpleCollections: opts.FlowSimpleCollections, quotePreference: opts.QuotePreference, } } // Serialize walks a Node tree and emits events to produce YAML output. // This is the primary method for the Serializer stage. func (s *Serializer) Serialize(node *Node) { s.init() s.node(node, "") } // Sentinel values for event creation. // These provide clarity at call sites, similar to http.NoBody. var ( noVersionDirective *VersionDirective = nil noTagDirective []TagDirective = nil ) // init initializes the serializer by emitting a STREAM_START event. func (s *Serializer) init() { if s.doneInit { return } s.emit(NewStreamStartEvent(UTF8_ENCODING)) s.doneInit = true } // Finish completes serialization by emitting a STREAM_END event. func (s *Serializer) Finish() { s.Emitter.OpenEnded = false s.emit(NewStreamEndEvent()) } // node serializes a Node tree into YAML events. // This is the core of the serializer stage - it walks the tree and produces // events. func (s *Serializer) node(node *Node, tail string) { // Zero nodes behave as nil. if node.Kind == 0 && node.IsZero() { s.emitScalar("null", "", "", PLAIN_SCALAR_STYLE, nil, nil, nil, nil) return } // Tags have been processed by Desolver: // - Empty tag = can be inferred or style handles it // - Non-empty tag = emit explicitly // Style has also been set by Desolver for quoting needs tag := node.Tag var forceQuoting bool if tag == "" && node.Kind == ScalarNode { // Empty tag with quoting style means the string type needs to // be preserved if node.Style&(SingleQuotedStyle|DoubleQuotedStyle|LiteralStyle|FoldedStyle) != 0 { forceQuoting = true } } switch node.Kind { case DocumentNode: event := NewDocumentStartEvent(noVersionDirective, noTagDirective, !s.explicitStart) event.HeadComment = []byte(node.HeadComment) s.emit(event) for _, node := range node.Content { s.node(node, "") } event = NewDocumentEndEvent(!s.explicitEnd) event.FootComment = []byte(node.FootComment) s.emit(event) case SequenceNode: style := BLOCK_SEQUENCE_STYLE // Use flow style if explicitly requested or if it's a simple // collection (scalar-only contents that fit within line width, // enabled via WithFlowSimpleCollections) if node.Style&FlowStyle != 0 || s.isSimpleCollection(node) { style = FLOW_SEQUENCE_STYLE } event := NewSequenceStartEvent([]byte(node.Anchor), []byte(longTag(tag)), tag == "", style) event.HeadComment = []byte(node.HeadComment) s.emit(event) for _, node := range node.Content { s.node(node, "") } event = NewSequenceEndEvent() event.LineComment = []byte(node.LineComment) event.FootComment = []byte(node.FootComment) s.emit(event) case MappingNode: style := BLOCK_MAPPING_STYLE // Use flow style if explicitly requested or if it's a simple // collection (scalar-only contents that fit within line width, // enabled via WithFlowSimpleCollections) if node.Style&FlowStyle != 0 || s.isSimpleCollection(node) { style = FLOW_MAPPING_STYLE } event := NewMappingStartEvent([]byte(node.Anchor), []byte(longTag(tag)), tag == "", style) event.TailComment = []byte(tail) event.HeadComment = []byte(node.HeadComment) s.emit(event) // The tail logic below moves the foot comment of prior keys to // the following key, since the value for each key may be a // nested structure and the foot needs to be processed only the // entirety of the value is streamed. The last tail is // processed with the mapping end event. var tail string for i := 0; i+1 < len(node.Content); i += 2 { k := node.Content[i] foot := k.FootComment if foot != "" { kopy := *k kopy.FootComment = "" k = &kopy } s.node(k, tail) tail = foot v := node.Content[i+1] s.node(v, "") } event = NewMappingEndEvent() event.TailComment = []byte(tail) event.LineComment = []byte(node.LineComment) event.FootComment = []byte(node.FootComment) s.emit(event) case AliasNode: event := NewAliasEvent([]byte(node.Value)) event.HeadComment = []byte(node.HeadComment) event.LineComment = []byte(node.LineComment) event.FootComment = []byte(node.FootComment) s.emit(event) case ScalarNode: value := node.Value if !utf8.ValidString(value) { stag := shortTag(tag) if stag == binaryTag { failDumpf(SerializerStage, "explicitly tagged !!binary data must be base64-encoded") } if stag != "" { failDumpf(SerializerStage, "cannot marshal invalid UTF-8 data as %s", stag) } // It can't be represented directly as YAML so use a binary tag // and represent it as base64. tag = binaryTag value = encodeBase64(value) } style := PLAIN_SCALAR_STYLE switch { case node.Style&DoubleQuotedStyle != 0: style = DOUBLE_QUOTED_SCALAR_STYLE case node.Style&SingleQuotedStyle != 0: style = SINGLE_QUOTED_SCALAR_STYLE case node.Style&LiteralStyle != 0: style = LITERAL_SCALAR_STYLE case node.Style&FoldedStyle != 0: style = FOLDED_SCALAR_STYLE case strings.Contains(value, "\n"): style = LITERAL_SCALAR_STYLE case forceQuoting: style = s.quotePreference.ScalarStyle() } s.emitScalar(value, node.Anchor, tag, style, []byte(node.HeadComment), []byte(node.LineComment), []byte(node.FootComment), []byte(tail)) default: failDumpf(SerializerStage, "cannot represent node with unknown kind %d", node.Kind) } } // emit sends an event to the underlying emitter. func (s *Serializer) emit(event Event) { s.must(s.Emitter.Emit(&event)) } // must panics if the given error is non-nil, routing to the appropriate stage. func (s *Serializer) must(err error) { if err == nil { return } var ee EmitterError if errors.As(err, &ee) { failDumpf(EmitterStage, "%s", ee.Message) } var we WriterError if errors.As(err, &we) { // Unwrap to get the original I/O error, stripping the // "write error: " prefix that WriterError adds internally. cause := we.Err if unwrapped := errors.Unwrap(we.Err); unwrapped != nil { cause = unwrapped } failDump(WriterStage, cause) } msg := err.Error() if msg == "" { msg = fmt.Sprintf("unknown problem generating YAML content with %T", err) } failDumpf(SerializerStage, "%s", msg) } // emitScalar emits a scalar event with the given value, anchor, tag, style, // and associated comments. func (s *Serializer) emitScalar( value, anchor, tag string, style ScalarStyle, head, line, foot, tail []byte, ) { implicit := tag == "" if !implicit { tag = longTag(tag) } event := NewScalarEvent([]byte(anchor), []byte(tag), []byte(value), implicit, implicit, style) event.HeadComment = head event.LineComment = line event.FootComment = foot event.TailComment = tail s.emit(event) } // isSimpleCollection checks if a node contains only scalar values and would // fit within the line width when rendered in flow style. func (s *Serializer) isSimpleCollection(node *Node) bool { if !s.flowSimpleCollections { return false } if node.Kind != SequenceNode && node.Kind != MappingNode { return false } // Check all children are scalars for _, child := range node.Content { if child.Kind != ScalarNode { return false } } // Estimate flow style length estimatedLen := s.estimateFlowLength(node) width := s.lineWidth if width <= 0 { width = 80 // Default width if not set } return estimatedLen > 0 && estimatedLen <= width } // estimateFlowLength estimates the character length of a node in flow style. func (s *Serializer) estimateFlowLength(node *Node) int { if node.Kind == SequenceNode { // [item1, item2, ...] = 2 + sum(len(items)) + 2*(len-1) length := 2 // [] for i, child := range node.Content { if i > 0 { length += 2 // ", " } length += len(child.Value) } return length } if node.Kind == MappingNode { // {key1: val1, key2: val2} = 2 + sum(key: val) + 2*(pairs-1) length := 2 // {} for i := 0; i < len(node.Content); i += 2 { if i > 0 { length += 2 // ", " } length += len(node.Content[i].Value) + 2 + len(node.Content[i+1].Value) // "key: val" } return length } return 0 } // NewStreamStartEvent creates a new STREAM-START event. func NewStreamStartEvent(encoding Encoding) Event { return Event{ Type: STREAM_START_EVENT, encoding: encoding, } } // NewStreamEndEvent creates a new STREAM-END event. func NewStreamEndEvent() Event { return Event{ Type: STREAM_END_EVENT, } } // NewDocumentStartEvent creates a new DOCUMENT-START event. func NewDocumentStartEvent(version_directive *VersionDirective, tag_directives []TagDirective, implicit bool) Event { return Event{ Type: DOCUMENT_START_EVENT, versionDirective: version_directive, tagDirectives: tag_directives, Implicit: implicit, } } // NewDocumentEndEvent creates a new DOCUMENT-END event. func NewDocumentEndEvent(implicit bool) Event { return Event{ Type: DOCUMENT_END_EVENT, Implicit: implicit, } } // NewAliasEvent creates a new ALIAS event. func NewAliasEvent(anchor []byte) Event { return Event{ Type: ALIAS_EVENT, Anchor: anchor, } } // NewScalarEvent creates a new SCALAR event. func NewScalarEvent(anchor, tag, value []byte, plain_implicit, quoted_implicit bool, style ScalarStyle) Event { return Event{ Type: SCALAR_EVENT, Anchor: anchor, Tag: tag, Value: value, Implicit: plain_implicit, quoted_implicit: quoted_implicit, Style: Style(style), } } // NewSequenceStartEvent creates a new SEQUENCE-START event. func NewSequenceStartEvent(anchor, tag []byte, implicit bool, style SequenceStyle) Event { return Event{ Type: SEQUENCE_START_EVENT, Anchor: anchor, Tag: tag, Implicit: implicit, Style: Style(style), } } // NewSequenceEndEvent creates a new SEQUENCE-END event. func NewSequenceEndEvent() Event { return Event{ Type: SEQUENCE_END_EVENT, } } // NewMappingStartEvent creates a new MAPPING-START event. func NewMappingStartEvent(anchor, tag []byte, implicit bool, style MappingStyle) Event { return Event{ Type: MAPPING_START_EVENT, Anchor: anchor, Tag: tag, Implicit: implicit, Style: Style(style), } } // NewMappingEndEvent creates a new MAPPING-END event. func NewMappingEndEvent() Event { return Event{ Type: MAPPING_END_EVENT, } } // Delete an event object. func (e *Event) Delete() { *e = Event{} } golang-go.yaml-yaml-v4-4.0.0~rc5/internal/libyaml/serializer_test.go000066400000000000000000000064351521353103300254350ustar00rootroot00000000000000// Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Tests for the Serializer stage package libyaml import ( "bytes" "testing" "go.yaml.in/yaml/v4/internal/testutil/assert" ) // buildNodeFromSpec recursively builds a Node from a NodeSpec func buildNodeFromSpec(spec map[string]any) *Node { node := &Node{} // Set kind if kindStr, ok := spec["kind"].(string); ok { switch kindStr { case "Document": node.Kind = DocumentNode case "Scalar": node.Kind = ScalarNode case "Sequence": node.Kind = SequenceNode case "Mapping": node.Kind = MappingNode } } // Set value if value, ok := spec["value"].(string); ok { node.Value = value } // Set style if styleStr, ok := spec["style"].(string); ok { switch styleStr { case "Single": node.Style = SingleQuotedStyle case "Double": node.Style = DoubleQuotedStyle case "Flow": node.Style = FlowStyle } } // Set content (recursive) if contentData, ok := spec["content"].([]any); ok { for _, item := range contentData { if itemMap, ok := item.(map[string]any); ok { child := buildNodeFromSpec(itemMap) node.Content = append(node.Content, child) } } } return node } func TestSerializer(t *testing.T) { RunTestCases(t, "serializer.yaml", map[string]TestHandler{ "serialize-scalar": func(t *testing.T, tc TestCase) { t.Helper() // Build node from nested spec nodeData, ok := tc.Node.Content.([]any) if !ok || len(nodeData) == 0 { t.Fatal("expected content in node spec") } // Create document with scalar doc := &Node{Kind: DocumentNode} for _, item := range nodeData { if itemMap, ok := item.(map[string]any); ok { doc.Content = append(doc.Content, buildNodeFromSpec(itemMap)) } } var buf bytes.Buffer s := NewSerializer(&buf, DefaultOptions) s.Serialize(doc) s.Finish() // Check output assert.Equal(t, tc.Want.(string), buf.String()) }, "serialize-collection": func(t *testing.T, tc TestCase) { t.Helper() // Build node from nested spec nodeData, ok := tc.Node.Content.([]any) if !ok || len(nodeData) == 0 { t.Fatal("expected content in node spec") } // Create document doc := &Node{Kind: DocumentNode} for _, item := range nodeData { if itemMap, ok := item.(map[string]any); ok { doc.Content = append(doc.Content, buildNodeFromSpec(itemMap)) } } // Serialize with appropriate indent opts := DefaultOptions if tc.Indent > 0 { opts.Indent = tc.Indent } var buf bytes.Buffer s := NewSerializer(&buf, opts) s.Serialize(doc) s.Finish() // Check output assert.Equal(t, tc.Want.(string), buf.String()) }, "serialize-style": func(t *testing.T, tc TestCase) { t.Helper() // Build node from nested spec nodeData, ok := tc.Node.Content.([]any) if !ok || len(nodeData) == 0 { t.Fatal("expected content in node spec") } // Create document doc := &Node{Kind: DocumentNode} for _, item := range nodeData { if itemMap, ok := item.(map[string]any); ok { doc.Content = append(doc.Content, buildNodeFromSpec(itemMap)) } } var buf bytes.Buffer s := NewSerializer(&buf, DefaultOptions) s.Serialize(doc) s.Finish() // Check output assert.Equal(t, tc.Want.(string), buf.String()) }, }) } golang-go.yaml-yaml-v4-4.0.0~rc5/internal/libyaml/structmeta.go000066400000000000000000000156001521353103300244120ustar00rootroot00000000000000// Copyright 2011-2019 Canonical Ltd // Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Struct metadata extraction for YAML marshaling/unmarshaling. // // This file analyzes Go struct types to build mappings between YAML keys and // struct fields. It parses struct tags like `yaml:"name,omitempty,flow,inline"` // and caches the results for efficient repeated access. // // Used by: // - Constructor: maps YAML keys to struct fields when unmarshaling // - Representer: maps struct fields to YAML keys when marshaling // // Key types: // - structInfo: cached metadata about a struct type // - fieldInfo: metadata about a single struct field // - getStructInfo(): analyzes a struct type and returns cached metadata package libyaml import ( "errors" "fmt" "reflect" "strings" "sync" ) // structInfo holds cached information about a struct's YAML-relevant fields. type structInfo struct { FieldsMap map[string]fieldInfo FieldsList []fieldInfo // InlineMap is the number of the field in the struct that // contains an ,inline map, or -1 if there's none. InlineMap int // InlineConstructors holds indexes to inlined fields that // contain constructor values. InlineConstructors [][]int } // fieldInfo holds information about a single struct field. type fieldInfo struct { Key string Num int OmitEmpty bool Flow bool // Id holds the unique field identifier, so we can cheaply // check for field duplicates without maintaining an extra map. Id int // Inline holds the field index if the field is part of an inlined struct. Inline []int } // structMap caches struct reflection information. // fieldMapMutex protects access to structMap. // constructorType holds the [reflect.Type] for the constructor interface. var ( structMap = make(map[reflect.Type]*structInfo) fieldMapMutex sync.RWMutex constructorType reflect.Type ) // constructor interface is defined here to detect types that implement // UnmarshalYAML during struct reflection. type constructor interface { UnmarshalYAML(value *Node) error } // init initializes the constructorType variable with the [reflect.Type] of constructor interface. func init() { var v constructor constructorType = reflect.ValueOf(&v).Elem().Type() } // hasConstructYAMLMethod checks if a type has an UnmarshalYAML method // that takes a *Node from an allowlisted v3 yaml package. This detects // v3 backward-compatible Unmarshaler implementations whose Node type // can't be checked via interface assertion from this package. func hasConstructYAMLMethod(t reflect.Type) bool { method, found := t.MethodByName("UnmarshalYAML") if !found { return false } // Check signature: func(*T) UnmarshalYAML(*Node) error mtype := method.Type if mtype.NumIn() != 2 || mtype.NumOut() != 1 { return false } // First param is receiver (already checked by MethodByName) // Second param should be a pointer to a Node-like struct paramType := mtype.In(1) if paramType.Kind() != reflect.Ptr { return false } elemType := paramType.Elem() if elemType.Kind() != reflect.Struct || elemType.Name() != "Node" || !isYAMLNodePkg(elemType.PkgPath()) { return false } // Return type should be error retType := mtype.Out(0) if retType.Kind() != reflect.Interface || retType.Name() != "error" { return false } return true } func isYAMLNodePkg(pkg string) bool { switch pkg { case "gopkg.in/yaml.v3", "go.yaml.in/yaml/v3": return true } return false } // getStructInfo returns cached information about a struct type's fields. // It parses struct tags and builds a map of field names to field info. func getStructInfo(st reflect.Type) (*structInfo, error) { fieldMapMutex.RLock() sinfo, found := structMap[st] fieldMapMutex.RUnlock() if found { return sinfo, nil } n := st.NumField() fieldsMap := make(map[string]fieldInfo) fieldsList := make([]fieldInfo, 0, n) inlineMap := -1 inlineConstructors := [][]int(nil) for i := 0; i != n; i++ { field := st.Field(i) if field.PkgPath != "" && !field.Anonymous { continue // Private field } info := fieldInfo{Num: i} tag := field.Tag.Get("yaml") if tag == "" && !strings.Contains(string(field.Tag), ":") { tag = string(field.Tag) } if tag == "-" { continue } inline := false fields := strings.Split(tag, ",") if len(fields) > 1 { for _, flag := range fields[1:] { switch flag { case "omitempty": info.OmitEmpty = true case "flow": info.Flow = true case "inline": inline = true default: return nil, fmt.Errorf("unsupported flag %q in tag %q of type %s", flag, tag, st) } } tag = fields[0] } if inline { switch field.Type.Kind() { case reflect.Map: if inlineMap >= 0 { return nil, errors.New("multiple ,inline maps in struct " + st.String()) } if field.Type.Key() != reflect.TypeOf("") { return nil, errors.New("option ,inline needs a map with string keys in struct " + st.String()) } inlineMap = info.Num case reflect.Struct, reflect.Pointer: ftype := field.Type for ftype.Kind() == reflect.Pointer { ftype = ftype.Elem() } if ftype.Kind() != reflect.Struct { return nil, errors.New("option ,inline may only be used on a struct or map field") } // Check for both libyaml.constructor and yaml.Unmarshaler (by method name) if reflect.PointerTo(ftype).Implements(constructorType) || hasConstructYAMLMethod(reflect.PointerTo(ftype)) { inlineConstructors = append(inlineConstructors, []int{i}) } else { sinfo, err := getStructInfo(ftype) if err != nil { return nil, err } for _, index := range sinfo.InlineConstructors { inlineConstructors = append(inlineConstructors, append([]int{i}, index...)) } for _, finfo := range sinfo.FieldsList { if _, found := fieldsMap[finfo.Key]; found { msg := "duplicated key '" + finfo.Key + "' in struct " + st.String() return nil, errors.New(msg) } if finfo.Inline == nil { finfo.Inline = []int{i, finfo.Num} } else { finfo.Inline = append([]int{i}, finfo.Inline...) } finfo.Id = len(fieldsList) fieldsMap[finfo.Key] = finfo fieldsList = append(fieldsList, finfo) } } default: return nil, errors.New("option ,inline may only be used on a struct or map field") } continue } if tag != "" { info.Key = tag } else { info.Key = strings.ToLower(field.Name) } if _, found = fieldsMap[info.Key]; found { msg := "duplicated key '" + info.Key + "' in struct " + st.String() return nil, errors.New(msg) } info.Id = len(fieldsList) fieldsList = append(fieldsList, info) fieldsMap[info.Key] = info } sinfo = &structInfo{ FieldsMap: fieldsMap, FieldsList: fieldsList, InlineMap: inlineMap, InlineConstructors: inlineConstructors, } fieldMapMutex.Lock() structMap[st] = sinfo fieldMapMutex.Unlock() return sinfo, nil } golang-go.yaml-yaml-v4-4.0.0~rc5/internal/libyaml/structmeta_test.go000066400000000000000000000052341521353103300254530ustar00rootroot00000000000000// Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Tests for isYAMLNodePkg and the Node type allowlist. package libyaml import ( "reflect" "testing" ) func TestIsYAMLNodePkg(t *testing.T) { tests := []struct { pkg string want bool }{ {"gopkg.in/yaml.v3", true}, {"go.yaml.in/yaml/v3", true}, {"example.com/mypkg", false}, {"gopkg.in/yaml.v2", false}, {"", false}, } for _, tt := range tests { if got := isYAMLNodePkg(tt.pkg); got != tt.want { t.Errorf("isYAMLNodePkg(%q) = %v, want %v", tt.pkg, got, tt.want) } } } // foreignNode is a user-defined type that happens to be named "Node" // but is NOT from a known yaml package. The allowlist must reject it. type foreignNode struct { Bogus int } // foreignNodeReceiver has an UnmarshalYAML method that takes a // *foreignNode — this must NOT be treated as a yaml.Unmarshaler. type foreignNodeReceiver struct { Value string } func (f *foreignNodeReceiver) UnmarshalYAML(n *foreignNode) error { f.Value = "should not be called" return nil } // yamlNodeReceiver has an UnmarshalYAML method that takes the real // *Node from this package — this MUST be accepted. type yamlNodeReceiver struct { Value string } func (y *yamlNodeReceiver) UnmarshalYAML(n *Node) error { if n.Value != "" { y.Value = n.Value } return nil } func TestForeignNodeRejected(t *testing.T) { // hasConstructYAMLMethod must return false for foreignNodeReceiver. ft := reflect.TypeOf(foreignNodeReceiver{}) if hasConstructYAMLMethod(reflect.PointerTo(ft)) { t.Error("hasConstructYAMLMethod accepted foreignNodeReceiver") } } func TestRealNodeNotInAllowlist(t *testing.T) { // v4 types (this package) are no longer in the allowlist — they use // the native constructor interface instead of the unsafe cast path. yt := reflect.TypeOf(yamlNodeReceiver{}) if hasConstructYAMLMethod(reflect.PointerTo(yt)) { t.Error("hasConstructYAMLMethod should not match v4 types") } } func TestForeignNodeUnmarshal(t *testing.T) { // Unmarshaling into foreignNodeReceiver must silently skip the // UnmarshalYAML method (not panic or corrupt memory). var f foreignNodeReceiver err := Load([]byte("hello"), &f) if err == nil { t.Fatal("expected error unmarshaling scalar into struct") } if f.Value == "should not be called" { t.Error("foreign UnmarshalYAML was incorrectly called") } } func TestRealNodeUnmarshal(t *testing.T) { // Unmarshaling into yamlNodeReceiver must call UnmarshalYAML. var y yamlNodeReceiver err := Load([]byte("hello"), &y) if err != nil { t.Fatalf("unexpected error: %v", err) } if y.Value != "hello" { t.Errorf("Value = %q, want %q", y.Value, "hello") } } golang-go.yaml-yaml-v4-4.0.0~rc5/internal/libyaml/testdata/000077500000000000000000000000001521353103300234775ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc5/internal/libyaml/testdata/composer.yaml000066400000000000000000000057521521353103300262230ustar00rootroot00000000000000# Copyright 2025 The go-yaml Project Contributors # SPDX-License-Identifier: Apache-2.0 # Composer stage test cases # # Purpose: Tests the Composer's ability to parse YAML and build node trees # # Test Types: # compose-scalar - Tests parsing scalar values # compose-collection - Tests parsing sequences and mappings # compose-style - Tests style detection (flow, literal, folded, quoted) # # Field Descriptions: # name: Human-readable test name # from: Input YAML text to parse (literal block scalar) # want: Expected node structure after parsing # kind: Expected node kind (Scalar, Sequence, Mapping) # tag: Expected YAML tag (only explicit tags; defaults applied by resolver) # value: Expected scalar value (for Scalar nodes) # style: Expected style flags # content: Expected child nodes (array of node objects) - compose-scalar: name: Plain string from: | hello want: kind: Scalar value: hello - compose-scalar: name: Integer from: | 42 want: kind: Scalar value: '42' - compose-scalar: name: Single quoted string from: | '123' want: kind: Scalar value: '123' style: Single - compose-scalar: name: Double quoted string from: | "hello" want: kind: Scalar value: hello style: Double - compose-scalar: name: Literal block scalar from: | | line 1 line 2 want: kind: Scalar value: "line 1\nline 2\n" style: Literal - compose-scalar: name: Folded block scalar from: | > line 1 line 2 want: kind: Scalar value: "line 1 line 2\n" style: Folded - compose-collection: name: Block sequence from: | - a - b - c want: kind: Sequence content: - kind: Scalar value: a - kind: Scalar value: b - kind: Scalar value: c - compose-collection: name: Block mapping from: | key1: value1 key2: value2 want: kind: Mapping content: - kind: Scalar value: key1 - kind: Scalar value: value1 - kind: Scalar value: key2 - kind: Scalar value: value2 - compose-style: name: Flow sequence from: | [a, b, c] want: kind: Sequence style: Flow content: - kind: Scalar value: a - kind: Scalar value: b - kind: Scalar value: c - compose-style: name: Flow mapping from: | {key: value} want: kind: Mapping style: Flow content: - kind: Scalar value: key - kind: Scalar value: value - compose-collection: name: Nested mapping from: | outer: inner: value want: kind: Mapping content: - kind: Scalar value: outer - kind: Mapping content: - kind: Scalar value: inner - kind: Scalar value: value golang-go.yaml-yaml-v4-4.0.0~rc5/internal/libyaml/testdata/constructor.yaml000066400000000000000000000035451521353103300267570ustar00rootroot00000000000000# Copyright 2025 The go-yaml Project Contributors # SPDX-License-Identifier: Apache-2.0 # Data loader test cases for scalar type resolution # # Purpose: Tests the YAML data loader's ability to correctly parse and resolve # different numeric types (positive/negative integers, floats, scientific notation) # # Test Types: # scalar-resolution - Verifies that scalar values are resolved to correct Go types - scalar-resolution: name: Positive integer yaml: "42" want: 42 - scalar-resolution: name: Negative integer yaml: "-42" want: -42 - scalar-resolution: name: Zero yaml: "0" want: 0 - scalar-resolution: name: Hex integer lowercase yaml: "0xff" want: 255 - scalar-resolution: name: Hex integer uppercase yaml: "0XFF" want: 255 - scalar-resolution: name: Float with decimal point yaml: "3.14" want: 3.14 - scalar-resolution: name: Negative float yaml: "-2.5" want: -2.5 - scalar-resolution: name: Float starting with decimal yaml: ".5" want: 0.5 - scalar-resolution: name: Float with trailing zeros yaml: "1.500" want: 1.5 - scalar-resolution: name: Boolean true yaml: "true" want: true - scalar-resolution: name: Boolean false yaml: "false" want: false - scalar-resolution: name: Null keyword yaml: 'null' want: null - scalar-resolution: name: String value yaml: "hello" want: 'hello' - scalar-resolution: name: String that looks like number but quoted yaml: '"42"' want: '42' - scalar-resolution: name: Mapping with numeric values yaml: | int: 42 neg: -10 float: 3.14 want: int: 42 neg: -10 float: 3.14 - scalar-resolution: name: Sequence with mixed numeric types yaml: | - 42 - -10 - 3.14 want: - 42 - -10 - 3.14 golang-go.yaml-yaml-v4-4.0.0~rc5/internal/libyaml/testdata/desolver.yaml000066400000000000000000000062131521353103300262100ustar00rootroot00000000000000# Copyright 2025 The go-yaml Project Contributors # SPDX-License-Identifier: Apache-2.0 # Desolver stage test cases # # Purpose: Tests the Desolver's ability to remove inferable tags from nodes # while preserving custom and special tags. # # Test Types: # desolve-inferable - Tests removing tags that can be inferred # desolve-preserve - Tests preserving tags that must be kept # desolve-string-quoting - Tests string quoting for type preservation # # Field Descriptions: # name: Human-readable test name # node: Input node to desolve # tag: YAML tag (e.g., "!!int", "!!str", "!custom") # value: Scalar value # kind: Node kind (Scalar, Mapping, Sequence) - defaults to Scalar # style: Node style (Tagged, SingleQuoted, etc.) # want: Expected results after desolving # kind: Node kind (if checking collection type) # tag: Expected tag after desolving (empty string means tag removed) # style: Scalar style (Plain, Single, Double, etc.) - desolve-inferable: name: Remove inferable int tag node: tag: '!!int' value: '123' want: tag: '' style: Plain - desolve-inferable: name: Remove inferable bool tag node: tag: '!!bool' value: 'true' want: tag: '' style: Plain - desolve-inferable: name: Remove inferable float tag node: tag: '!!float' value: '3.14' want: tag: '' style: Plain - desolve-inferable: name: Remove inferable str tag node: tag: '!!str' value: 'hello' want: tag: '' style: Plain - desolve-preserve: name: Preserve custom tag node: tag: '!tag:something' value: '123' want: tag: '!tag:something' style: Plain - desolve-inferable: name: Elide implicit merge tag node: tag: '!!merge' value: '<<' want: tag: '' style: Plain - desolve-preserve: name: Preserve merge tag with TaggedStyle node: tag: '!!merge' value: '<<' style: Tagged want: tag: '!!merge' style: Tagged - desolve-preserve: name: Preserve binary tag node: tag: '!!binary' value: 'SGVsbG8=' want: tag: '!!binary' style: Plain - desolve-preserve: name: Preserve omap tag on collection node: tag: '!!omap' kind: Mapping want: kind: Mapping tag: '!!omap' - desolve-string-quoting: name: Quote string that looks like int node: tag: '!!str' value: '123' want: tag: '' style: Single - desolve-string-quoting: name: Quote string that looks like bool node: tag: '!!str' value: 'true' want: tag: '' style: Single - desolve-string-quoting: name: Quote string that looks like float node: tag: '!!str' value: '3.14' want: tag: '' style: Single - desolve-inferable: name: Remove float tag when value is integer node: tag: '!!float' value: '1' want: tag: '' style: Plain - desolve-preserve: name: Preserve tag with TaggedStyle node: tag: '!!int' value: '123' style: Tagged want: tag: '!!int' golang-go.yaml-yaml-v4-4.0.0~rc5/internal/libyaml/testdata/emitter.yaml000066400000000000000000000355451521353103300260500ustar00rootroot00000000000000# Copyright 2025 The go-yaml Project Contributors # SPDX-License-Identifier: Apache-2.0 # Emitter test cases for libyaml emitter # # Purpose: Tests the YAML emitter that converts events into YAML text output. # # Format: Test cases use type-as-key format: # - test-type: # name: Test case name # ... # # Test Types: # emit - Emit events to string, verify output contains expected strings # emit-config - Emit events with specific emitter configuration # roundtrip - Parse input YAML, emit it back, verify output contains expected strings # emit-writer - Emit events to io.Writer, verify output contains expected strings # # Common Keys: # name - Test case name (string) # data - List of event specifications to emit (for emit, emit-config, emit-writer) # conf - Emitter configuration map (for emit-config type only): # * canonical - Boolean, emit in canonical YAML form # * indent - Integer (2-9), indentation width in spaces # * width - Integer, line width (-1 for unlimited, or positive number) # * unicode - Boolean, enable Unicode character output # yaml - Input YAML string (for roundtrip type only) # want - Expected result (format varies by test type) # # Event Specification Format (for data lists): # Simplified (type only): - EVENT_TYPE # Detailed (with fields): - EVENT_TYPE: # encoding: ... (for STREAM_START_EVENT) # value: ... (for SCALAR_EVENT) # anchor: ... (for SCALAR_EVENT, ALIAS_EVENT) # tag: ... (for SCALAR_EVENT) # implicit: ... (for various events) # style: ... (PLAIN_SCALAR_STYLE, SINGLE_QUOTED_SCALAR_STYLE, # DOUBLE_QUOTED_SCALAR_STYLE, LITERAL_SCALAR_STYLE, # FOLDED_SCALAR_STYLE, BLOCK_SEQUENCE_STYLE, # FLOW_SEQUENCE_STYLE, BLOCK_MAPPING_STYLE, # FLOW_MAPPING_STYLE) # Simple emit tests - emit: name: Simple scalar data: - STREAM_START_EVENT: encoding: UTF8_ENCODING - DOCUMENT_START_EVENT: implicit: true - SCALAR_EVENT: value: hello implicit: true style: PLAIN_SCALAR_STYLE - DOCUMENT_END_EVENT: implicit: true - STREAM_END_EVENT want: hello - emit: name: Simple mapping data: - STREAM_START_EVENT: encoding: UTF8_ENCODING - DOCUMENT_START_EVENT: implicit: true - MAPPING_START_EVENT: implicit: true style: BLOCK_MAPPING_STYLE - SCALAR_EVENT: value: key implicit: true style: PLAIN_SCALAR_STYLE - SCALAR_EVENT: value: value implicit: true style: PLAIN_SCALAR_STYLE - MAPPING_END_EVENT - DOCUMENT_END_EVENT: implicit: true - STREAM_END_EVENT want: - key - value - emit: name: Block sequence data: - STREAM_START_EVENT: encoding: UTF8_ENCODING - DOCUMENT_START_EVENT: implicit: true - SEQUENCE_START_EVENT: implicit: true style: BLOCK_SEQUENCE_STYLE - SCALAR_EVENT: value: item1 implicit: true style: PLAIN_SCALAR_STYLE - SCALAR_EVENT: value: item2 implicit: true style: PLAIN_SCALAR_STYLE - SEQUENCE_END_EVENT - DOCUMENT_END_EVENT: implicit: true - STREAM_END_EVENT want: - item1 - item2 - '-' - emit: name: Flow sequence data: - STREAM_START_EVENT: encoding: UTF8_ENCODING - DOCUMENT_START_EVENT: implicit: true - SEQUENCE_START_EVENT: implicit: true style: FLOW_SEQUENCE_STYLE - SCALAR_EVENT: value: '1' implicit: true style: PLAIN_SCALAR_STYLE - SCALAR_EVENT: value: '2' implicit: true style: PLAIN_SCALAR_STYLE - SCALAR_EVENT: value: '3' implicit: true style: PLAIN_SCALAR_STYLE - SEQUENCE_END_EVENT - DOCUMENT_END_EVENT: implicit: true - STREAM_END_EVENT want: - '[' - ']' - emit: name: Flow mapping data: - STREAM_START_EVENT: encoding: UTF8_ENCODING - DOCUMENT_START_EVENT: implicit: true - MAPPING_START_EVENT: implicit: true style: FLOW_MAPPING_STYLE - SCALAR_EVENT: value: a implicit: true style: PLAIN_SCALAR_STYLE - SCALAR_EVENT: value: '1' implicit: true style: PLAIN_SCALAR_STYLE - SCALAR_EVENT: value: b implicit: true style: PLAIN_SCALAR_STYLE - SCALAR_EVENT: value: '2' implicit: true style: PLAIN_SCALAR_STYLE - MAPPING_END_EVENT - DOCUMENT_END_EVENT: implicit: true - STREAM_END_EVENT want: - '{' - '}' - emit: name: Explicit document data: - STREAM_START_EVENT: encoding: UTF8_ENCODING - DOCUMENT_START_EVENT: implicit: false - SCALAR_EVENT: value: value implicit: true style: PLAIN_SCALAR_STYLE - DOCUMENT_END_EVENT: implicit: false - STREAM_END_EVENT want: - '---' - '...' - emit: name: Anchor and alias data: - STREAM_START_EVENT: encoding: UTF8_ENCODING - DOCUMENT_START_EVENT: implicit: true - SEQUENCE_START_EVENT: implicit: true style: BLOCK_SEQUENCE_STYLE - SCALAR_EVENT: anchor: myanchor value: value implicit: true style: PLAIN_SCALAR_STYLE - ALIAS_EVENT: anchor: myanchor - SEQUENCE_END_EVENT - DOCUMENT_END_EVENT: implicit: true - STREAM_END_EVENT want: - '&myanchor' - '*myanchor' - emit: name: Tag data: - STREAM_START_EVENT: encoding: UTF8_ENCODING - DOCUMENT_START_EVENT: implicit: true - SCALAR_EVENT: tag: 'tag:yaml.org,2002:str' value: value implicit: false style: PLAIN_SCALAR_STYLE - DOCUMENT_END_EVENT: implicit: true - STREAM_END_EVENT want: '!!str' - emit: name: Single quoted scalar data: - STREAM_START_EVENT: encoding: UTF8_ENCODING - DOCUMENT_START_EVENT: implicit: true - SCALAR_EVENT: value: quoted value implicit: true style: SINGLE_QUOTED_SCALAR_STYLE - DOCUMENT_END_EVENT: implicit: true - STREAM_END_EVENT want: "'" - emit: name: Double quoted scalar data: - STREAM_START_EVENT: encoding: UTF8_ENCODING - DOCUMENT_START_EVENT: implicit: true - SCALAR_EVENT: value: quoted value implicit: true style: DOUBLE_QUOTED_SCALAR_STYLE - DOCUMENT_END_EVENT: implicit: true - STREAM_END_EVENT want: '"' - emit: name: Literal scalar data: - STREAM_START_EVENT: encoding: UTF8_ENCODING - DOCUMENT_START_EVENT: implicit: true - MAPPING_START_EVENT: implicit: true style: BLOCK_MAPPING_STYLE - SCALAR_EVENT: value: key implicit: true style: PLAIN_SCALAR_STYLE - SCALAR_EVENT: value: "line1\nline2\n" implicit: true style: LITERAL_SCALAR_STYLE - MAPPING_END_EVENT - DOCUMENT_END_EVENT: implicit: true - STREAM_END_EVENT want: '|' - emit: name: Folded scalar data: - STREAM_START_EVENT: encoding: UTF8_ENCODING - DOCUMENT_START_EVENT: implicit: true - MAPPING_START_EVENT: implicit: true style: BLOCK_MAPPING_STYLE - SCALAR_EVENT: value: key implicit: true style: PLAIN_SCALAR_STYLE - SCALAR_EVENT: value: "folded text\n" implicit: true style: FOLDED_SCALAR_STYLE - MAPPING_END_EVENT - DOCUMENT_END_EVENT: implicit: true - STREAM_END_EVENT want: '>' # Config tests - emit-config: name: Canonical mode conf: canonical: true data: - STREAM_START_EVENT: encoding: UTF8_ENCODING - DOCUMENT_START_EVENT: implicit: false - SCALAR_EVENT: value: value implicit: true style: PLAIN_SCALAR_STYLE - DOCUMENT_END_EVENT: implicit: false - STREAM_END_EVENT want: --- - emit-config: name: Custom indent conf: indent: 4 data: - STREAM_START_EVENT: encoding: UTF8_ENCODING - DOCUMENT_START_EVENT: implicit: true - MAPPING_START_EVENT: implicit: true style: BLOCK_MAPPING_STYLE - SCALAR_EVENT: value: key implicit: true style: PLAIN_SCALAR_STYLE - MAPPING_START_EVENT: implicit: true style: BLOCK_MAPPING_STYLE - SCALAR_EVENT: value: nested implicit: true style: PLAIN_SCALAR_STYLE - SCALAR_EVENT: value: value implicit: true style: PLAIN_SCALAR_STYLE - MAPPING_END_EVENT - MAPPING_END_EVENT - DOCUMENT_END_EVENT: implicit: true - STREAM_END_EVENT want: key - emit-config: name: Custom width conf: width: 40 data: - STREAM_START_EVENT: encoding: UTF8_ENCODING - DOCUMENT_START_EVENT: implicit: true - SCALAR_EVENT: value: short implicit: true style: PLAIN_SCALAR_STYLE - DOCUMENT_END_EVENT: implicit: true - STREAM_END_EVENT want: short - emit-config: name: Unicode mode conf: unicode: true data: - STREAM_START_EVENT: encoding: UTF8_ENCODING - DOCUMENT_START_EVENT: implicit: true - SCALAR_EVENT: value: "unicode: \u00e9" implicit: true style: PLAIN_SCALAR_STYLE - DOCUMENT_END_EVENT: implicit: true - STREAM_END_EVENT want: unicode - emit: name: Multiple documents data: - STREAM_START_EVENT: encoding: UTF8_ENCODING - DOCUMENT_START_EVENT: implicit: false - SCALAR_EVENT: value: doc1 implicit: true style: PLAIN_SCALAR_STYLE - DOCUMENT_END_EVENT: implicit: false - DOCUMENT_START_EVENT: implicit: false - SCALAR_EVENT: value: doc2 implicit: true style: PLAIN_SCALAR_STYLE - DOCUMENT_END_EVENT: implicit: false - STREAM_END_EVENT want: --- - emit: name: Nested structures data: - STREAM_START_EVENT: encoding: UTF8_ENCODING - DOCUMENT_START_EVENT: implicit: true - MAPPING_START_EVENT: implicit: true style: BLOCK_MAPPING_STYLE - SCALAR_EVENT: value: parent implicit: true style: PLAIN_SCALAR_STYLE - SEQUENCE_START_EVENT: implicit: true style: BLOCK_SEQUENCE_STYLE - MAPPING_START_EVENT: implicit: true style: BLOCK_MAPPING_STYLE - SCALAR_EVENT: value: child implicit: true style: PLAIN_SCALAR_STYLE - SCALAR_EVENT: value: value implicit: true style: PLAIN_SCALAR_STYLE - MAPPING_END_EVENT - SEQUENCE_END_EVENT - MAPPING_END_EVENT - DOCUMENT_END_EVENT: implicit: true - STREAM_END_EVENT want: - parent - child # Roundtrip test - roundtrip: name: Roundtrip yaml: | key: value list: - item1 - item2 want: - key - value - item1 # Writer test - emit-writer: name: Writer data: - STREAM_START_EVENT: encoding: UTF8_ENCODING - DOCUMENT_START_EVENT: implicit: true - SCALAR_EVENT: value: test implicit: true style: PLAIN_SCALAR_STYLE - DOCUMENT_END_EVENT: implicit: true - STREAM_END_EVENT want: test # Emitter API tests (from api.yaml) - api-new: name: New emitter with: NewEmitter test: - nil: [buffer, false] - len: [buffer, 128] # output_buffer_size - nil: [raw-buffer, false] - nil: [states, false] - nil: [events, false] - eq: [best-width, -1] - api-method: name: Emitter set canonical true with: NewEmitter call: [SetCanonical, true] test: - eq: [canonical, true] - api-method: name: Emitter set canonical false with: NewEmitter call: [SetCanonical, false] test: - eq: [canonical, false] - api-method: name: Emitter set indent 2 with: NewEmitter call: [SetIndent, 2] test: - eq: [BestIndent, 2] - api-method: name: Emitter set indent 5 with: NewEmitter call: [SetIndent, 5] test: - eq: [BestIndent, 5] - api-method: name: Emitter set indent clamp low with: NewEmitter call: [SetIndent, 1] test: - eq: [BestIndent, 2] - api-method: name: Emitter set indent clamp high with: NewEmitter call: [SetIndent, 10] test: - eq: [BestIndent, 2] - api-method: name: Emitter set width 80 with: NewEmitter call: [SetWidth, 80] test: - eq: [best-width, 80] - api-method: name: Emitter set width -1 with: NewEmitter call: [SetWidth, -1] test: - eq: [best-width, -1] - api-method: name: Emitter set width clamp with: NewEmitter call: [SetWidth, -10] test: - eq: [best-width, -1] - api-method: name: Emitter set unicode true with: NewEmitter call: [SetUnicode, true] test: - eq: [unicode, true] - api-method: name: Emitter set unicode false with: NewEmitter call: [SetUnicode, false] test: - eq: [unicode, false] - api-method: name: Emitter set line break with: NewEmitter call: [SetLineBreak, LN_BREAK] test: - eq: [line-break, LN_BREAK] - api-panic: name: Emitter set encoding twice with: NewEmitter init: [SetEncoding, UTF8_ENCODING] call: [SetEncoding, UTF16LE_ENCODING] want: must set the output encoding only once - api-delete: name: Emitter delete with: NewEmitter test: - nil: [output-buffer, true] - len: [buffer, 0] # Event constructor tests - api-new-event: name: New stream start event call: [NewStreamStartEvent, UTF8_ENCODING] test: - eq: [Type, STREAM_START_EVENT] - eq: [encoding, UTF8_ENCODING] - api-new-event: name: New stream end event call: [NewStreamEndEvent] test: - eq: [Type, STREAM_END_EVENT] - api-new-event: name: New document end event call: [NewDocumentEndEvent, false] test: - eq: [Type, DOCUMENT_END_EVENT] - eq: [Implicit, false] - api-new-event: name: New alias event byte: true call: [NewAliasEvent, myanchor] test: - eq: [Type, ALIAS_EVENT] - eq: [Anchor, myanchor] - api-new-event: name: New sequence end event call: [NewSequenceEndEvent] test: - eq: [Type, SEQUENCE_END_EVENT] - api-new-event: name: New mapping end event call: [NewMappingEndEvent] test: - eq: [Type, MAPPING_END_EVENT] golang-go.yaml-yaml-v4-4.0.0~rc5/internal/libyaml/testdata/errors.yaml000066400000000000000000000155061521353103300257060ustar00rootroot00000000000000# Copyright 2025 The go-yaml Project Contributors # SPDX-License-Identifier: Apache-2.0 # Error type tests # # Purpose: Tests YAML error types and their formatting # # Format: Test cases use type-as-key format: # - test-type: # name: Test case name # from: input error data # want: expected error string output # also: unwrap # (optional) test that Unwrap() returns underlying error # # Test Types: # load-error - Tests LoadError formatting with different stages # emitter-error - Tests EmitterError formatting (dump stack) # writer-error - Tests WriterError formatting and Unwrap (dump stack) # load-errors - Tests LoadErrors formatting # load-errors-as - Tests LoadErrors.As method # load-errors-is - Tests LoadErrors.Is method # type-error - Tests deprecated TypeError formatting # LoadError tests - different stages - load-error: name: Reader stage error from: stage: reader mark: line: 1 column: 3 index: 2 message: 'invalid UTF-8 octet (value: 255)' err: invalid UTF-8 octet want: 'go-yaml load error in reader at L1.C3: invalid UTF-8 octet (value: 255)' also: unwrap - load-error: name: Scanner stage error from: stage: scanner mark: line: 2 column: 6 index: 42 message: unexpected token want: 'go-yaml load error in scanner at L2.C6: unexpected token' - load-error: name: Scanner stage error with context from: stage: scanner context_mark: line: 2 column: 0 index: 20 context_message: while scanning a block scalar mark: line: 4 column: 9 index: 60 message: found unexpected end of stream want: 'go-yaml load error in scanner (while scanning a block scalar) at L2-L4.C9: found unexpected end of stream' - load-error: name: Parser stage error from: stage: parser mark: line: 3 column: 11 index: 50 message: did not find expected key want: 'go-yaml load error in parser at L3.C11: did not find expected key' - load-error: name: Parser stage error with context from: stage: parser context_mark: line: 1 column: 0 index: 0 context_message: while parsing a block mapping mark: line: 4 column: 6 index: 80 message: did not find expected key want: 'go-yaml load error in parser (while parsing a block mapping) at L1-L4.C6: did not find expected key' - load-error: name: Composer stage error from: stage: composer mark: line: 1 column: 5 index: 4 message: unknown anchor 'foo' referenced want: "go-yaml load error in composer at L1.C5: unknown anchor 'foo' referenced" - load-error: name: Resolver stage error from: stage: resolver mark: line: 1 column: 0 index: 0 message: cannot construct !!str `error` as a !!float want: 'go-yaml load error in resolver at L1: cannot construct !!str `error` as a !!float' - load-error: name: Constructor stage error from: stage: constructor mark: line: 1 column: 0 index: 0 message: cannot construct !!map into string err: cannot construct !!map into string want: 'go-yaml load error in constructor at L1: cannot construct !!map into string' also: unwrap - load-error: name: Constructor stage error at unknown position from: stage: constructor message: cannot construct value err: cannot construct value want: 'go-yaml load error in constructor at : cannot construct value' also: unwrap # DumpError tests - dump stages - dump-error: name: Representer stage error from: stage: representer message: cannot represent type chan int want: 'go-yaml dump error in representer: cannot represent type chan int' - dump-error: name: Representer stage error with cause from: stage: representer message: 'MarshalYAML failed: some error' err: 'some error' want: "go-yaml dump error in representer: MarshalYAML failed: some error" also: unwrap - dump-error: name: Serializer stage error from: stage: serializer message: cannot represent node with unknown kind 99 want: 'go-yaml dump error in serializer: cannot represent node with unknown kind 99' - dump-error: name: Emitter stage error from: stage: emitter message: expected STREAM-START want: 'go-yaml dump error in emitter: expected STREAM-START' - dump-error: name: Writer stage error with cause from: stage: writer message: write error err: write error want: 'go-yaml dump error in writer: write error' also: unwrap # EmitterError tests (dump stack) - emitter-error: name: Basic emitter error from: message: expected STREAM-START want: 'yaml: expected STREAM-START' # WriterError tests (dump stack) - writer-error: name: Writer error with unwrap from: message: write error want: 'yaml: write error' also: unwrap # LoadErrors tests - load-errors: name: Single error from: errors: - line: 3 message: field not found want: 'yaml: construct errors: line 3: field not found' - load-errors: name: Multiple errors from: errors: - line: 5 message: field 'foo' not found in type struct - line: 8 message: field 'bar' not found in type struct want: "yaml: construct errors: line 5: field 'foo' not found in type struct; line 8: field 'bar' not found in type struct" - load-errors: name: Error at unknown position from: errors: - line: 0 message: unknown field want: 'yaml: construct errors: : unknown field' # LoadErrors.As tests - load-errors-as: name: As TypeError from: errors: - line: 3 message: first error - line: 7 message: second error as: TypeError want_as: true want_messages: - 'line 3: first error' - 'line 7: second error' # LoadErrors.Is tests - load-errors-is: name: Contains error from: errors: - line: 5 message: target error is: target error want_is: true - load-errors-is: name: Does not contain error from: errors: - line: 5 message: different error is: target error want_is: false # TypeError tests (backward compatibility) - type-error: name: Single type error from: errors: - field not found want: 'yaml: unmarshal errors: field not found' - type-error: name: Multiple type errors from: errors: - 'line 3: first error' - 'line 5: second error' want: 'yaml: unmarshal errors: line 3: first error; line 5: second error' golang-go.yaml-yaml-v4-4.0.0~rc5/internal/libyaml/testdata/node.yaml000066400000000000000000000132771521353103300253220ustar00rootroot00000000000000# Tests for node.go functions # Testing isZero(), SetString(), shortTag(), longTag(), and Node methods # isZero function tests - name: isZero with empty string type: isZero from: "" want: true - name: isZero with non-empty string type: isZero from: "hello" want: false - name: isZero with zero int type: isZero from: 0 want: true - name: isZero with non-zero int type: isZero from: 42 want: false - name: isZero with zero float type: isZero from: 0.0 want: true - name: isZero with non-zero float type: isZero from: 3.14 want: false - name: isZero with false bool type: isZero from: false want: true - name: isZero with true bool type: isZero from: true want: false - name: isZero with nil slice type: isZero from: null want: true also: slice - name: isZero with empty slice type: isZero from: [] want: true - name: isZero with non-empty slice type: isZero from: [1, 2, 3] want: false - name: isZero with nil map type: isZero from: null want: true also: map - name: isZero with empty map type: isZero from: {} want: true - name: isZero with non-empty map type: isZero from: {key: value} want: false # SetString method tests - name: SetString with simple ASCII string type: set-string from: "hello world" want: kind: ScalarNode tag: "!!str" value: "hello world" style: plain - name: SetString with valid UTF-8 string type: set-string from: "hello 世界" want: kind: ScalarNode tag: "!!str" value: "hello 世界" style: plain - name: SetString with multiline string type: set-string from: "line1\nline2\nline3" want: kind: ScalarNode tag: "!!str" value: "line1\nline2\nline3" style: literal - name: SetString with multiline containing non-whitespace type: set-string from: "a\nb" want: kind: ScalarNode tag: "!!str" value: "a\nb" style: literal - name: SetString with short newline string type: set-string from: "a" want: kind: ScalarNode tag: "!!str" value: "a" style: plain - name: SetString with only whitespace and newline type: set-string from: " \n " want: kind: ScalarNode tag: "!!str" value: " \n " style: plain - name: SetString with empty string type: set-string from: "" want: kind: ScalarNode tag: "!!str" value: "" style: plain - name: SetString with invalid UTF-8 type: set-string-binary input_hex: "fffe" want: kind: ScalarNode tag: "!!binary" style: plain # shortTag and longTag tests - name: shortTag for standard str tag type: short-tag node: kind: Scalar tag: "tag:yaml.org,2002:str" want: "!!str" - name: shortTag for standard int tag type: short-tag node: kind: Scalar tag: "tag:yaml.org,2002:int" want: "!!int" - name: shortTag for standard seq tag type: short-tag node: kind: Sequence tag: "tag:yaml.org,2002:seq" want: "!!seq" - name: shortTag for standard map tag type: short-tag node: kind: Mapping tag: "tag:yaml.org,2002:map" want: "!!map" - name: shortTag for custom long tag type: short-tag node: kind: Scalar tag: "tag:yaml.org,2002:custom" want: "!!custom" - name: shortTag for already short tag type: short-tag node: kind: Scalar tag: "!!str" want: "!!str" - name: shortTag for custom short tag type: short-tag node: kind: Scalar tag: "!mytag" want: "!mytag" - name: shortTag for empty tag on mapping type: short-tag node: kind: Mapping tag: "" want: "!!map" - name: shortTag for empty tag on sequence type: short-tag node: kind: Sequence tag: "" want: "!!seq" - name: shortTag for empty tag on scalar type: short-tag node: kind: Scalar tag: "" want: "!!str" - name: shortTag for quoted scalar type: short-tag node: kind: Scalar tag: "" style: DoubleQuoted want: "!!str" - name: longTag for standard str tag type: long-tag node: kind: Scalar tag: "!!str" want: "tag:yaml.org,2002:str" - name: longTag for standard int tag type: long-tag node: kind: Scalar tag: "!!int" want: "tag:yaml.org,2002:int" - name: longTag for custom short tag type: long-tag node: kind: Scalar tag: "!!custom" want: "tag:yaml.org,2002:custom" - name: longTag for already long tag type: long-tag node: kind: Scalar tag: "tag:yaml.org,2002:str" want: "tag:yaml.org,2002:str" - name: longTag for non-standard tag type: long-tag node: kind: Scalar tag: "!mytag" want: "!mytag" # Node.IsZero tests - name: Node.IsZero on zero node type: node-is-zero node: kind: "" want: true - name: Node.IsZero on node with kind type: node-is-zero node: kind: Scalar want: false - name: Node.IsZero on node with tag type: node-is-zero node: kind: "" tag: "!!str" want: false - name: Node.IsZero on node with value type: node-is-zero node: kind: "" value: "test" want: false # shouldUseLiteralStyle helper tests - name: shouldUseLiteralStyle with no newline type: should-literal from: "hello world" want: false - name: shouldUseLiteralStyle with newline and content type: should-literal from: "hello\nworld" want: true - name: shouldUseLiteralStyle with single char type: should-literal from: "a" want: false - name: shouldUseLiteralStyle with newline but length < 2 type: should-literal from: "\n" want: false - name: shouldUseLiteralStyle with only whitespace type: should-literal from: " \n \n " want: false - name: shouldUseLiteralStyle with whitespace and one char type: should-literal from: " a\n " want: true - name: shouldUseLiteralStyle with empty string type: should-literal from: "" want: false golang-go.yaml-yaml-v4-4.0.0~rc5/internal/libyaml/testdata/options.yaml000066400000000000000000000171351521353103300260650ustar00rootroot00000000000000# Tests for options.go # Testing Option functions, ApplyOptions, and CombineOptions # WithIndent tests - name: WithIndent with valid value 2 type: with-indent from: 2 want: indent: 2 - name: WithIndent with valid value 4 type: with-indent from: 4 want: indent: 4 - name: WithIndent with valid value 9 type: with-indent from: 9 want: indent: 9 - name: WithIndent with invalid value 1 type: with-indent from: 1 like: "indent must be between 2 and 9" - name: WithIndent with invalid value 10 type: with-indent from: 10 like: "indent must be between 2 and 9" - name: WithIndent with invalid value 0 type: with-indent from: 0 like: "indent must be between 2 and 9" # WithCompactSeqIndent tests - name: WithCompactSeqIndent with no args (default true) type: with-compact-seq-indent from: [] want: compact_seq_indent: true - name: WithCompactSeqIndent with true type: with-compact-seq-indent from: [true] want: compact_seq_indent: true - name: WithCompactSeqIndent with false type: with-compact-seq-indent from: [false] want: compact_seq_indent: false - name: WithCompactSeqIndent with too many args type: with-compact-seq-indent from: [true, false] like: "accepts at most one argument" # WithKnownFields tests - name: WithKnownFields with no args (default true) type: with-known-fields from: [] want: known_fields: true - name: WithKnownFields with true type: with-known-fields from: [true] want: known_fields: true - name: WithKnownFields with false type: with-known-fields from: [false] want: known_fields: false - name: WithKnownFields with too many args type: with-known-fields from: [true, false] like: "accepts at most one argument" # WithSingleDocument tests - name: WithSingleDocument with no args (default true) type: with-single-document from: [] want: single_document: true - name: WithSingleDocument with true type: with-single-document from: [true] want: single_document: true - name: WithSingleDocument with false type: with-single-document from: [false] want: single_document: false - name: WithSingleDocument with too many args type: with-single-document from: [true, false] like: "accepts at most one argument" # WithStreamNodes tests - name: WithStreamNodes with no args (default true) type: with-stream-nodes from: [] want: stream_nodes: true - name: WithStreamNodes with true type: with-stream-nodes from: [true] want: stream_nodes: true - name: WithStreamNodes with false type: with-stream-nodes from: [false] want: stream_nodes: false - name: WithStreamNodes with too many args type: with-stream-nodes from: [true, false] like: "accepts at most one argument" # WithAllDocuments tests - name: WithAllDocuments with no args (default true) type: with-all-documents from: [] want: all_documents: true - name: WithAllDocuments with true type: with-all-documents from: [true] want: all_documents: true - name: WithAllDocuments with false type: with-all-documents from: [false] want: all_documents: false - name: WithAllDocuments with too many args type: with-all-documents from: [true, false] like: "accepts at most one argument" # WithLineWidth tests - name: WithLineWidth with positive value type: with-line-width from: 100 want: line_width: 100 - name: WithLineWidth with zero type: with-line-width from: 0 want: line_width: 0 - name: WithLineWidth with negative (unlimited) type: with-line-width from: -5 want: line_width: -1 # WithUnicode tests - name: WithUnicode with no args (default true) type: with-unicode from: [] want: unicode: true - name: WithUnicode with true type: with-unicode from: [true] want: unicode: true - name: WithUnicode with false type: with-unicode from: [false] want: unicode: false - name: WithUnicode with too many args type: with-unicode from: [true, false] like: "accepts at most one argument" # WithUniqueKeys tests - name: WithUniqueKeys with no args (default true) type: with-unique-keys from: [] want: unique_keys: true - name: WithUniqueKeys with true type: with-unique-keys from: [true] want: unique_keys: true - name: WithUniqueKeys with false type: with-unique-keys from: [false] want: unique_keys: false - name: WithUniqueKeys with too many args type: with-unique-keys from: [true, false] like: "accepts at most one argument" # WithCanonical tests - name: WithCanonical with no args (default true) type: with-canonical from: [] want: canonical: true - name: WithCanonical with true type: with-canonical from: [true] want: canonical: true - name: WithCanonical with false type: with-canonical from: [false] want: canonical: false - name: WithCanonical with too many args type: with-canonical from: [true, false] like: "accepts at most one argument" # WithLineBreak tests - name: WithLineBreak with LN type: with-line-break from: LN_BREAK want: line_break: LN_BREAK - name: WithLineBreak with CR type: with-line-break from: CR_BREAK want: line_break: CR_BREAK - name: WithLineBreak with CRLN type: with-line-break from: CRLN_BREAK want: line_break: CRLN_BREAK # WithExplicitStart tests - name: WithExplicitStart with no args (default true) type: with-explicit-start from: [] want: explicit_start: true - name: WithExplicitStart with true type: with-explicit-start from: [true] want: explicit_start: true - name: WithExplicitStart with false type: with-explicit-start from: [false] want: explicit_start: false - name: WithExplicitStart with too many args type: with-explicit-start from: [true, false] like: "accepts at most one argument" # WithExplicitEnd tests - name: WithExplicitEnd with no args (default true) type: with-explicit-end from: [] want: explicit_end: true - name: WithExplicitEnd with true type: with-explicit-end from: [true] want: explicit_end: true - name: WithExplicitEnd with false type: with-explicit-end from: [false] want: explicit_end: false - name: WithExplicitEnd with too many args type: with-explicit-end from: [true, false] like: "accepts at most one argument" # WithFlowSimpleCollections tests - name: WithFlowSimpleCollections with no args (default true) type: with-flow-simple-collections from: [] want: flow_simple_collections: true - name: WithFlowSimpleCollections with true type: with-flow-simple-collections from: [true] want: flow_simple_collections: true - name: WithFlowSimpleCollections with false type: with-flow-simple-collections from: [false] want: flow_simple_collections: false - name: WithFlowSimpleCollections with too many args type: with-flow-simple-collections from: [true, false] like: "accepts at most one argument" # WithQuotePreference tests - name: WithQuotePreference with QuoteSingle type: with-quote-preference from: QuoteSingle want: quote_preference: QuoteSingle - name: WithQuotePreference with QuoteDouble type: with-quote-preference from: QuoteDouble want: quote_preference: QuoteDouble - name: WithQuotePreference with QuoteLegacy type: with-quote-preference from: QuoteLegacy want: quote_preference: QuoteLegacy - name: WithQuotePreference with invalid value type: with-quote-preference from: 99 like: "invalid QuoteStyle value" # ApplyOptions tests - name: ApplyOptions with no options (v4 defaults) type: apply-options want: indent: 2 compact_seq_indent: true line_width: 80 unicode: true unique_keys: true canonical: false golang-go.yaml-yaml-v4-4.0.0~rc5/internal/libyaml/testdata/parser.yaml000066400000000000000000000226441521353103300256670ustar00rootroot00000000000000# Copyright 2025 The go-yaml Project Contributors # SPDX-License-Identifier: Apache-2.0 # Parser test cases for libyaml parser # # Purpose: Tests the YAML parser that converts tokens into events following YAML grammar rules. # # Format: Test cases use type-as-key format: # - test-type: # name: Test case name # ... # # Test Types: # parse-events - Verifies event sequence only # parse-events-detailed - Verifies complete event properties (anchor, tag, value, etc.) # parse-error - Validates error detection for invalid input # # Common Keys: # name - Test case name (string) # yaml - Input YAML string to parse # want - Expected result (format varies by test type): # * For parse-error: boolean (defaults to true if omitted; set to false if no error expected) # * For parse-events: list of event type names (scalars or type-as-key format) # * For parse-events-detailed: list of event specifications (type-as-key format) # # Event Specification Format (for want lists): # Simplified (type only): - EVENT_TYPE # Detailed (with fields): - EVENT_TYPE: # value: ... # anchor: ... # tag: ... # implicit: ... # style: ... # version-directive: {major: N, minor: N} # tag-directives: [{handle: ..., prefix: ...}] # Simple event type tests (just checking event type sequence) - parse-events: name: Simple scalar yaml: | hello want: - STREAM_START_EVENT - DOCUMENT_START_EVENT - SCALAR_EVENT - DOCUMENT_END_EVENT - STREAM_END_EVENT - parse-events: name: Simple mapping yaml: | key: value want: - STREAM_START_EVENT - DOCUMENT_START_EVENT - MAPPING_START_EVENT - SCALAR_EVENT - SCALAR_EVENT - MAPPING_END_EVENT - DOCUMENT_END_EVENT - STREAM_END_EVENT - parse-events: name: Block sequence yaml: | - item1 - item2 want: - STREAM_START_EVENT - DOCUMENT_START_EVENT - SEQUENCE_START_EVENT - SCALAR_EVENT - SCALAR_EVENT - SEQUENCE_END_EVENT - DOCUMENT_END_EVENT - STREAM_END_EVENT - parse-events: name: Flow sequence yaml: | [1, 2, 3] want: - STREAM_START_EVENT - DOCUMENT_START_EVENT - SEQUENCE_START_EVENT - SCALAR_EVENT - SCALAR_EVENT - SCALAR_EVENT - SEQUENCE_END_EVENT - DOCUMENT_END_EVENT - STREAM_END_EVENT - parse-events: name: Flow mapping yaml: | {a: 1, b: 2} want: - STREAM_START_EVENT - DOCUMENT_START_EVENT - MAPPING_START_EVENT - SCALAR_EVENT - SCALAR_EVENT - SCALAR_EVENT - SCALAR_EVENT - MAPPING_END_EVENT - DOCUMENT_END_EVENT - STREAM_END_EVENT - parse-events: name: Empty input yaml: '' want: - STREAM_START_EVENT - STREAM_END_EVENT # Detailed tests (checking event properties like anchor, tag, value) - parse-events-detailed: name: Explicit document yaml: | --- key: value ... want: - STREAM_START_EVENT - DOCUMENT_START_EVENT: implicit: false - MAPPING_START_EVENT - SCALAR_EVENT: value: key - SCALAR_EVENT: value: value - MAPPING_END_EVENT - DOCUMENT_END_EVENT - STREAM_END_EVENT - parse-events-detailed: name: Anchor and alias yaml: | - &anchor value - *anchor want: - STREAM_START_EVENT - DOCUMENT_START_EVENT - SEQUENCE_START_EVENT - SCALAR_EVENT: anchor: anchor value: value - ALIAS_EVENT: anchor: anchor - SEQUENCE_END_EVENT - DOCUMENT_END_EVENT - STREAM_END_EVENT - parse-events-detailed: name: Tag yaml: | !!str value want: - STREAM_START_EVENT - DOCUMENT_START_EVENT - SCALAR_EVENT: tag: tag:yaml.org,2002:str value: value - DOCUMENT_END_EVENT - STREAM_END_EVENT - parse-events-detailed: name: Scalar value yaml: | key: hello world want: - STREAM_START_EVENT - DOCUMENT_START_EVENT - MAPPING_START_EVENT - SCALAR_EVENT: value: key - SCALAR_EVENT: value: hello world - MAPPING_END_EVENT - DOCUMENT_END_EVENT - STREAM_END_EVENT - parse-events-detailed: name: Implicit document yaml: | value want: - STREAM_START_EVENT - DOCUMENT_START_EVENT: implicit: true - SCALAR_EVENT: value: value - DOCUMENT_END_EVENT - STREAM_END_EVENT - parse-events-detailed: name: Version directive yaml: | %YAML 1.1 --- key: value want: - STREAM_START_EVENT - DOCUMENT_START_EVENT: version-directive: major: 1 minor: 1 - MAPPING_START_EVENT - SCALAR_EVENT: value: key - SCALAR_EVENT: value: value - MAPPING_END_EVENT - DOCUMENT_END_EVENT - STREAM_END_EVENT - parse-events-detailed: name: Tag directive yaml: | %TAG !yaml! tag:yaml.org,2002: --- key: value want: - STREAM_START_EVENT - DOCUMENT_START_EVENT: tag-directives: - handle: '!yaml!' prefix: 'tag:yaml.org,2002:' - MAPPING_START_EVENT - SCALAR_EVENT: value: key - SCALAR_EVENT: value: value - MAPPING_END_EVENT - DOCUMENT_END_EVENT - STREAM_END_EVENT # Structural tests (complex structures) - parse-events: name: Nested structures yaml: | parent: - item1 - item2: nested: value want: - STREAM_START_EVENT - DOCUMENT_START_EVENT - MAPPING_START_EVENT - SCALAR_EVENT - SEQUENCE_START_EVENT - SCALAR_EVENT - MAPPING_START_EVENT - SCALAR_EVENT - MAPPING_START_EVENT - SCALAR_EVENT - SCALAR_EVENT - MAPPING_END_EVENT - MAPPING_END_EVENT - SEQUENCE_END_EVENT - MAPPING_END_EVENT - DOCUMENT_END_EVENT - STREAM_END_EVENT - parse-events: name: Multiple documents yaml: | --- doc1 --- doc2 want: - STREAM_START_EVENT - DOCUMENT_START_EVENT - SCALAR_EVENT - DOCUMENT_END_EVENT - DOCUMENT_START_EVENT - SCALAR_EVENT - DOCUMENT_END_EVENT - STREAM_END_EVENT - parse-events: name: Complex mapping yaml: | ? key1 : value1 ? key2 : value2 want: - STREAM_START_EVENT - DOCUMENT_START_EVENT - MAPPING_START_EVENT - SCALAR_EVENT - SCALAR_EVENT - SCALAR_EVENT - SCALAR_EVENT - MAPPING_END_EVENT - DOCUMENT_END_EVENT - STREAM_END_EVENT - parse-events: name: Flow sequence in mapping yaml: | key: [1, 2, 3] want: - STREAM_START_EVENT - DOCUMENT_START_EVENT - MAPPING_START_EVENT - SCALAR_EVENT - SEQUENCE_START_EVENT - SCALAR_EVENT - SCALAR_EVENT - SCALAR_EVENT - SEQUENCE_END_EVENT - MAPPING_END_EVENT - DOCUMENT_END_EVENT - STREAM_END_EVENT - parse-events: name: Block mapping in sequence yaml: | - key1: value1 - key2: value2 want: - STREAM_START_EVENT - DOCUMENT_START_EVENT - SEQUENCE_START_EVENT - MAPPING_START_EVENT - SCALAR_EVENT - SCALAR_EVENT - MAPPING_END_EVENT - MAPPING_START_EVENT - SCALAR_EVENT - SCALAR_EVENT - MAPPING_END_EVENT - SEQUENCE_END_EVENT - DOCUMENT_END_EVENT - STREAM_END_EVENT - parse-events-detailed: name: Leading document-end before explicit document yaml: | ... ... --- key: value want: - STREAM_START_EVENT - DOCUMENT_START_EVENT: implicit: false - MAPPING_START_EVENT - SCALAR_EVENT: value: key - SCALAR_EVENT: value: value - MAPPING_END_EVENT - DOCUMENT_END_EVENT - STREAM_END_EVENT - parse-events-detailed: name: Leading document-end before implicit document yaml: | ... ... key: value want: - STREAM_START_EVENT - DOCUMENT_START_EVENT: implicit: true - MAPPING_START_EVENT - SCALAR_EVENT: value: key - SCALAR_EVENT: value: value - MAPPING_END_EVENT - DOCUMENT_END_EVENT - STREAM_END_EVENT # Error tests - parse-error: name: Error state yaml: | key: : invalid # Parser API tests (from api.yaml) - api-new: name: New parser with: NewParser test: - nil: [raw-buffer, false] - cap: [raw-buffer, 512] # input_raw_buffer_size - nil: [buffer, false] - cap: [buffer, 1536] # input_buffer_size - api-method: name: Parser set input string with: NewParser byte: true call: [SetInputString, 'key: value'] test: - eq: [input, 'key: value'] - eq: [input-pos, 0] - nil: [read-handler, false] - api-method: name: Parser set encoding with: NewParser call: [SetEncoding, UTF8_ENCODING] test: - eq: [encoding, UTF8_ENCODING] - api-panic: name: Parser set input string twice with: NewParser byte: true init: [SetInputString, first] call: [SetInputString, second] want: must set the input source only once - api-panic: name: Parser set encoding twice with: NewParser init: [SetEncoding, UTF8_ENCODING] call: [SetEncoding, UTF16LE_ENCODING] want: must set the encoding only once - api-delete: name: Parser delete with: NewParser byte: true init: [SetInputString, test] test: - len: [input, 0] - len: [buffer, 0] golang-go.yaml-yaml-v4-4.0.0~rc5/internal/libyaml/testdata/reader.yaml000066400000000000000000000115221521353103300256260ustar00rootroot00000000000000# Copyright 2025 The go-yaml Project Contributors # SPDX-License-Identifier: Apache-2.0 # Reader tests for Parser input handling # # Purpose: Tests Parser methods that handle reading and processing YAML input, # including encoding detection, buffer management, and error handling. # # Format: Test cases use type-as-key format: # - test-type: # name: Test case name # ... # # Test Types: # reader-set-error - Tests setReaderError method # reader-determine-encoding - Tests determineEncoding method # reader-update-raw-buffer - Tests updateRawBuffer method # reader-update-buffer - Tests updateBuffer method # reader-panic - Tests methods that should panic # # Common Keys: # name - Test case name (string, title case) # data - Input bytes (string or hex byte array like [0xEF, 0xBB, 0xBF]) # args - Arguments to pass to method (scalar value or array of values) # init - Setup to perform before test (map of field: value) # * eof: true - Set parser.eof to true before test # want - Expected return value: # * For reader-set-error: boolean (false for error cases) # * For reader-determine-encoding: boolean (defaults to true if omitted) # * For reader-update-raw-buffer: boolean (defaults to true if omitted) # * For reader-update-buffer: boolean (defaults to true if omitted) # * For reader-panic: panic message substring (string) # func - Function to call for panic tests (updateBuffer, etc.) # test - Field checks to perform after method call (list of operator: [field, value]) # Field check operators: # * eq: [field, value] - Equals (supports constants like UTF8_ENCODING or hex like 0x61) # * len-gt: [field, N] - Length greater than N # * gte: [field, N] - Greater than or equal to N # * eq: [buffer-N, X] - Check byte at index N equals X (e.g., eq: [buffer-0, 0x61]) # setReaderError tests - reader-set-error: name: Set reader error args: [test problem, 10, 0x1234] want: false test: - eq: [ErrorType, READER_ERROR] - eq: [Problem, test problem] - eq: [ProblemOffset, 10] - eq: [ProblemValue, 0x1234] # determineEncoding tests - reader-determine-encoding: name: Determine encoding UTF-8 with BOM data: [0xEF, 0xBB, 0xBF, 0x74, 0x65, 0x73, 0x74] test: - eq: [encoding, UTF8_ENCODING] - eq: [raw_buffer_pos, 3] - reader-determine-encoding: name: Determine encoding UTF-16LE data: [0xFF, 0xFE, 0x74, 0x65, 0x73, 0x74] test: - eq: [encoding, UTF16LE_ENCODING] - reader-determine-encoding: name: Determine encoding UTF-16BE data: [0xFE, 0xFF, 0x74, 0x65, 0x73, 0x74] test: - eq: [encoding, UTF16BE_ENCODING] - reader-determine-encoding: name: Determine encoding default UTF-8 data: 'test: value' test: - eq: [encoding, UTF8_ENCODING] # updateRawBuffer tests - reader-update-raw-buffer: name: Update raw buffer with data data: test data test: - len-gt: [raw_buffer, 0] - reader-update-raw-buffer: name: Update raw buffer at EOF data: '' - reader-update-raw-buffer: name: Update raw buffer when already EOF data: '' init: eof: true # updateBuffer tests - reader-update-buffer: name: Update buffer single byte UTF-8 data: abc args: 3 test: - gte: [unread, 3] - eq: [buffer-0, 0x61] - eq: [buffer-1, 0x62] - eq: [buffer-2, 0x63] - reader-update-buffer: name: Update buffer multi-byte UTF-8 data: [0x61, 0xC2, 0xA9, 0x62] args: 3 test: - gte: [unread, 3] - reader-update-buffer: name: Update buffer control character error data: [0x01] args: 1 want: false test: - eq: [ErrorType, READER_ERROR] - reader-update-buffer: name: Update buffer allowed control characters data: [0x09, 0x0A, 0x0D] args: 3 - reader-update-buffer: name: Update buffer UTF-16LE data: [0xFF, 0xFE, 0x61, 0x00] args: 1 test: - eq: [encoding, UTF16LE_ENCODING] - reader-update-buffer: name: Update buffer UTF-16BE data: [0xFE, 0xFF, 0x00, 0x61] args: 1 test: - eq: [encoding, UTF16BE_ENCODING] - reader-update-buffer: name: Update buffer surrogate pair UTF-16 data: [0xFF, 0xFE, 0x3D, 0xD8, 0x4A, 0xDC] args: 1 test: - gte: [unread, 1] - reader-update-buffer: name: Update buffer invalid surrogate pair data: [0xFF, 0xFE, 0x3D, 0xD8, 0x00, 0x00] args: 1 want: false test: - eq: [ErrorType, READER_ERROR] - reader-update-buffer: name: Update buffer unexpected low surrogate data: [0xFF, 0xFE, 0x00, 0xDC, 0x00, 0x00] args: 1 want: false test: - eq: [ErrorType, READER_ERROR] # Panic tests - reader-panic: name: Update buffer without read handler func: updateBuffer args: 1 want: read handler must be set golang-go.yaml-yaml-v4-4.0.0~rc5/internal/libyaml/testdata/representer.yaml000066400000000000000000000042641521353103300267270ustar00rootroot00000000000000# Copyright 2025 The go-yaml Project Contributors # SPDX-License-Identifier: Apache-2.0 # Representer stage test cases # # Purpose: Tests the Representer's ability to convert Go values to YAML nodes # with proper tags and structure. # # Test Types: # represent-scalar - Tests representing basic scalar types # represent-collection - Tests representing maps and sequences # # Field Descriptions: # name: Human-readable test name # from: Input Go value to represent (int, string, bool, float, null, array, map, binary string) # want: Expected node structure after representation # kind: Expected node kind (Scalar, Sequence, Mapping) # tag: Expected YAML tag (e.g., "!!int", "!!str", "!!binary") # value: Expected scalar value (for Scalar nodes) # content: Expected child nodes (array of node objects for Sequence/Mapping nodes) - represent-scalar: name: Integer to node from: 42 want: kind: Scalar tag: '!!int' value: '42' - represent-scalar: name: String to node from: hello want: kind: Scalar tag: '!!str' value: hello - represent-scalar: name: Boolean true to node from: true want: kind: Scalar tag: '!!bool' value: 'true' - represent-scalar: name: Float to node from: 3.14 want: kind: Scalar tag: '!!float' value: '3.14' - represent-scalar: name: Null to node from: null want: kind: Scalar tag: '!!null' value: 'null' # - represent-scalar: # name: Binary data gets binary tag # from: "\xFF\xFE" # want: # kind: Scalar # tag: '!!binary' - represent-collection: name: Slice to sequence node from: [1, 2, 3] want: kind: Sequence tag: '!!seq' content: - kind: Scalar tag: '!!int' value: '1' - kind: Scalar tag: '!!int' value: '2' - kind: Scalar tag: '!!int' value: '3' - represent-collection: name: Map to mapping node from: {a: 1} want: kind: Mapping tag: '!!map' content: - kind: Scalar tag: '!!str' value: a - kind: Scalar tag: '!!int' value: '1' golang-go.yaml-yaml-v4-4.0.0~rc5/internal/libyaml/testdata/resolver.yaml000066400000000000000000000077161521353103300262370ustar00rootroot00000000000000# Copyright 2025 The go-yaml Project Contributors # SPDX-License-Identifier: Apache-2.0 # Resolver stage test cases # # Purpose: Tests the Resolver's ability to assign default tags and infer types # # Test Types: # resolve-default - Tests default tag assignment (quoted scalars, collections) # resolve-infer - Tests inferring tags from plain scalar values # resolve-preserve - Tests that tagged/styled scalars keep their tags # # Field Descriptions: # name: Human-readable test name # node: Input node to resolve # kind: Node kind (Scalar, Sequence, Mapping) # value: Scalar value # tag: Pre-existing tag (for preserve tests) # style: Node style (for preserve tests) # want: Expected results after resolution # tag: Expected tag after resolution - resolve-default: name: Default single quoted scalar to str node: kind: Scalar value: '123' style: Single want: tag: '!!str' - resolve-default: name: Default double quoted scalar to str node: kind: Scalar value: 'hello' style: Double want: tag: '!!str' - resolve-default: name: Default literal scalar to str node: kind: Scalar value: 'line 1\nline 2\n' style: Literal want: tag: '!!str' - resolve-default: name: Default folded scalar to str node: kind: Scalar value: 'line 1 line 2\n' style: Folded want: tag: '!!str' - resolve-default: name: Default sequence to seq node: kind: Sequence want: tag: '!!seq' - resolve-default: name: Default mapping to map node: kind: Mapping want: tag: '!!map' - resolve-infer: name: Infer int tag from decimal node: value: '123' want: tag: '!!int' - resolve-infer: name: Infer int tag from negative node: value: '-42' want: tag: '!!int' - resolve-infer: name: Infer int tag from hex node: value: '0x1A' want: tag: '!!int' - resolve-infer: name: Infer int tag from octal node: value: '0o17' want: tag: '!!int' - resolve-infer: name: Infer float tag from decimal node: value: '3.14' want: tag: '!!float' - resolve-infer: name: Infer float tag from positive infinity node: value: '.inf' want: tag: '!!float' - resolve-infer: name: Infer float tag from negative infinity node: value: '-.inf' want: tag: '!!float' - resolve-infer: name: Infer float tag from NaN node: value: '.nan' want: tag: '!!float' - resolve-infer: name: Infer bool tag from true node: value: 'true' want: tag: '!!bool' - resolve-infer: name: Infer bool tag from false node: value: 'false' want: tag: '!!bool' - resolve-infer: name: Infer bool tag from True node: value: 'True' want: tag: '!!bool' - resolve-infer: name: Infer bool tag from FALSE node: value: 'FALSE' want: tag: '!!bool' - resolve-infer: name: Infer null tag from null node: value: 'null' want: tag: '!!null' - resolve-infer: name: Infer null tag from tilde node: value: '~' want: tag: '!!null' - resolve-infer: name: Infer null tag from empty node: value: '' want: tag: '!!null' - resolve-infer: name: Infer timestamp tag from date node: value: '2006-01-02' want: tag: '!!timestamp' - resolve-infer: name: Infer str tag from plain text node: value: 'hello world' want: tag: '!!str' - resolve-infer: name: Infer str tag from number-like string node: value: '123abc' want: tag: '!!str' - resolve-preserve: name: Preserve explicit str tag node: value: '123' tag: '!!str' want: tag: '!!str' - resolve-preserve: name: Preserve custom tag node: value: '123' tag: '!custom' want: tag: '!custom' golang-go.yaml-yaml-v4-4.0.0~rc5/internal/libyaml/testdata/scanner.yaml000066400000000000000000000564301521353103300260240ustar00rootroot00000000000000# Copyright 2025 The go-yaml Project Contributors # SPDX-License-Identifier: Apache-2.0 # Scanner test cases for libyaml scanner # # Purpose: Tests the YAML scanner/tokenizer that converts YAML text into tokens. # # Format: Test cases use type-as-key format: # - test-type: # name: Test case name # ... # # Test Types: # scan-tokens - Verifies token sequence only # scan-tokens-detailed - Verifies complete token properties (value, style, etc.) # scan-error - Validates error detection for invalid input # # Common Keys: # name - Test case name (string) # yaml - Input YAML string to scan # want - Expected result (format varies by test type): # * For scan-error: boolean (defaults to true if omitted; set to false if no error expected) # * For scan-tokens: list of token type names (scalars or type-as-key format) # * For scan-tokens-detailed: list of token specifications (type-as-key format) # # Token Specification Format (for want lists): # Simplified (type only): - TOKEN_TYPE # Detailed (with fields): - TOKEN_TYPE: # value: ... # style: ... # Simple token type tests - scan-tokens: name: Simple scalar yaml: |- hello want: - STREAM_START_TOKEN - SCALAR_TOKEN - STREAM_END_TOKEN - scan-tokens: name: Simple mapping yaml: |- key: value want: - STREAM_START_TOKEN - BLOCK_MAPPING_START_TOKEN - KEY_TOKEN - SCALAR_TOKEN - VALUE_TOKEN - SCALAR_TOKEN - BLOCK_END_TOKEN - STREAM_END_TOKEN - scan-tokens: name: Block sequence yaml: | - item1 - item2 want: - STREAM_START_TOKEN - BLOCK_SEQUENCE_START_TOKEN - BLOCK_ENTRY_TOKEN - SCALAR_TOKEN - BLOCK_ENTRY_TOKEN - SCALAR_TOKEN - BLOCK_END_TOKEN - STREAM_END_TOKEN - scan-tokens: name: Flow sequence yaml: |- [1, 2, 3] want: - STREAM_START_TOKEN - FLOW_SEQUENCE_START_TOKEN - SCALAR_TOKEN - FLOW_ENTRY_TOKEN - SCALAR_TOKEN - FLOW_ENTRY_TOKEN - SCALAR_TOKEN - FLOW_SEQUENCE_END_TOKEN - STREAM_END_TOKEN - scan-tokens: name: Flow mapping yaml: |- {a: 1, b: 2} want: - STREAM_START_TOKEN - FLOW_MAPPING_START_TOKEN - KEY_TOKEN - SCALAR_TOKEN - VALUE_TOKEN - SCALAR_TOKEN - FLOW_ENTRY_TOKEN - KEY_TOKEN - SCALAR_TOKEN - VALUE_TOKEN - SCALAR_TOKEN - FLOW_MAPPING_END_TOKEN - STREAM_END_TOKEN - scan-tokens: name: Document markers yaml: | --- key: value ... want: - STREAM_START_TOKEN - DOCUMENT_START_TOKEN - BLOCK_MAPPING_START_TOKEN - KEY_TOKEN - SCALAR_TOKEN - VALUE_TOKEN - SCALAR_TOKEN - BLOCK_END_TOKEN - DOCUMENT_END_TOKEN - STREAM_END_TOKEN - scan-tokens: name: Empty input yaml: |- want: - STREAM_START_TOKEN - STREAM_END_TOKEN - scan-tokens: name: Complex nesting yaml: | parent: - item1 - nested: key: value want: - STREAM_START_TOKEN - BLOCK_MAPPING_START_TOKEN - KEY_TOKEN - SCALAR_TOKEN - VALUE_TOKEN - BLOCK_SEQUENCE_START_TOKEN - BLOCK_ENTRY_TOKEN - SCALAR_TOKEN - BLOCK_ENTRY_TOKEN - BLOCK_MAPPING_START_TOKEN - KEY_TOKEN - SCALAR_TOKEN - VALUE_TOKEN - BLOCK_MAPPING_START_TOKEN - KEY_TOKEN - SCALAR_TOKEN - VALUE_TOKEN - SCALAR_TOKEN - BLOCK_END_TOKEN - BLOCK_END_TOKEN - BLOCK_END_TOKEN - BLOCK_END_TOKEN - STREAM_END_TOKEN # Detailed token tests (checking token properties) - scan-tokens-detailed: name: Anchor and alias yaml: | - &anchor value - *anchor want: - STREAM_START_TOKEN - BLOCK_SEQUENCE_START_TOKEN - BLOCK_ENTRY_TOKEN - ANCHOR_TOKEN: value: anchor - SCALAR_TOKEN: value: value - BLOCK_ENTRY_TOKEN - ALIAS_TOKEN: value: anchor - BLOCK_END_TOKEN - STREAM_END_TOKEN - scan-tokens-detailed: name: Tag yaml: |- !!str value want: - STREAM_START_TOKEN - TAG_TOKEN: value: '!!' - SCALAR_TOKEN: value: value - STREAM_END_TOKEN - scan-tokens-detailed: name: Single quoted scalar yaml: |- 'hello world' want: - STREAM_START_TOKEN - SCALAR_TOKEN: style: SINGLE_QUOTED_SCALAR_STYLE value: hello world - STREAM_END_TOKEN - scan-tokens-detailed: name: Double quoted scalar yaml: |- "hello world" want: - STREAM_START_TOKEN - SCALAR_TOKEN: style: DOUBLE_QUOTED_SCALAR_STYLE value: hello world - STREAM_END_TOKEN - scan-tokens-detailed: name: Literal scalar yaml: | key: | line1 line2 want: - STREAM_START_TOKEN - BLOCK_MAPPING_START_TOKEN - KEY_TOKEN - SCALAR_TOKEN: value: key - VALUE_TOKEN - SCALAR_TOKEN: style: LITERAL_SCALAR_STYLE value: "line1\nline2\n" - BLOCK_END_TOKEN - STREAM_END_TOKEN - scan-tokens-detailed: name: Folded scalar yaml: | key: > folded text want: - STREAM_START_TOKEN - BLOCK_MAPPING_START_TOKEN - KEY_TOKEN - SCALAR_TOKEN: value: key - VALUE_TOKEN - SCALAR_TOKEN: style: FOLDED_SCALAR_STYLE value: "folded text\n" - BLOCK_END_TOKEN - STREAM_END_TOKEN - scan-tokens-detailed: name: Version directive yaml: | %YAML 1.1 --- key: value want: - STREAM_START_TOKEN - VERSION_DIRECTIVE_TOKEN - DOCUMENT_START_TOKEN - BLOCK_MAPPING_START_TOKEN - KEY_TOKEN - SCALAR_TOKEN: value: key - VALUE_TOKEN - SCALAR_TOKEN: value: value - BLOCK_END_TOKEN - STREAM_END_TOKEN - scan-tokens-detailed: name: Tag directive yaml: | %TAG !yaml! tag:yaml.org,2002: --- key: value want: - STREAM_START_TOKEN - TAG_DIRECTIVE_TOKEN - DOCUMENT_START_TOKEN - BLOCK_MAPPING_START_TOKEN - KEY_TOKEN - SCALAR_TOKEN: value: key - VALUE_TOKEN - SCALAR_TOKEN: value: value - BLOCK_END_TOKEN - STREAM_END_TOKEN - scan-tokens-detailed: name: Escape sequences yaml: |- "hello\nworld" want: - STREAM_START_TOKEN - SCALAR_TOKEN: style: DOUBLE_QUOTED_SCALAR_STYLE value: "hello\nworld" - STREAM_END_TOKEN - scan-tokens-detailed: name: Flow mapping colon followed by comma, bracket, or brace yaml: |- {a:,b: [c:],d: {e:}} want: - STREAM_START_TOKEN - FLOW_MAPPING_START_TOKEN - KEY_TOKEN - SCALAR_TOKEN: value: a - VALUE_TOKEN - FLOW_ENTRY_TOKEN - KEY_TOKEN - SCALAR_TOKEN: value: b - VALUE_TOKEN - FLOW_SEQUENCE_START_TOKEN - KEY_TOKEN - SCALAR_TOKEN: value: c - VALUE_TOKEN - FLOW_SEQUENCE_END_TOKEN - FLOW_ENTRY_TOKEN - KEY_TOKEN - SCALAR_TOKEN: value: d - VALUE_TOKEN - FLOW_MAPPING_START_TOKEN - KEY_TOKEN - SCALAR_TOKEN: value: e - VALUE_TOKEN - FLOW_MAPPING_END_TOKEN - FLOW_MAPPING_END_TOKEN - STREAM_END_TOKEN # Error test - scan-error: name: Invalid character yaml: "\x01" # Flow indicator error tests - scan-error: name: Comma in tag yaml: "!foo,bar value" like: found character ',' that is not allowed in a YAML tag - scan-error: name: Left bracket in tag yaml: "!foo[bar value" like: found character '\[' that is not allowed in a YAML tag - scan-error: name: Right bracket in tag yaml: "!foo]bar value" like: found character '\]' that is not allowed in a YAML tag - scan-error: name: Left brace in tag yaml: "!foo{bar value" like: found character '\{' that is not allowed in a YAML tag - scan-error: name: Right brace in tag yaml: "!foo}bar value" like: found character '\}' that is not allowed in a YAML tag - scan-error: name: Hash in tag yaml: "!foo#bar value" like: found character '#' that is not allowed in a YAML tag - scan-error: name: Double quote in tag yaml: '!foo"bar value' like: found character '"' that is not allowed in a YAML tag - scan-error: name: Less than in tag yaml: "!foobar value" like: found character '>' that is not allowed in a YAML tag - scan-error: name: Backslash in tag yaml: '!foo\bar value' like: found character '\\' that is not allowed in a YAML tag # Copyright 2025 The go-yaml Project Contributors # SPDX-License-Identifier: Apache-2.0 # Character classification and utility function tests # # Purpose: Tests internal character classification and conversion functions used # throughout the YAML parser and emitter. # # Format: Test cases use type-as-key format: # - test-type: # name: Test case name # ... # # Test Types: # char-predicate - Tests is* functions that return boolean # char-convert - Tests as* and width functions that return integer # # Common Keys: # name - Test case name (string) # func - Function to call (isAlpha, isDigit, asHex, width, etc.) # data - Input bytes (string or hex bytes like [0x00]) # index - Byte index to test (integer, defaults to 0) # want - Expected result: # * For char-predicate: boolean (defaults to true if omitted; set to false if needed) # * For char-convert: integer (required) # isAlpha tests - char-predicate: name: Is alpha - letters func: isAlpha data: abc - char-predicate: name: Is alpha - uppercase func: isAlpha data: ABC - char-predicate: name: Is alpha - digits func: isAlpha data: '123' - char-predicate: name: Is alpha - underscore func: isAlpha data: _- - char-predicate: name: Is alpha - dash at index 1 func: isAlpha data: _- index: 1 - char-predicate: name: Is alpha - space func: isAlpha data: ' ' want: false - char-predicate: name: Is alpha - exclamation func: isAlpha data: '!' want: false - char-predicate: name: Is alpha - at sign func: isAlpha data: '@' want: false # isFlowIndicator tests - char-predicate: name: Is flow indicator - left bracket func: isFlowIndicator data: '[' - char-predicate: name: Is flow indicator - right bracket func: isFlowIndicator data: ']' - char-predicate: name: Is flow indicator - left brace func: isFlowIndicator data: '{' - char-predicate: name: Is flow indicator - right brace func: isFlowIndicator data: '}' - char-predicate: name: Is flow indicator - comma func: isFlowIndicator data: ',' - char-predicate: name: Is flow indicator - letter func: isFlowIndicator data: a want: false - char-predicate: name: Is flow indicator - colon func: isFlowIndicator data: ':' want: false # isAnchorChar tests - char-predicate: name: Is anchor char - letters func: isAnchorChar data: abc - char-predicate: name: Is anchor char - digits func: isAnchorChar data: '123' - char-predicate: name: Is anchor char - underscore dash func: isAnchorChar data: _- - char-predicate: name: Is anchor char - colon func: isAnchorChar data: ':' want: false - char-predicate: name: Is anchor char - left bracket func: isAnchorChar data: '[' want: false - char-predicate: name: Is anchor char - space func: isAnchorChar data: ' ' want: false - char-predicate: name: Is anchor char - newline func: isAnchorChar data: "\n" want: false - char-predicate: name: Is anchor char - BOM func: isAnchorChar data: [0xEF, 0xBB, 0xBF] want: false # isColon tests - char-predicate: name: Is colon - colon func: isColon data: ':' - char-predicate: name: Is colon - letter func: isColon data: a want: false # isDigit tests - char-predicate: name: Is digit - zero func: isDigit data: '0' - char-predicate: name: Is digit - five func: isDigit data: '5' - char-predicate: name: Is digit - nine func: isDigit data: '9' - char-predicate: name: Is digit - letter func: isDigit data: a want: false - char-predicate: name: Is digit - space func: isDigit data: ' ' want: false # isHex tests - char-predicate: name: Is hex - zero func: isHex data: '0' - char-predicate: name: Is hex - nine func: isHex data: '9' - char-predicate: name: Is hex - uppercase A func: isHex data: A - char-predicate: name: Is hex - uppercase F func: isHex data: F - char-predicate: name: Is hex - lowercase a func: isHex data: a - char-predicate: name: Is hex - lowercase f func: isHex data: f - char-predicate: name: Is hex - uppercase G func: isHex data: G want: false - char-predicate: name: Is hex - lowercase g func: isHex data: g want: false # isASCII tests - char-predicate: name: Is ASCII - letter func: isASCII data: a - char-predicate: name: Is ASCII - 0x7F func: isASCII data: [0x7F] - char-predicate: name: Is ASCII - 0x80 func: isASCII data: [0x80] want: false - char-predicate: name: Is ASCII - 0xFF func: isASCII data: [0xFF] want: false # isPrintable tests - char-predicate: name: Is printable - LF func: isPrintable data: [0x0A] - char-predicate: name: Is printable - space func: isPrintable data: [0x20] - char-predicate: name: Is printable - tilde func: isPrintable data: [0x7E] - char-predicate: name: Is printable - nbsp func: isPrintable data: [0xC2, 0xA0] - char-predicate: name: Is printable - null func: isPrintable data: [0x00] want: false - char-predicate: name: Is printable - control char func: isPrintable data: [0x19] want: false # isZeroChar tests - char-predicate: name: Is zero char - null func: isZeroChar data: [0x00] - char-predicate: name: Is zero char - letter func: isZeroChar data: a want: false # isBOM tests - char-predicate: name: Is BOM - UTF-8 BOM func: isBOM data: [0xEF, 0xBB, 0xBF] - char-predicate: name: Is BOM - regular text func: isBOM data: abc want: false # isSpace tests - char-predicate: name: Is space - space func: isSpace data: ' ' - char-predicate: name: Is space - letter func: isSpace data: a want: false # isTab tests - char-predicate: name: Is tab - tab func: isTab data: "\t" - char-predicate: name: Is tab - space func: isTab data: ' ' want: false # isBlank tests - char-predicate: name: Is blank - space func: isBlank data: ' ' - char-predicate: name: Is blank - tab func: isBlank data: "\t" - char-predicate: name: Is blank - letter func: isBlank data: a want: false - char-predicate: name: Is blank - newline func: isBlank data: "\n" want: false # isLineBreak tests - char-predicate: name: Is line break - CR func: isLineBreak data: "\r" - char-predicate: name: Is line break - LF func: isLineBreak data: "\n" - char-predicate: name: Is line break - NEL func: isLineBreak data: [0xC2, 0x85] - char-predicate: name: Is line break - LS func: isLineBreak data: [0xE2, 0x80, 0xA8] - char-predicate: name: Is line break - PS func: isLineBreak data: [0xE2, 0x80, 0xA9] - char-predicate: name: Is line break - letter func: isLineBreak data: a want: false - char-predicate: name: Is line break - space func: isLineBreak data: ' ' want: false # isCRLF tests - char-predicate: name: Is CRLF - CRLF func: isCRLF data: "\r\n" - char-predicate: name: Is CRLF - LF only func: isCRLF data: [0x0A, 0x00] want: false - char-predicate: name: Is CRLF - CR only func: isCRLF data: [0x0D, 0x00] want: false # isBreakOrZero tests - char-predicate: name: Is break or zero - CR func: isBreakOrZero data: "\r" - char-predicate: name: Is break or zero - LF func: isBreakOrZero data: "\n" - char-predicate: name: Is break or zero - null func: isBreakOrZero data: [0x00] - char-predicate: name: Is break or zero - NEL func: isBreakOrZero data: [0xC2, 0x85] - char-predicate: name: Is break or zero - letter func: isBreakOrZero data: a want: false # isSpaceOrZero tests - char-predicate: name: Is space or zero - space func: isSpaceOrZero data: ' ' - char-predicate: name: Is space or zero - CR func: isSpaceOrZero data: "\r" - char-predicate: name: Is space or zero - LF func: isSpaceOrZero data: "\n" - char-predicate: name: Is space or zero - null func: isSpaceOrZero data: [0x00] - char-predicate: name: Is space or zero - letter func: isSpaceOrZero data: a want: false # isBlankOrZero tests - char-predicate: name: Is blank or zero - space func: isBlankOrZero data: ' ' - char-predicate: name: Is blank or zero - tab func: isBlankOrZero data: "\t" - char-predicate: name: Is blank or zero - CR func: isBlankOrZero data: "\r" - char-predicate: name: Is blank or zero - LF func: isBlankOrZero data: "\n" - char-predicate: name: Is blank or zero - null func: isBlankOrZero data: [0x00] - char-predicate: name: Is blank or zero - letter func: isBlankOrZero data: a want: false # Converter functions - asDigit tests - char-convert: name: As digit - zero func: asDigit data: '0' want: 0 - char-convert: name: As digit - five func: asDigit data: '5' want: 5 - char-convert: name: As digit - nine func: asDigit data: '9' want: 9 # asHex tests - char-convert: name: As hex - zero func: asHex data: '0' want: 0 - char-convert: name: As hex - nine func: asHex data: '9' want: 9 - char-convert: name: As hex - uppercase A func: asHex data: A want: 10 - char-convert: name: As hex - uppercase F func: asHex data: F want: 15 - char-convert: name: As hex - lowercase a func: asHex data: a want: 10 - char-convert: name: As hex - lowercase f func: asHex data: f want: 15 # width tests - char-convert: name: Width - 0x00 func: width data: [0x00] want: 1 - char-convert: name: Width - 0x7F func: width data: [0x7F] want: 1 - char-convert: name: Width - 0xC0 func: width data: [0xC0] want: 2 - char-convert: name: Width - 0xDF func: width data: [0xDF] want: 2 - char-convert: name: Width - 0xE0 func: width data: [0xE0] want: 3 - char-convert: name: Width - 0xEF func: width data: [0xEF] want: 3 - char-convert: name: Width - 0xF0 func: width data: [0xF0] want: 4 - char-convert: name: Width - 0xF7 func: width data: [0xF7] want: 4 - char-convert: name: Width - 0xF8 func: width data: [0xF8] want: 0 # isTagURIChar tests - char-predicate: name: Is tag URI char - alphanumeric (non-verbatim) func: isTagURIChar data: abc123 args: [false] - char-predicate: name: Is tag URI char - underscore (non-verbatim) func: isTagURIChar data: _ args: [false] - char-predicate: name: Is tag URI char - dash (non-verbatim) func: isTagURIChar data: '-' args: [false] - char-predicate: name: Is tag URI char - semicolon (non-verbatim) func: isTagURIChar data: ; args: [false] - char-predicate: name: Is tag URI char - slash (non-verbatim) func: isTagURIChar data: / args: [false] - char-predicate: name: Is tag URI char - question mark (non-verbatim) func: isTagURIChar data: '?' args: [false] - char-predicate: name: Is tag URI char - colon (non-verbatim) func: isTagURIChar data: ':' args: [false] - char-predicate: name: Is tag URI char - at sign (non-verbatim) func: isTagURIChar data: '@' args: [false] - char-predicate: name: Is tag URI char - ampersand (non-verbatim) func: isTagURIChar data: '&' args: [false] - char-predicate: name: Is tag URI char - equals (non-verbatim) func: isTagURIChar data: = args: [false] - char-predicate: name: Is tag URI char - plus (non-verbatim) func: isTagURIChar data: + args: [false] - char-predicate: name: Is tag URI char - dollar (non-verbatim) func: isTagURIChar data: $ args: [false] - char-predicate: name: Is tag URI char - dot (non-verbatim) func: isTagURIChar data: . args: [false] - char-predicate: name: Is tag URI char - exclamation (non-verbatim) func: isTagURIChar data: '!' args: [false] - char-predicate: name: Is tag URI char - tilde (non-verbatim) func: isTagURIChar data: '~' args: [false] - char-predicate: name: Is tag URI char - asterisk (non-verbatim) func: isTagURIChar data: '*' args: [false] - char-predicate: name: Is tag URI char - single quote (non-verbatim) func: isTagURIChar data: "'" args: [false] - char-predicate: name: Is tag URI char - left paren (non-verbatim) func: isTagURIChar data: ( args: [false] - char-predicate: name: Is tag URI char - right paren (non-verbatim) func: isTagURIChar data: ) args: [false] - char-predicate: name: Is tag URI char - percent (non-verbatim) func: isTagURIChar data: '%' args: [false] - char-predicate: name: Is tag URI char - comma (non-verbatim) func: isTagURIChar data: ',' args: [false] want: false - char-predicate: name: Is tag URI char - left bracket (non-verbatim) func: isTagURIChar data: '[' args: [false] want: false - char-predicate: name: Is tag URI char - right bracket (non-verbatim) func: isTagURIChar data: ']' args: [false] want: false - char-predicate: name: Is tag URI char - left brace (non-verbatim) func: isTagURIChar data: '{' args: [false] want: false - char-predicate: name: Is tag URI char - right brace (non-verbatim) func: isTagURIChar data: '}' args: [false] want: false - char-predicate: name: Is tag URI char - comma (verbatim) func: isTagURIChar data: ',' args: [true] - char-predicate: name: Is tag URI char - left bracket (verbatim) func: isTagURIChar data: '[' args: [true] - char-predicate: name: Is tag URI char - right bracket (verbatim) func: isTagURIChar data: ']' args: [true] - char-predicate: name: Is tag URI char - left brace (verbatim) func: isTagURIChar data: '{' args: [true] - char-predicate: name: Is tag URI char - right brace (verbatim) func: isTagURIChar data: '}' args: [true] - char-predicate: name: Is tag URI char - space (non-verbatim) func: isTagURIChar data: ' ' args: [false] want: false - char-predicate: name: Is tag URI char - newline (non-verbatim) func: isTagURIChar data: [0x0A] args: [false] want: false - char-predicate: name: Is tag URI char - hash (non-verbatim) func: isTagURIChar data: '#' args: [false] want: false golang-go.yaml-yaml-v4-4.0.0~rc5/internal/libyaml/testdata/serializer.yaml000066400000000000000000000063421521353103300265410ustar00rootroot00000000000000# Copyright 2025 The go-yaml Project Contributors # SPDX-License-Identifier: Apache-2.0 # Serializer stage test cases # # Purpose: Tests the Serializer's ability to convert nodes to YAML output # # Test Types: # serialize-scalar - Tests serializing scalar nodes # serialize-collection - Tests serializing maps and sequences # serialize-style - Tests different YAML styles (flow, literal, etc) # # Field Descriptions: # name: Human-readable test name # node: Input node structure to serialize # kind: Node kind (Document, Scalar, Sequence, Mapping) # value: Scalar value # style: Style (Single, Double, Flow, etc.) # content: Child nodes (array of node objects) # indent: Indentation width (2 or 4 spaces) # want: Expected YAML output (literal block scalar) - serialize-scalar: name: Plain string node: kind: Document content: - kind: Scalar value: hello want: | hello - serialize-scalar: name: Integer node: kind: Document content: - kind: Scalar value: '42' want: | 42 - serialize-scalar: name: Single quoted string node: kind: Document content: - kind: Scalar value: '123' style: Single want: | '123' - serialize-scalar: name: Double quoted string node: kind: Document content: - kind: Scalar value: 'hello' style: Double want: | "hello" - serialize-collection: name: Sequence node: kind: Document content: - kind: Sequence content: - kind: Scalar value: a - kind: Scalar value: b want: | - a - b - serialize-collection: name: Mapping node: kind: Document content: - kind: Mapping content: - kind: Scalar value: key - kind: Scalar value: value want: | key: value - serialize-style: name: Flow sequence node: kind: Document content: - kind: Sequence style: Flow content: - kind: Scalar value: a - kind: Scalar value: b want: | [a, b] - serialize-style: name: Flow mapping node: kind: Document content: - kind: Mapping style: Flow content: - kind: Scalar value: key - kind: Scalar value: value want: | {key: value} - serialize-collection: name: Nested mapping with 2-space indent indent: 2 node: kind: Document content: - kind: Mapping content: - kind: Scalar value: outer - kind: Mapping content: - kind: Scalar value: inner - kind: Scalar value: value want: | outer: inner: value - serialize-collection: name: Nested mapping with 4-space indent indent: 4 node: kind: Document content: - kind: Mapping content: - kind: Scalar value: outer - kind: Mapping content: - kind: Scalar value: inner - kind: Scalar value: value want: | outer: inner: value golang-go.yaml-yaml-v4-4.0.0~rc5/internal/libyaml/testdata/writer.yaml000066400000000000000000000037271521353103300257100ustar00rootroot00000000000000# Copyright 2025 The go-yaml Project Contributors # SPDX-License-Identifier: Apache-2.0 # Writer tests for Emitter output handling # # Purpose: Tests Emitter methods that handle writing and flushing output, # including buffer management and write handlers. # # Format: Test cases use type-as-key format: # - test-type: # name: Test case name # ... # # Test Types: # writer-flush - Tests flush method with various scenarios # writer-error - Tests flush with error conditions # writer-panic - Tests methods that should panic # # Common Keys: # name - Test case name (string, title case) # type - Output handler type (string | writer | error-writer) # data - Data to write to buffer (string or hex byte array) # data2 - Second data chunk for multi-flush tests (optional) # func - Function to call for panic tests (flush, etc.) # want - Expected result: # * For writer-flush: expected output string # * For writer-error: error message substring # * For writer-panic: panic message substring # test - Field checks to perform after flush (list of operator: [field, value]) # Field check operators: # * eq: [field, value] - Equals # * len: [field, N] - Length equals N # # flush tests - writer-flush: name: Flush empty buffer type: string want: '' test: - eq: [buffer_pos, 0] - writer-flush: name: Flush with data type: string data: test data want: test data test: - eq: [buffer_pos, 0] - writer-flush: name: Flush multiple times type: string data: first data2: second want: firstsecond - writer-flush: name: Flush with writer type: writer data: test data want: test data # error tests - writer-error: name: Flush with write error type: error-writer data: test want: write error # panic tests - writer-panic: name: Flush without write handler func: flush data: test want: write handler not set golang-go.yaml-yaml-v4-4.0.0~rc5/internal/libyaml/testdata/yaml.yaml000066400000000000000000000063531521353103300253340ustar00rootroot00000000000000# Copyright 2025 The go-yaml Project Contributors # SPDX-License-Identifier: Apache-2.0 # YAML helper function tests # # Purpose: Tests utility functions and type conversions (String() methods, style accessors). # # Format: Test cases use type-as-key format: # - test-type: # name: Test case name # ... # # Test Types: # enum-string - Tests String() methods of enum types (converts enum value to string) # style-accessor - Tests style accessor methods on Event type # # Common Keys: # name - Test case name (string) # enum - Enum type and value to test (for enum-string): [Type, VALUE] # Type: ScalarStyle | TokenType | EventType | ParserState # VALUE: integer or constant name # want - Expected result (for enum-string): string representing the expected String() output # test - Style accessor method and value (for style-accessor): [Method, STYLE] # Method: ScalarStyle | SequenceStyle | MappingStyle # STYLE: style constant name # ScalarStyle.String() tests - enum-string: name: Scalar style plain enum: [ScalarStyle, PLAIN_SCALAR_STYLE] want: Plain - enum-string: name: Scalar style single enum: [ScalarStyle, SINGLE_QUOTED_SCALAR_STYLE] want: Single - enum-string: name: Scalar style double enum: [ScalarStyle, DOUBLE_QUOTED_SCALAR_STYLE] want: Double - enum-string: name: Scalar style literal enum: [ScalarStyle, LITERAL_SCALAR_STYLE] want: Literal - enum-string: name: Scalar style folded enum: [ScalarStyle, FOLDED_SCALAR_STYLE] want: Folded - enum-string: name: Scalar style any enum: [ScalarStyle, ANY_SCALAR_STYLE] want: '' - enum-string: name: Scalar style unknown enum: [ScalarStyle, 99] want: '' # TokenType.String() tests - enum-string: name: Token no token enum: [TokenType, NO_TOKEN] want: NO_TOKEN - enum-string: name: Token stream start enum: [TokenType, STREAM_START_TOKEN] want: STREAM_START_TOKEN - enum-string: name: Token scalar enum: [TokenType, SCALAR_TOKEN] want: SCALAR_TOKEN - enum-string: name: Token unknown enum: [TokenType, 99] want: # EventType.String() tests - enum-string: name: Event no event enum: [EventType, NO_EVENT] want: none - enum-string: name: Event stream start enum: [EventType, STREAM_START_EVENT] want: stream start - enum-string: name: Event scalar enum: [EventType, SCALAR_EVENT] want: scalar - enum-string: name: Event unknown enum: [EventType, 99] want: unknown event 99 # ParserState.String() tests - enum-string: name: Parser state stream start enum: [ParserState, PARSE_STREAM_START_STATE] want: PARSE_STREAM_START_STATE - enum-string: name: Parser state end enum: [ParserState, PARSE_END_STATE] want: PARSE_END_STATE - enum-string: name: Parser state unknown enum: [ParserState, 99] want: # Event style accessor tests - style-accessor: name: Event scalar style test: [ScalarStyle, DOUBLE_QUOTED_SCALAR_STYLE] - style-accessor: name: Event sequence style test: [SequenceStyle, FLOW_SEQUENCE_STYLE] - style-accessor: name: Event mapping style test: [MappingStyle, BLOCK_MAPPING_STYLE] golang-go.yaml-yaml-v4-4.0.0~rc5/internal/libyaml/testdata_test.go000066400000000000000000001271421521353103300250740ustar00rootroot00000000000000// Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Tests for YAML test data loading. // Verifies test data loading utilities and scalar coercion functions. package libyaml import ( "bytes" "errors" "fmt" "io" "os" "path/filepath" "reflect" "runtime" "strconv" "strings" "testing" "go.yaml.in/yaml/v4/internal/testutil/assert" "go.yaml.in/yaml/v4/internal/testutil/datatest" ) // NodeSpec describes an input node for pipeline stage tests. // Used in nested test format for representer, desolver, and serializer tests. type NodeSpec struct { Tag string `yaml:"tag"` // YAML tag (e.g., "!!int", "!!str") Value string `yaml:"value"` // Scalar value Kind string `yaml:"kind"` // Node kind: Scalar, Mapping, Sequence, Document Style string `yaml:"style"` // Style: Tagged, SingleQuoted, DoubleQuoted, Flow Content any `yaml:"content"` // Nested content for collections } // WantSpec describes expected test results for pipeline stage tests. // Used in nested test format for representer, desolver, and serializer tests. type WantSpec struct { Tag string `yaml:"tag"` // Expected tag Value string `yaml:"value"` // Expected scalar value Kind string `yaml:"kind"` // Expected node kind Quoted bool `yaml:"quoted"` // Whether scalar should be quoted ContentCount int `yaml:"content_count"` // Expected number of content children Yaml string `yaml:"yaml"` // Expected YAML output } // TestCase represents a single test case loaded from YAML type TestCase struct { Name string `yaml:"name"` Type string `yaml:"type"` // Common fields Yaml string `yaml:"yaml"` InputHex string `yaml:"input_hex"` InputBytes string `yaml:"input_bytes"` From any `yaml:"from"` // Input data for tests Want any `yaml:"want"` // Expected output Also string `yaml:"also"` // Test modifiers (e.g., "unwrap") Like string `yaml:"like"` // Regex pattern to match error message WantSpecs []EventSpec // Populated from Want for detailed tests // scan_tokens_detailed WantTokens []TokenSpec // Populated from Want for detailed tests // emit tests Events []EventSpec `yaml:"data"` WantContains []string // Populated from Want for emit tests Config EmitterConfig `yaml:"conf"` // style_accessor tests (must come before Checks due to shared yaml:"test" tag) StyleTest []any `yaml:"test"` // [Method, STYLE] where Method is string and STYLE is int or string constant // api_new tests Constructor string `yaml:"with"` Checks []FieldCheck `yaml:"test"` // encoding_detect tests WantEncoding string `yaml:"want_encoding"` // char_classify tests Cases []CharTestCase `yaml:"cases"` // yamlprivate tests (char-predicate, char-convert) Function string `yaml:"func"` // Function to call Input ByteInput `yaml:"data"` // Can be string or []int (hex bytes) Index int `yaml:"index"` // Defaults to 0 // reader tests Args Args `yaml:"args"` // Arguments to pass to method (can be scalar or array) // writer tests Output string `yaml:"output_type"` // Output handler type (string, writer, error-writer) Data2 string `yaml:"data2"` // Second data chunk for multi-flush tests // read_handler tests Handler string `yaml:"handler"` ReadSize int `yaml:"read_size"` WantData string `yaml:"want_data"` WantEOF bool `yaml:"want_eof"` // enum_string tests Enum []any `yaml:"enum"` // [Type, Value] where Type is string and Value is int or string // api_method, api_panic, api_delete tests, and reader tests Bytes bool `yaml:"byte"` Method []any `yaml:"call"` Setup any `yaml:"init"` // Can be []interface{} (api tests) or map[string]interface{} (reader tests) // Pipeline stage tests (representer, desolver, serializer) - nested format // For representer: use From for input value // For desolver: use Node for input node to desolve // For serializer: use Node for input node to serialize, Yaml for expected output // Note: Want field (type any) is used - cast to map in test handlers for representer/desolver Node NodeSpec `yaml:"node"` // Input/expected node specification Indent int `yaml:"indent"` // Indentation setting for serializer tests // Error test specific fields As string `yaml:"as"` // Type name for errors.As tests Is string `yaml:"is"` // Error message for errors.Is tests WantAs bool `yaml:"want_as"` // Expected result for errors.As WantIs bool `yaml:"want_is"` // Expected result for errors.Is WantMessages []any `yaml:"want_messages"` // Expected messages for TypeError } // constantRegistry holds libyaml-specific constants var constantRegistry = datatest.NewConstantRegistry() // constantMap maps constant names to their integer values (for backward compatibility) var constantMap = map[string]int{ // ScalarStyle (bit-shifted starting at iota=1) "ANY_SCALAR_STYLE": 0, "PLAIN_SCALAR_STYLE": 2, "SINGLE_QUOTED_SCALAR_STYLE": 4, "DOUBLE_QUOTED_SCALAR_STYLE": 8, "LITERAL_SCALAR_STYLE": 16, "FOLDED_SCALAR_STYLE": 32, // TokenType "NO_TOKEN": 0, "STREAM_START_TOKEN": 1, "STREAM_END_TOKEN": 2, "VERSION_DIRECTIVE_TOKEN": 3, "TAG_DIRECTIVE_TOKEN": 4, "DOCUMENT_START_TOKEN": 5, "DOCUMENT_END_TOKEN": 6, "BLOCK_SEQUENCE_START_TOKEN": 7, "BLOCK_MAPPING_START_TOKEN": 8, "BLOCK_END_TOKEN": 9, "FLOW_SEQUENCE_START_TOKEN": 10, "FLOW_SEQUENCE_END_TOKEN": 11, "FLOW_MAPPING_START_TOKEN": 12, "FLOW_MAPPING_END_TOKEN": 13, "BLOCK_ENTRY_TOKEN": 14, "FLOW_ENTRY_TOKEN": 15, "KEY_TOKEN": 16, "VALUE_TOKEN": 17, "ALIAS_TOKEN": 18, "ANCHOR_TOKEN": 19, "TAG_TOKEN": 20, "SCALAR_TOKEN": 21, // EventType "NO_EVENT": 0, "STREAM_START_EVENT": 1, "STREAM_END_EVENT": 2, "DOCUMENT_START_EVENT": 3, "DOCUMENT_END_EVENT": 4, "ALIAS_EVENT": 5, "SCALAR_EVENT": 6, "SEQUENCE_START_EVENT": 7, "SEQUENCE_END_EVENT": 8, "MAPPING_START_EVENT": 9, "MAPPING_END_EVENT": 10, "TAIL_COMMENT_EVENT": 11, // ParserState "PARSE_STREAM_START_STATE": 0, "PARSE_IMPLICIT_DOCUMENT_START_STATE": 1, "PARSE_DOCUMENT_START_STATE": 2, "PARSE_DOCUMENT_CONTENT_STATE": 3, "PARSE_DOCUMENT_END_STATE": 4, "PARSE_BLOCK_NODE_STATE": 5, "PARSE_BLOCK_SEQUENCE_FIRST_ENTRY_STATE": 6, "PARSE_BLOCK_SEQUENCE_ENTRY_STATE": 7, "PARSE_INDENTLESS_SEQUENCE_ENTRY_STATE": 8, "PARSE_BLOCK_MAPPING_FIRST_KEY_STATE": 9, "PARSE_BLOCK_MAPPING_KEY_STATE": 10, "PARSE_BLOCK_MAPPING_VALUE_STATE": 11, "PARSE_FLOW_SEQUENCE_FIRST_ENTRY_STATE": 12, "PARSE_FLOW_SEQUENCE_ENTRY_STATE": 13, "PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_KEY_STATE": 14, "PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_VALUE_STATE": 15, "PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_END_STATE": 16, "PARSE_FLOW_MAPPING_FIRST_KEY_STATE": 17, "PARSE_FLOW_MAPPING_KEY_STATE": 18, "PARSE_FLOW_MAPPING_VALUE_STATE": 19, "PARSE_FLOW_MAPPING_EMPTY_VALUE_STATE": 20, "PARSE_END_STATE": 21, // SequenceStyle / MappingStyle "ANY_SEQUENCE_STYLE": 0, "BLOCK_SEQUENCE_STYLE": 1, "FLOW_SEQUENCE_STYLE": 2, "ANY_MAPPING_STYLE": 0, "BLOCK_MAPPING_STYLE": 1, "FLOW_MAPPING_STYLE": 2, // Encoding "ANY_ENCODING": 0, "UTF8_ENCODING": 1, "UTF16LE_ENCODING": 2, "UTF16BE_ENCODING": 3, // LineBreak "ANY_BREAK": 0, "CR_BREAK": 1, "LN_BREAK": 2, "CRLN_BREAK": 3, // ErrorType "NO_ERROR": 0, "MEMORY_ERROR": 1, "READER_ERROR": 2, "SCANNER_ERROR": 3, "PARSER_ERROR": 4, "COMPOSER_ERROR": 5, "WRITER_ERROR": 6, "EMITTER_ERROR": 7, } func init() { // Populate constantRegistry with all constants from constantMap for name, value := range constantMap { constantRegistry.Register(name, value) } } // resolveConstant converts a constant name string to its integer value func resolveConstant(t *testing.T, name string) int { t.Helper() val, ok := constantMap[name] if !ok { t.Fatalf("unknown constant name: %s", name) } return val } // IntOrStr wraps the shared datatest.IntOrStr with libyaml's constant registry type IntOrStr struct { datatest.IntOrStr } func (ios *IntOrStr) FromValue(v any) error { ios.Registry = constantRegistry return ios.IntOrStr.FromValue(v) } // ByteInput is an alias to the shared datatest.ByteInput type ByteInput = datatest.ByteInput // Args is an alias to the shared datatest.Args type Args = datatest.Args // EventSpec specifies an event in YAML format type EventSpec struct { Type string `yaml:"type"` Encoding string `yaml:"encoding"` Implicit bool `yaml:"implicit"` QuotedImplicit bool `yaml:"quoted_implicit"` Anchor string `yaml:"anchor"` Tag string `yaml:"tag"` Value string `yaml:"value"` Style string `yaml:"style"` VersionDirective *VersionDirectiveSpec `yaml:"version-directive"` TagDirectives []TagDirectiveSpec `yaml:"tag-directives"` } // VersionDirectiveSpec specifies a version directive type VersionDirectiveSpec struct { Major int `yaml:"major"` Minor int `yaml:"minor"` } // TagDirectiveSpec specifies a tag directive type TagDirectiveSpec struct { Handle string `yaml:"handle"` Prefix string `yaml:"prefix"` } // TokenSpec specifies a token in YAML format type TokenSpec struct { Type string `yaml:"type"` Value string `yaml:"value"` Style string `yaml:"style"` } // EmitterConfig specifies emitter configuration type EmitterConfig struct { Canonical bool `yaml:"canonical"` Indent int `yaml:"indent"` Width int `yaml:"width"` Unicode bool `yaml:"unicode"` LineBreak string `yaml:"line_break"` } // SetupSpec specifies test setup type SetupSpec struct { Constructor string `yaml:"constructor"` Calls []CallSpec `yaml:"calls"` } // CallSpec specifies a method call type CallSpec struct { Method string `yaml:"method"` Args []ArgSpec `yaml:"args"` } // ArgSpec specifies a method argument type ArgSpec struct { Bytes string `yaml:"bytes"` String string `yaml:"string"` Int int `yaml:"int"` Bool bool `yaml:"bool"` Hex string `yaml:"hex"` Reader bool `yaml:"reader"` // Creates a strings.Reader from String field Writer bool `yaml:"writer"` // Creates a bytes.Buffer } // FieldCheck specifies a field check type FieldCheck struct { Nil []any `yaml:"nil"` Cap []any `yaml:"cap"` Len []any `yaml:"len"` LenGt []any `yaml:"len-gt"` // Length greater than Eq []any `yaml:"eq"` Gte []any `yaml:"gte"` // Greater than or equal } // CharTestCase represents a character classification test case type CharTestCase struct { InputHex string `yaml:"input_hex"` Pos int `yaml:"pos"` Want any `yaml:"want"` // Can be bool or int } // unmarshalTestCases converts raw YAML data to TestCase structs using yamltest func unmarshalTestCases(data any) ([]TestCase, error) { casesSlice, ok := data.([]any) if !ok { return nil, fmt.Errorf("expected []interface{}, got %T", data) } var testCases []TestCase for i, item := range casesSlice { caseMap, ok := item.(map[string]any) if !ok { return nil, fmt.Errorf("test case %d: expected map[string]interface{}, got %T", i, item) } // Normalize type-as-key format for top-level test cases caseMap = datatest.NormalizeTypeAsKey(caseMap) var tc TestCase if err := datatest.UnmarshalStruct(&tc, caseMap); err != nil { return nil, fmt.Errorf("test case %d: %w", i, err) } testCases = append(testCases, tc) } return testCases, nil } func LoadTestCases(filename string) ([]TestCase, error) { // Get the path relative to this file _, thisFile, _, _ := runtime.Caller(0) dir := filepath.Dir(thisFile) path := filepath.Join(dir, "testdata", filename) data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("failed to read %s: %w", filename, err) } // Load YAML using LoadAny from loader.go rawData, err := LoadAny(data) if err != nil { return nil, fmt.Errorf("failed to parse %s: %w", filename, err) } // Convert to TestCase structs cases, err := unmarshalTestCases(rawData) if err != nil { return nil, fmt.Errorf("failed to unmarshal test cases from %s: %w", filename, err) } // Post-process: convert Want to WantSpecs/WantTokens for detailed tests for i := range cases { // Determine which field to populate based on test type switch cases[i].Type { case "parse-events-detailed": if cases[i].Want != nil { // Want should be []interface{} of maps, convert to []EventSpec wantSlice, ok := cases[i].Want.([]any) if !ok { return nil, fmt.Errorf("test %s: want should be a sequence, got %T", cases[i].Name, cases[i].Want) } cases[i].WantSpecs = make([]EventSpec, len(wantSlice)) for j, item := range wantSlice { var itemMap map[string]any // Check if item is a scalar string (simplified format) if strVal, ok := item.(string); ok { // Convert scalar to map with type field itemMap = map[string]any{"type": strVal} } else { itemMap, ok = item.(map[string]any) if !ok { return nil, fmt.Errorf("test %s: want[%d] should be a map or string, got %T", cases[i].Name, j, item) } // Normalize type-as-key format itemMap = datatest.NormalizeTypeAsKey(itemMap) } if err := datatest.UnmarshalStruct(&cases[i].WantSpecs[j], itemMap); err != nil { return nil, fmt.Errorf("test %s: want[%d]: %w", cases[i].Name, j, err) } } } case "scan-tokens-detailed": if cases[i].Want != nil { // Want should be []interface{} of maps, convert to []TokenSpec wantSlice, ok := cases[i].Want.([]any) if !ok { return nil, fmt.Errorf("test %s: want should be a sequence, got %T", cases[i].Name, cases[i].Want) } cases[i].WantTokens = make([]TokenSpec, len(wantSlice)) for j, item := range wantSlice { var itemMap map[string]any // Check if item is a scalar string (simplified format) if strVal, ok := item.(string); ok { // Convert scalar to map with type field itemMap = map[string]any{"type": strVal} } else { itemMap, ok = item.(map[string]any) if !ok { return nil, fmt.Errorf("test %s: want[%d] should be a map or string, got %T", cases[i].Name, j, item) } // Normalize type-as-key format itemMap = datatest.NormalizeTypeAsKey(itemMap) } if err := datatest.UnmarshalStruct(&cases[i].WantTokens[j], itemMap); err != nil { return nil, fmt.Errorf("test %s: want[%d]: %w", cases[i].Name, j, err) } } } } // Post-process: convert Want to WantContains for emit tests switch cases[i].Type { case "emit", "emit-config", "roundtrip", "emit-writer": if cases[i].Want != nil { switch v := cases[i].Want.(type) { case string: // Scalar want value cases[i].WantContains = []string{v} case []any: // Sequence want values for _, item := range v { if str, ok := item.(string); ok { cases[i].WantContains = append(cases[i].WantContains, str) } } } } } } return cases, nil } // ParseEventType converts a string to EventType func ParseEventType(t *testing.T, s string) EventType { t.Helper() switch s { case "NO_EVENT": return NO_EVENT case "STREAM_START_EVENT": return STREAM_START_EVENT case "STREAM_END_EVENT": return STREAM_END_EVENT case "DOCUMENT_START_EVENT": return DOCUMENT_START_EVENT case "DOCUMENT_END_EVENT": return DOCUMENT_END_EVENT case "ALIAS_EVENT": return ALIAS_EVENT case "SCALAR_EVENT": return SCALAR_EVENT case "SEQUENCE_START_EVENT": return SEQUENCE_START_EVENT case "SEQUENCE_END_EVENT": return SEQUENCE_END_EVENT case "MAPPING_START_EVENT": return MAPPING_START_EVENT case "MAPPING_END_EVENT": return MAPPING_END_EVENT default: t.Fatalf("unknown event type: %s", s) return NO_EVENT } } // ParseTokenType converts a string to TokenType func ParseTokenType(t *testing.T, s string) TokenType { t.Helper() switch s { case "NO_TOKEN": return NO_TOKEN case "STREAM_START_TOKEN": return STREAM_START_TOKEN case "STREAM_END_TOKEN": return STREAM_END_TOKEN case "VERSION_DIRECTIVE_TOKEN": return VERSION_DIRECTIVE_TOKEN case "TAG_DIRECTIVE_TOKEN": return TAG_DIRECTIVE_TOKEN case "DOCUMENT_START_TOKEN": return DOCUMENT_START_TOKEN case "DOCUMENT_END_TOKEN": return DOCUMENT_END_TOKEN case "BLOCK_SEQUENCE_START_TOKEN": return BLOCK_SEQUENCE_START_TOKEN case "BLOCK_MAPPING_START_TOKEN": return BLOCK_MAPPING_START_TOKEN case "BLOCK_END_TOKEN": return BLOCK_END_TOKEN case "FLOW_SEQUENCE_START_TOKEN": return FLOW_SEQUENCE_START_TOKEN case "FLOW_SEQUENCE_END_TOKEN": return FLOW_SEQUENCE_END_TOKEN case "FLOW_MAPPING_START_TOKEN": return FLOW_MAPPING_START_TOKEN case "FLOW_MAPPING_END_TOKEN": return FLOW_MAPPING_END_TOKEN case "BLOCK_ENTRY_TOKEN": return BLOCK_ENTRY_TOKEN case "FLOW_ENTRY_TOKEN": return FLOW_ENTRY_TOKEN case "KEY_TOKEN": return KEY_TOKEN case "VALUE_TOKEN": return VALUE_TOKEN case "ALIAS_TOKEN": return ALIAS_TOKEN case "ANCHOR_TOKEN": return ANCHOR_TOKEN case "TAG_TOKEN": return TAG_TOKEN case "SCALAR_TOKEN": return SCALAR_TOKEN default: t.Fatalf("unknown token type: %s", s) return NO_TOKEN } } // ParseEncoding converts a string to Encoding func ParseEncoding(t *testing.T, s string) Encoding { t.Helper() switch s { case "ANY_ENCODING": return ANY_ENCODING case "UTF8_ENCODING": return UTF8_ENCODING case "UTF16LE_ENCODING": return UTF16LE_ENCODING case "UTF16BE_ENCODING": return UTF16BE_ENCODING default: t.Fatalf("unknown encoding: %s", s) return ANY_ENCODING } } // ParseScalarStyle converts a string to ScalarStyle func ParseScalarStyle(t *testing.T, s string) ScalarStyle { t.Helper() switch s { case "ANY_SCALAR_STYLE": return ANY_SCALAR_STYLE case "PLAIN_SCALAR_STYLE": return PLAIN_SCALAR_STYLE case "SINGLE_QUOTED_SCALAR_STYLE": return SINGLE_QUOTED_SCALAR_STYLE case "DOUBLE_QUOTED_SCALAR_STYLE": return DOUBLE_QUOTED_SCALAR_STYLE case "LITERAL_SCALAR_STYLE": return LITERAL_SCALAR_STYLE case "FOLDED_SCALAR_STYLE": return FOLDED_SCALAR_STYLE default: t.Fatalf("unknown scalar style: %s", s) return ANY_SCALAR_STYLE } } // ParseSequenceStyle converts a string to SequenceStyle func ParseSequenceStyle(t *testing.T, s string) SequenceStyle { t.Helper() switch s { case "ANY_SEQUENCE_STYLE": return ANY_SEQUENCE_STYLE case "BLOCK_SEQUENCE_STYLE": return BLOCK_SEQUENCE_STYLE case "FLOW_SEQUENCE_STYLE": return FLOW_SEQUENCE_STYLE default: t.Fatalf("unknown sequence style: %s", s) return ANY_SEQUENCE_STYLE } } // ParseMappingStyle converts a string to MappingStyle func ParseMappingStyle(t *testing.T, s string) MappingStyle { t.Helper() switch s { case "ANY_MAPPING_STYLE": return ANY_MAPPING_STYLE case "BLOCK_MAPPING_STYLE": return BLOCK_MAPPING_STYLE case "FLOW_MAPPING_STYLE": return FLOW_MAPPING_STYLE default: t.Fatalf("unknown mapping style: %s", s) return ANY_MAPPING_STYLE } } // CreateEventFromSpec creates an Event from an EventSpec func CreateEventFromSpec(t *testing.T, spec EventSpec) Event { t.Helper() eventType := ParseEventType(t, spec.Type) switch eventType { case STREAM_START_EVENT: encoding := UTF8_ENCODING if spec.Encoding != "" { encoding = ParseEncoding(t, spec.Encoding) } return NewStreamStartEvent(encoding) case STREAM_END_EVENT: return NewStreamEndEvent() case DOCUMENT_START_EVENT: var vd *VersionDirective if spec.VersionDirective != nil { vd = &VersionDirective{ major: int8(spec.VersionDirective.Major), minor: int8(spec.VersionDirective.Minor), } } var td []TagDirective for _, tagSpec := range spec.TagDirectives { td = append(td, TagDirective{ handle: []byte(tagSpec.Handle), prefix: []byte(tagSpec.Prefix), }) } return NewDocumentStartEvent(vd, td, spec.Implicit) case DOCUMENT_END_EVENT: return NewDocumentEndEvent(spec.Implicit) case ALIAS_EVENT: return NewAliasEvent([]byte(spec.Anchor)) case SCALAR_EVENT: style := PLAIN_SCALAR_STYLE if spec.Style != "" { style = ParseScalarStyle(t, spec.Style) } return NewScalarEvent( []byte(spec.Anchor), []byte(spec.Tag), []byte(spec.Value), spec.Implicit, spec.QuotedImplicit, style, ) case SEQUENCE_START_EVENT: style := BLOCK_SEQUENCE_STYLE if spec.Style != "" { style = ParseSequenceStyle(t, spec.Style) } return NewSequenceStartEvent( []byte(spec.Anchor), []byte(spec.Tag), spec.Implicit, style, ) case SEQUENCE_END_EVENT: return NewSequenceEndEvent() case MAPPING_START_EVENT: style := BLOCK_MAPPING_STYLE if spec.Style != "" { style = ParseMappingStyle(t, spec.Style) } return NewMappingStartEvent( []byte(spec.Anchor), []byte(spec.Tag), spec.Implicit, style, ) case MAPPING_END_EVENT: return NewMappingEndEvent() default: t.Fatalf("unsupported event type: %v", eventType) return Event{} } } // HexToBytes converts a hex string to bytes // HexToBytes is now provided by the shared datatest package var HexToBytes = datatest.HexToBytes // GetField is now provided by the shared datatest package var GetField = datatest.GetField // CreateArgValue creates a value from an ArgSpec func CreateArgValue(t *testing.T, spec ArgSpec) any { t.Helper() if spec.Bytes != "" { return []byte(spec.Bytes) } if spec.String != "" { if spec.Reader { return bytes.NewReader([]byte(spec.String)) } return spec.String } if spec.Hex != "" { return HexToBytes(t, spec.Hex) } if spec.Writer { return new(bytes.Buffer) } // Default to the first non-zero value if spec.Int != 0 { return spec.Int } if spec.Bool { return spec.Bool } return nil } // CallMethod is now provided by the shared datatest package var CallMethod = datatest.CallMethod // CreateObject creates an object using a constructor function func CreateObject(t *testing.T, constructorName string) any { t.Helper() switch constructorName { case "NewParser": return NewParser() case "NewEmitter": return NewEmitter() default: t.Fatalf("unknown constructor: %s", constructorName) return nil } } // emitEvents is a helper to emit events and return the output func emitEvents(events []Event) (string, error) { emitter := NewEmitter() var output []byte emitter.SetOutputString(&output) for i := range events { if err := emitter.Emit(&events[i]); err != nil { return "", err } } return string(output), nil } // parseEvents is a helper to parse input and return event types func parseEvents(input string) ([]EventType, bool) { parser := NewParser() parser.SetInputString([]byte(input)) var types []EventType for { var event Event if err := parser.Parse(&event); err != nil { if errors.Is(err, io.EOF) { return types, true } return nil, false } types = append(types, event.Type) if event.Type == STREAM_END_EVENT { break } } return types, true } // parseEventsDetailed is a helper to parse input and return full events func parseEventsDetailed(input string) ([]Event, bool) { parser := NewParser() parser.SetInputString([]byte(input)) var events []Event for { var event Event if err := parser.Parse(&event); err != nil { if errors.Is(err, io.EOF) { return events, true } return nil, false } events = append(events, event) if event.Type == STREAM_END_EVENT { break } } return events, true } // scanTokens is a helper to scan input and return token types func scanTokens(input string) ([]TokenType, bool) { parser := NewParser() parser.SetInputString([]byte(input)) var types []TokenType for { var token Token if err := parser.Scan(&token); err != nil { if errors.Is(err, io.EOF) { return types, true } return nil, false } types = append(types, token.Type) if token.Type == STREAM_END_TOKEN { break } } return types, true } // scanTokensDetailed is a helper to scan input and return full tokens func scanTokensDetailed(input string) ([]Token, bool) { parser := NewParser() parser.SetInputString([]byte(input)) var tokens []Token for { var token Token if err := parser.Scan(&token); err != nil { if errors.Is(err, io.EOF) { return tokens, true } return nil, false } tokens = append(tokens, token) if token.Type == STREAM_END_TOKEN { break } } return tokens, true } // ConfigureEmitter configures an emitter from an EmitterConfig func ConfigureEmitter(emitter *Emitter, config EmitterConfig) { if config.Canonical { emitter.SetCanonical(true) } if config.Indent > 0 { emitter.SetIndent(config.Indent) } if config.Width != 0 { emitter.SetWidth(config.Width) } if config.Unicode { emitter.SetUnicode(true) } if config.LineBreak != "" { // Parse line break style if needed switch config.LineBreak { case "LN": emitter.SetLineBreak(LN_BREAK) case "CR": emitter.SetLineBreak(CR_BREAK) case "CRLF": emitter.SetLineBreak(CRLN_BREAK) } } } // RunEmitTest runs an emit test case func RunEmitTest(t *testing.T, tc TestCase) { t.Helper() var events []Event for _, eventSpec := range tc.Events { events = append(events, CreateEventFromSpec(t, eventSpec)) } var output []byte var emitter *Emitter if tc.Type == "emit-config" { e := NewEmitter() emitter = &e emitter.SetOutputString(&output) ConfigureEmitter(emitter, tc.Config) for i := range events { err := emitter.Emit(&events[i]) assert.NoErrorf(t, err, "Emit() error: %v", err) } } else { result, err := emitEvents(events) assert.NoErrorf(t, err, "emitEvents() error: %v", err) output = []byte(result) } for _, expected := range tc.WantContains { assert.Truef(t, bytes.Contains(output, []byte(expected)), "output should contain %q, got %q", expected, string(output)) } } // RunRoundTripTest runs a roundtrip test case func RunRoundTripTest(t *testing.T, tc TestCase) { t.Helper() parser := NewParser() parser.SetInputString([]byte(tc.Yaml)) var events []Event for { var event Event if err := parser.Parse(&event); err != nil { break } events = append(events, event) if event.Type == STREAM_END_EVENT { break } } emitter := NewEmitter() var output []byte emitter.SetOutputString(&output) for i := range events { err := emitter.Emit(&events[i]) assert.NoErrorf(t, err, "Emit() error: %v", err) } result := string(output) for _, expected := range tc.WantContains { assert.Truef(t, bytes.Contains(output, []byte(expected)), "output should contain %q, got %q", expected, result) } } // GetWriter extracts an [io.Writer] from an interface value func GetWriter(t *testing.T, v any) io.Writer { t.Helper() if w, ok := v.(io.Writer); ok { return w } t.Fatalf("value is not an io.Writer: %T", v) return nil } // GetReader extracts an [io.Reader] from an interface value func GetReader(t *testing.T, v any) io.Reader { t.Helper() if r, ok := v.(io.Reader); ok { return r } t.Fatalf("value is not an io.Reader: %T", v) return nil } // TestHandler is a function that runs a specific test type type TestHandler func(*testing.T, TestCase) // RunTestCases loads test cases from a YAML file and runs them using the provided handlers func RunTestCases(t *testing.T, filename string, handlers map[string]TestHandler) { t.Helper() cases, err := LoadTestCases(filename) assert.NoErrorf(t, err, "Failed to load test cases: %v", err) for _, tc := range cases { tc := tc t.Run(tc.Name, func(t *testing.T) { handler, ok := handlers[tc.Type] if !ok { t.Fatalf("unknown test type: %s", tc.Type) } handler(t, tc) }) } } // WantBool extracts a bool from tc.Want, returning defaultVal if Want is nil // WantBool is now provided by the shared datatest package var WantBool = datatest.WantBool // API test handlers // These test runner functions are used by both parser and emitter tests func runAPINewTest(t *testing.T, tc TestCase) { t.Helper() obj := createObject(t, tc.Constructor) runFieldChecks(t, obj, tc.Checks) } func runAPIMethodTest(t *testing.T, tc TestCase) { t.Helper() obj := createObject(t, tc.Constructor) // Run setup if any if tc.Setup != nil { if setupList, ok := tc.Setup.([]any); ok && len(setupList) > 0 { callMethodFromList(t, obj, setupList, tc.Bytes) } } // Call the main method callMethodFromList(t, obj, tc.Method, tc.Bytes) // Run checks runFieldChecks(t, obj, tc.Checks) } func runAPIPanicTest(t *testing.T, tc TestCase) { t.Helper() obj := createObject(t, tc.Constructor) // Run setup if any if tc.Setup != nil { if setupList, ok := tc.Setup.([]any); ok && len(setupList) > 0 { callMethodFromList(t, obj, setupList, tc.Bytes) } } // The main method call should panic // Want can be either a string or a single-element sequence var wantMsg string switch v := tc.Want.(type) { case string: wantMsg = v case []any: if len(v) > 0 { msg, ok := v[0].(string) assert.Truef(t, ok, "Want[0] should be string, got %T", v[0]) wantMsg = msg } else { t.Fatalf("Want slice is empty, expected at least one element") } default: t.Fatalf("want must be a string or sequence, got %T", tc.Want) } assert.PanicMatchesf(t, wantMsg, func() { callMethodFromList(t, obj, tc.Method, tc.Bytes) }, "Expected panic: %s", wantMsg) } func runAPIDeleteTest(t *testing.T, tc TestCase) { t.Helper() obj := createObject(t, tc.Constructor) // Run setup if any if tc.Setup != nil { if setupList, ok := tc.Setup.([]any); ok && len(setupList) > 0 { callMethodFromList(t, obj, setupList, tc.Bytes) } } // Call Delete method callMethodFromList(t, obj, []any{"Delete"}, false) // Run checks after delete runFieldChecks(t, obj, tc.Checks) } func runAPINewEventTest(t *testing.T, tc TestCase) { t.Helper() event := createEventFromList(t, tc.Method, tc.Bytes) runFieldChecks(t, &event, tc.Checks) } // API test helper functions // These functions support both parser and emitter API tests // createObject creates a Parser or Emitter based on constructor name func createObject(t *testing.T, constructor string) any { t.Helper() switch constructor { case "NewParser": p := NewParser() return &p case "NewEmitter": e := NewEmitter() return &e default: t.Fatalf("unknown constructor: %s", constructor) } return nil } // createEventFromList creates an Event from a method list [constructor, args...] func createEventFromList(t *testing.T, methodList []any, useBytes bool) Event { t.Helper() if len(methodList) == 0 { t.Fatalf("empty method list") } constructor, ok := methodList[0].(string) if !ok { t.Fatalf("constructor should be string, got %T", methodList[0]) } args := methodList[1:] switch constructor { case "NewStreamStartEvent": if len(args) != 1 { t.Fatalf("%s expects 1 argument, got %d", constructor, len(args)) } encoding := parseArg(t, args[0]) return NewStreamStartEvent(Encoding(encoding)) case "NewStreamEndEvent": if len(args) != 0 { t.Fatalf("%s expects 0 arguments, got %d", constructor, len(args)) } return NewStreamEndEvent() case "NewDocumentEndEvent": if len(args) != 1 { t.Fatalf("%s expects 1 argument, got %d", constructor, len(args)) } implicit := parseBoolArg(t, args[0]) return NewDocumentEndEvent(implicit) case "NewAliasEvent": if len(args) != 1 { t.Fatalf("%s expects 1 argument, got %d", constructor, len(args)) } anchor := parseStringArg(t, args[0], useBytes) return NewAliasEvent([]byte(anchor)) case "NewSequenceEndEvent": if len(args) != 0 { t.Fatalf("%s expects 0 arguments, got %d", constructor, len(args)) } return NewSequenceEndEvent() case "NewMappingEndEvent": if len(args) != 0 { t.Fatalf("%s expects 0 arguments, got %d", constructor, len(args)) } return NewMappingEndEvent() default: t.Fatalf("unknown event constructor: %s", constructor) } return Event{} } // callMethodFromList calls a method from a list [methodName, args...] func callMethodFromList(t *testing.T, obj any, methodList []any, useBytes bool) { t.Helper() if len(methodList) == 0 { t.Fatalf("empty method list") } methodName, ok := methodList[0].(string) if !ok { t.Fatalf("method name should be string, got %T", methodList[0]) } args := methodList[1:] v := reflect.ValueOf(obj) if v.Kind() == reflect.Ptr { v = v.Elem() } m := v.Addr().MethodByName(methodName) if !m.IsValid() { t.Fatalf("method not found: %s", methodName) } methodType := m.Type() numParams := methodType.NumIn() // Validate argument count if len(args) != numParams { t.Fatalf("method %s expects %d arguments, got %d", methodName, numParams, len(args)) } // Build argument list var callArgs []reflect.Value for i, arg := range args { paramType := methodType.In(i) // Handle different parameter types if paramType.Kind() == reflect.Bool { val := parseBoolArg(t, arg) callArgs = append(callArgs, reflect.ValueOf(val)) } else if paramType.Kind() == reflect.Slice && paramType.Elem().Kind() == reflect.Uint8 { // Byte slice parameter str := parseStringArg(t, arg, useBytes) callArgs = append(callArgs, reflect.ValueOf([]byte(str))) } else { // Try parsing as constant/int val := parseArg(t, arg) convertedVal := reflect.ValueOf(val).Convert(paramType) callArgs = append(callArgs, convertedVal) } } // Call the method m.Call(callArgs) } // parseArg parses an argument which could be int, bool, or string constant func parseArg(t *testing.T, arg any) int { t.Helper() switch v := arg.(type) { case int: return v case string: if looksLikeConstant(v) { return parseConstant(t, v) } // Try parsing as int if val, err := strconv.Atoi(v); err == nil { return val } t.Fatalf("cannot parse arg as int: %v", arg) default: t.Fatalf("unsupported arg type: %T", arg) } return 0 } // parseBoolArg parses a boolean argument func parseBoolArg(t *testing.T, arg any) bool { t.Helper() switch v := arg.(type) { case bool: return v case string: switch v { case "true": return true case "false": return false default: t.Fatalf("cannot parse string as bool (expected 'true' or 'false'): %q", v) } default: t.Fatalf("cannot parse arg as bool: %v (type %T)", arg, arg) } return false } // parseStringArg parses a string argument func parseStringArg(t *testing.T, arg any, useBytes bool) string { t.Helper() switch v := arg.(type) { case string: return v default: t.Fatalf("cannot parse arg as string: %v (type %T)", arg, arg) } return "" } // looksLikeConstant checks if a string looks like a constant name func looksLikeConstant(s string) bool { if len(s) == 0 { return false } // Check if it's all uppercase letters, digits, and underscores for _, c := range s { if !((c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_') { return false } } return true } // parseConstant parses a constant name to its integer value func parseConstant(t *testing.T, name string) int { t.Helper() // Handle boolean values if name == "true" { return 1 } if name == "false" { return 0 } // Try parsing as int first if val, err := strconv.Atoi(name); err == nil { return val } // Use IntOrStr to parse other constants ios := IntOrStr{} err := ios.FromValue(name) if err != nil { t.Fatalf("failed to parse constant %q: %v", name, err) } return ios.Value } // hasLength checks if a slice has exactly the expected length // Returns true if length matches, false if empty, and fails fatally otherwise func hasLength(t *testing.T, slice []any, expected int) bool { t.Helper() if len(slice) == 0 { return false } if len(slice) != expected { t.Fatalf("expected exactly %d args, got %d", expected, len(slice)) } return true } // runFieldChecks runs field checks on an object func runFieldChecks(t *testing.T, obj any, checks []FieldCheck) { t.Helper() v := reflect.ValueOf(obj) if v.Kind() == reflect.Ptr { v = v.Elem() } for _, check := range checks { // Handle nil checks if hasLength(t, check.Nil, 2) { fieldName, ok := check.Nil[0].(string) if !ok { t.Fatalf("Nil[0] should be string, got %T", check.Nil[0]) } wantNil, ok := check.Nil[1].(bool) if !ok { t.Fatalf("Nil[1] should be bool, got %T", check.Nil[1]) } field := getField(t, v, fieldName) if field.IsValid() { isNil := field.IsNil() if wantNil != isNil { if wantNil { t.Errorf("%s should be nil", fieldName) } else { t.Errorf("%s should not be nil", fieldName) } } } } // Handle cap checks if hasLength(t, check.Cap, 2) { fieldName, ok := check.Cap[0].(string) if !ok { t.Fatalf("Cap[0] should be string, got %T", check.Cap[0]) } wantCap, ok := check.Cap[1].(int) if !ok { t.Fatalf("Cap[1] should be int, got %T", check.Cap[1]) } field := getField(t, v, fieldName) if field.IsValid() && wantCap > 0 { if field.Cap() != wantCap { t.Errorf("%s cap = %d, want %d", fieldName, field.Cap(), wantCap) } } } // Handle len checks if hasLength(t, check.Len, 2) { fieldName, ok := check.Len[0].(string) if !ok { t.Fatalf("Len[0] should be string, got %T", check.Len[0]) } wantLen, ok := check.Len[1].(int) if !ok { t.Fatalf("Len[1] should be int, got %T", check.Len[1]) } field := getField(t, v, fieldName) if field.IsValid() && wantLen > 0 { if field.Len() != wantLen { t.Errorf("%s len = %d, want %d", fieldName, field.Len(), wantLen) } } } // Handle len-gt checks if hasLength(t, check.LenGt, 2) { fieldName, ok := check.LenGt[0].(string) if !ok { t.Fatalf("LenGt[0] should be string, got %T", check.LenGt[0]) } minLen, ok := check.LenGt[1].(int) if !ok { t.Fatalf("LenGt[1] should be int, got %T", check.LenGt[1]) } field := getField(t, v, fieldName) if field.IsValid() && minLen > 0 { if field.Len() <= minLen { t.Errorf("%s len = %d, want > %d", fieldName, field.Len(), minLen) } } } // Handle eq checks if hasLength(t, check.Eq, 2) { fieldName, ok := check.Eq[0].(string) if !ok { t.Fatalf("Eq[0] should be string, got %T", check.Eq[0]) } expectedValue := check.Eq[1] checkEqual(t, v, fieldName, expectedValue) } // Handle gte checks if hasLength(t, check.Gte, 2) { fieldName, ok := check.Gte[0].(string) if !ok { t.Fatalf("Gte[0] should be string, got %T", check.Gte[0]) } minValue, ok := check.Gte[1].(int) if !ok { t.Fatalf("Gte[1] should be int, got %T", check.Gte[1]) } field := getField(t, v, fieldName) if field.IsValid() { got := getIntValue(t, field, fieldName) if got < minValue { t.Errorf("%s = %d, want >= %d", fieldName, got, minValue) } } } } } // getField retrieves a field from a struct, handling special field names func getField(t *testing.T, v reflect.Value, fieldName string) reflect.Value { t.Helper() // Handle special field names like buffer-0, buffer-1 if strings.HasPrefix(fieldName, "buffer-") { var bufferIndex int _, err := fmt.Sscanf(fieldName, "buffer-%d", &bufferIndex) if err == nil { // Validate that buffer index is non-negative if bufferIndex < 0 { t.Fatalf("invalid buffer index: %s (index must be non-negative)", fieldName) } // Return invalid value - buffer index checks are handled separately return reflect.Value{} } } // Convert hyphenated YAML key to underscored Go field name goFieldName := strings.ReplaceAll(fieldName, "-", "_") field := v.FieldByName(goFieldName) if !field.IsValid() { t.Fatalf("field not found: %s (looking for %s)", fieldName, goFieldName) } return field } // getIntValue extracts an integer value from a field func getIntValue(t *testing.T, field reflect.Value, fieldName string) int { t.Helper() // Use reflection Kind() instead of type assertions switch field.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return int(field.Int()) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: return int(field.Uint()) default: t.Fatalf("%s: expected numeric field, got %s", fieldName, field.Kind()) } return 0 } // checkEqual performs an equality check on a field func checkEqual(t *testing.T, v reflect.Value, fieldName string, expectedValue any) { t.Helper() // Handle buffer-N special case var bufferIndex int isBufferIndex := false if strings.HasPrefix(fieldName, "buffer-") { _, err := fmt.Sscanf(fieldName, "buffer-%d", &bufferIndex) if err == nil { // Validate that buffer index is non-negative if bufferIndex < 0 { t.Fatalf("invalid buffer index: %s (index must be non-negative)", fieldName) } isBufferIndex = true } } var field reflect.Value if isBufferIndex { field = v.FieldByName("buffer") if !field.IsValid() { t.Fatalf("buffer field not found for %s", fieldName) } // Check specific byte in buffer if field.Kind() == reflect.Slice && field.Type().Elem().Kind() == reflect.Uint8 { if bufferIndex >= field.Len() { t.Errorf("%s: index %d out of range (buffer len=%d)", fieldName, bufferIndex, field.Len()) return } got := int(field.Index(bufferIndex).Uint()) expected := expectedValue if str, ok := expectedValue.(string); ok && looksLikeConstant(str) { expected = parseConstant(t, str) } else if intVal, ok := expectedValue.(int); ok { expected = intVal } if got != expected { t.Errorf("%s = %v, want %v", fieldName, got, expected) } return } else { t.Errorf("%s: buffer field is not a byte slice", fieldName) return } } field = getField(t, v, fieldName) if !field.IsValid() { return } // Parse constant if it's a string that looks like a constant name var expectedInt int var hasExpectedInt bool expected := expectedValue if str, ok := expectedValue.(string); ok && looksLikeConstant(str) { expectedInt = parseConstant(t, str) hasExpectedInt = true } else if intVal, ok := expectedValue.(int); ok { expectedInt = intVal hasExpectedInt = true } // Get value based on type (handle unexported fields) var got any if field.CanInterface() { // For exported fields, convert expected to field's type if hasExpectedInt { expected = reflect.ValueOf(expectedInt).Convert(field.Type()).Interface() } got = field.Interface() // Handle byte slice comparison if field.Type().Kind() == reflect.Slice && field.Type().Elem().Kind() == reflect.Uint8 { if str, ok := expected.(string); ok { expected = []byte(str) } } } else { // For unexported fields, use type-specific accessors switch field.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: val := field.Int() if val > int64(int(^uint(0)>>1)) || val < int64(-int(^uint(0)>>1)-1) { t.Errorf("field %s value %d overflows int", fieldName, val) return } got = int(val) if hasExpectedInt { expected = expectedInt } case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: val := field.Uint() if val > uint64(int(^uint(0)>>1)) { t.Errorf("field %s value %d overflows int", fieldName, val) return } got = int(val) if hasExpectedInt { expected = expectedInt } case reflect.Bool: got = field.Bool() case reflect.String: got = field.String() case reflect.Slice: // Handle byte slice comparison if field.Type().Elem().Kind() == reflect.Uint8 { got = field.Bytes() if str, ok := expected.(string); ok { expected = []byte(str) } } default: t.Errorf("cannot compare unexported field %s of kind %s", fieldName, field.Kind()) return } } if !reflect.DeepEqual(got, expected) { t.Errorf("%s = %v, want %v", fieldName, got, expected) } } golang-go.yaml-yaml-v4-4.0.0~rc5/internal/libyaml/writer.go000066400000000000000000000013131521353103300235270ustar00rootroot00000000000000// Copyright 2006-2010 Kirill Simonov // Copyright 2011-2019 Canonical Ltd // Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 AND MIT // Output writer with buffering. // Provides write buffering for the emitter stage. package libyaml import "fmt" // Flush the output buffer. func (emitter *Emitter) flush() error { if emitter.write_handler == nil { panic("write handler not set") } // Check if the buffer is empty. if emitter.buffer_pos == 0 { return nil } if err := emitter.write_handler(emitter, emitter.buffer[:emitter.buffer_pos]); err != nil { return WriterError{ Err: fmt.Errorf("write error: %w", err), } } emitter.buffer_pos = 0 return nil } golang-go.yaml-yaml-v4-4.0.0~rc5/internal/libyaml/writer_test.go000066400000000000000000000047401521353103300245750ustar00rootroot00000000000000// Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Tests for the output writer. // Verifies buffered write operations for the emitter. package libyaml import ( "bytes" "errors" "testing" "go.yaml.in/yaml/v4/internal/testutil/assert" ) func TestWriter(t *testing.T) { RunTestCases(t, "writer.yaml", map[string]TestHandler{ "writer-flush": runWriterFlushTest, "writer-error": runWriterErrorTest, "writer-panic": runWriterPanicTest, }) } func runWriterFlushTest(t *testing.T, tc TestCase) { t.Helper() emitter := NewEmitter() var output []byte var buf bytes.Buffer // Setup output handler if tc.Output == "string" { emitter.SetOutputString(&output) } else { emitter.SetOutputWriter(&buf) } // Helper to write data and flush writeAndFlush := func(data string) { if len(data) > 0 { copy(emitter.buffer[:], []byte(data)) emitter.buffer_pos = len(data) } err := emitter.flush() assert.NoErrorf(t, err, "flush() error: %v", err) } // Write and flush data (once or twice) writeAndFlush(string(tc.Input)) if len(tc.Data2) > 0 { writeAndFlush(tc.Data2) } // Check output actual := output if tc.Output == "writer" { actual = buf.Bytes() } want, ok := tc.Want.(string) assert.Truef(t, ok, "Want should be string, got %T", tc.Want) assert.Equalf(t, want, string(actual), "flush() output = %q, want %q", string(actual), want) // Run field checks if tc.Checks != nil { runFieldChecks(t, &emitter, tc.Checks) } } func runWriterErrorTest(t *testing.T, tc TestCase) { t.Helper() emitter := NewEmitter() emitter.SetOutputWriter(&errorWriter{}) if len(tc.Input) > 0 { copy(emitter.buffer[:], tc.Input) emitter.buffer_pos = len(tc.Input) } err := emitter.flush() want, ok := tc.Want.(string) assert.Truef(t, ok, "Want should be string, got %T", tc.Want) assert.ErrorMatchesf(t, want, err, "flush() should return error matching %q, got %v", want, err) } func runWriterPanicTest(t *testing.T, tc TestCase) { t.Helper() emitter := NewEmitter() if len(tc.Input) > 0 { copy(emitter.buffer[:], tc.Input) emitter.buffer_pos = len(tc.Input) } want, ok := tc.Want.(string) assert.Truef(t, ok, "Want should be string, got %T", tc.Want) assert.PanicMatchesf(t, want, func() { _ = emitter.flush() }, "Expected panic: %s", want) } // errorWriter is a writer that always returns an error type errorWriter struct{} func (w *errorWriter) Write(p []byte) (n int, err error) { return 0, errors.New("write error") } golang-go.yaml-yaml-v4-4.0.0~rc5/internal/libyaml/yaml.go000066400000000000000000000341651521353103300231700ustar00rootroot00000000000000// Copyright 2006-2010 Kirill Simonov // Copyright 2011-2019 Canonical Ltd // Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 AND MIT // Core libyaml types and structures. // Defines Parser, Emitter, Event, Token, and related constants for YAML // processing. package libyaml import ( "fmt" "strings" ) // VersionDirective holds the YAML version directive data. type VersionDirective struct { major int8 // The major version number. minor int8 // The minor version number. } // Major returns the major version number. func (v *VersionDirective) Major() int { return int(v.major) } // Minor returns the minor version number. func (v *VersionDirective) Minor() int { return int(v.minor) } // TagDirective holds the YAML tag directive data. type TagDirective struct { handle []byte // The tag handle. prefix []byte // The tag prefix. } // GetHandle returns the tag handle. func (t *TagDirective) GetHandle() string { return string(t.handle) } // GetPrefix returns the tag prefix. func (t *TagDirective) GetPrefix() string { return string(t.prefix) } // Encoding represents the character encoding of a YAML stream. type Encoding int // The stream encoding. const ( // Let the parser choose the encoding. ANY_ENCODING Encoding = iota UTF8_ENCODING // The default UTF-8 encoding. UTF16LE_ENCODING // The UTF-16-LE encoding with BOM. UTF16BE_ENCODING // The UTF-16-BE encoding with BOM. ) // LineBreak represents the line break style used in YAML output. type LineBreak int // Line break types. const ( // Let the parser choose the break type. ANY_BREAK LineBreak = iota CR_BREAK // Use CR for line breaks (Mac style). LN_BREAK // Use LN for line breaks (Unix style). CRLN_BREAK // Use CR LN for line breaks (DOS style). ) // QuoteStyle represents the preferred quote style for scalar values. type QuoteStyle int // Quote style types for required quoting. const ( QuoteSingle QuoteStyle = iota // Prefer single quotes when quoting is required. QuoteDouble // Prefer double quotes when quoting is required. QuoteLegacy // Legacy behavior: double in representer, single in emitter. ) // ScalarStyle returns the scalar style for this quote preference in the // representer/serializer context. // In this context, both QuoteDouble and QuoteLegacy use double quotes. func (q QuoteStyle) ScalarStyle() ScalarStyle { if q == QuoteDouble || q == QuoteLegacy { return DOUBLE_QUOTED_SCALAR_STYLE } return SINGLE_QUOTED_SCALAR_STYLE } // ErrorType represents the category of error that occurred during processing. type ErrorType int // Many bad things could happen with the parser and emitter. const ( // No error is produced. NO_ERROR ErrorType = iota MEMORY_ERROR // Cannot allocate or reallocate a block of memory. READER_ERROR // Cannot read or decode the input stream. SCANNER_ERROR // Cannot scan the input stream. PARSER_ERROR // Cannot parse the input stream. COMPOSER_ERROR // Cannot compose a YAML document. WRITER_ERROR // Cannot write to the output stream. EMITTER_ERROR // Cannot emit a YAML stream. ) // Mark holds the pointer position. type Mark struct { Index int // The position index. Line int // The position line (1-indexed; 0 means unknown). Column int // The position column (1-indexed; 0 means unknown). } // String returns a human-readable string representation of the position mark. func (m Mark) String() string { var builder strings.Builder if m.Line == 0 { return "" } fmt.Fprintf(&builder, "line %d", m.Line) if m.Column > 0 { fmt.Fprintf(&builder, ", column %d", m.Column) } return builder.String() } // shortString returns a compact position string. // Returns "" when Line is 0 (position not known). // When Column is 0 (unknown), it is omitted from output ("L{line}"); // otherwise it is displayed as "L{line}.C{col}". func (m Mark) shortString() string { if m.Line == 0 { return "" } if m.Column > 0 { return fmt.Sprintf("L%d.C%d", m.Line, m.Column) } return fmt.Sprintf("L%d", m.Line) } // rangeString formats a position range from start mark m to end mark. // Both marks use shortString for their individual display. // When marks are on the same line: // - Both Column==0: just "L2" (unknown columns, no range shown) // - Both Column>0: "L2.C6-C7" (compact column range) // - Mixed columns: "L1.C4-L1" (full start with line-only end) // // When marks are on different lines: "L1.C8-L2.C3" func (m Mark) rangeString(end Mark) string { start := m.shortString() if m.Line == end.Line { if m.Column == 0 && end.Column == 0 { // Same line, unknown columns: just "L2" return start } if m.Column > 0 && end.Column > 0 { if m.Column == end.Column { // Same position: just "L2.C6" return start } // Same line with columns: "L2.C6-C7" return fmt.Sprintf("%s-C%d", start, end.Column) } } return fmt.Sprintf("%s-%s", start, end.shortString()) } // Node Styles // styleInt is the underlying type for style constants. type styleInt int8 // ScalarStyle represents the formatting style of a scalar value. type ScalarStyle styleInt // Scalar styles. const ( // Let the emitter choose the style. ANY_SCALAR_STYLE ScalarStyle = 0 PLAIN_SCALAR_STYLE ScalarStyle = 1 << iota // The plain scalar style. SINGLE_QUOTED_SCALAR_STYLE // The single-quoted scalar style. DOUBLE_QUOTED_SCALAR_STYLE // The double-quoted scalar style. LITERAL_SCALAR_STYLE // The literal scalar style. FOLDED_SCALAR_STYLE // The folded scalar style. ) // String returns a string representation of a [ScalarStyle]. func (style ScalarStyle) String() string { switch style { case PLAIN_SCALAR_STYLE: return "Plain" case SINGLE_QUOTED_SCALAR_STYLE: return "Single" case DOUBLE_QUOTED_SCALAR_STYLE: return "Double" case LITERAL_SCALAR_STYLE: return "Literal" case FOLDED_SCALAR_STYLE: return "Folded" default: return "" } } // SequenceStyle represents the formatting style of a sequence node. type SequenceStyle styleInt // Sequence styles. const ( // Let the emitter choose the style. ANY_SEQUENCE_STYLE SequenceStyle = iota BLOCK_SEQUENCE_STYLE // The block sequence style. FLOW_SEQUENCE_STYLE // The flow sequence style. ) // MappingStyle represents the formatting style of a mapping node. type MappingStyle styleInt // Mapping styles. const ( // Let the emitter choose the style. ANY_MAPPING_STYLE MappingStyle = iota BLOCK_MAPPING_STYLE // The block mapping style. FLOW_MAPPING_STYLE // The flow mapping style. ) // Tokens // TokenType represents the type of a scanned token. type TokenType int // Token types. const ( // An empty token. NO_TOKEN TokenType = iota STREAM_START_TOKEN // A STREAM-START token. STREAM_END_TOKEN // A STREAM-END token. VERSION_DIRECTIVE_TOKEN // A VERSION-DIRECTIVE token. TAG_DIRECTIVE_TOKEN // A TAG-DIRECTIVE token. DOCUMENT_START_TOKEN // A DOCUMENT-START token. DOCUMENT_END_TOKEN // A DOCUMENT-END token. BLOCK_SEQUENCE_START_TOKEN // A BLOCK-SEQUENCE-START token. BLOCK_MAPPING_START_TOKEN // A BLOCK-SEQUENCE-END token. BLOCK_END_TOKEN // A BLOCK-END token. FLOW_SEQUENCE_START_TOKEN // A FLOW-SEQUENCE-START token. FLOW_SEQUENCE_END_TOKEN // A FLOW-SEQUENCE-END token. FLOW_MAPPING_START_TOKEN // A FLOW-MAPPING-START token. FLOW_MAPPING_END_TOKEN // A FLOW-MAPPING-END token. BLOCK_ENTRY_TOKEN // A BLOCK-ENTRY token. FLOW_ENTRY_TOKEN // A FLOW-ENTRY token. KEY_TOKEN // A KEY token. VALUE_TOKEN // A VALUE token. ALIAS_TOKEN // An ALIAS token. ANCHOR_TOKEN // An ANCHOR token. TAG_TOKEN // A TAG token. SCALAR_TOKEN // A SCALAR token. COMMENT_TOKEN // A COMMENT token. ) // String returns a string representation of the token type. func (tt TokenType) String() string { switch tt { case NO_TOKEN: return "NO_TOKEN" case STREAM_START_TOKEN: return "STREAM_START_TOKEN" case STREAM_END_TOKEN: return "STREAM_END_TOKEN" case VERSION_DIRECTIVE_TOKEN: return "VERSION_DIRECTIVE_TOKEN" case TAG_DIRECTIVE_TOKEN: return "TAG_DIRECTIVE_TOKEN" case DOCUMENT_START_TOKEN: return "DOCUMENT_START_TOKEN" case DOCUMENT_END_TOKEN: return "DOCUMENT_END_TOKEN" case BLOCK_SEQUENCE_START_TOKEN: return "BLOCK_SEQUENCE_START_TOKEN" case BLOCK_MAPPING_START_TOKEN: return "BLOCK_MAPPING_START_TOKEN" case BLOCK_END_TOKEN: return "BLOCK_END_TOKEN" case FLOW_SEQUENCE_START_TOKEN: return "FLOW_SEQUENCE_START_TOKEN" case FLOW_SEQUENCE_END_TOKEN: return "FLOW_SEQUENCE_END_TOKEN" case FLOW_MAPPING_START_TOKEN: return "FLOW_MAPPING_START_TOKEN" case FLOW_MAPPING_END_TOKEN: return "FLOW_MAPPING_END_TOKEN" case BLOCK_ENTRY_TOKEN: return "BLOCK_ENTRY_TOKEN" case FLOW_ENTRY_TOKEN: return "FLOW_ENTRY_TOKEN" case KEY_TOKEN: return "KEY_TOKEN" case VALUE_TOKEN: return "VALUE_TOKEN" case ALIAS_TOKEN: return "ALIAS_TOKEN" case ANCHOR_TOKEN: return "ANCHOR_TOKEN" case TAG_TOKEN: return "TAG_TOKEN" case SCALAR_TOKEN: return "SCALAR_TOKEN" case COMMENT_TOKEN: return "COMMENT_TOKEN" } return "" } // Token holds information about a scanning token. type Token struct { // The token type. Type TokenType // The start/end of the token. StartMark, EndMark Mark // The stream encoding (for STREAM_START_TOKEN). encoding Encoding // The alias/anchor/scalar Value or tag/tag directive handle // (for ALIAS_TOKEN, ANCHOR_TOKEN, SCALAR_TOKEN, TAG_TOKEN, TAG_DIRECTIVE_TOKEN). Value []byte // The tag suffix (for TAG_TOKEN). suffix []byte // The tag directive prefix (for TAG_DIRECTIVE_TOKEN). prefix []byte // The scalar Style (for SCALAR_TOKEN). Style ScalarStyle // The version directive major/minor (for VERSION_DIRECTIVE_TOKEN). major, minor int8 } // Events // EventType represents the type of a parsing or emitting event. type EventType int8 // Event types. const ( // An empty event. NO_EVENT EventType = iota STREAM_START_EVENT // A STREAM-START event. STREAM_END_EVENT // A STREAM-END event. DOCUMENT_START_EVENT // A DOCUMENT-START event. DOCUMENT_END_EVENT // A DOCUMENT-END event. ALIAS_EVENT // An ALIAS event. SCALAR_EVENT // A SCALAR event. SEQUENCE_START_EVENT // A SEQUENCE-START event. SEQUENCE_END_EVENT // A SEQUENCE-END event. MAPPING_START_EVENT // A MAPPING-START event. MAPPING_END_EVENT // A MAPPING-END event. TAIL_COMMENT_EVENT ) // eventStrings maps EventType constants to their string representations. var eventStrings = []string{ NO_EVENT: "none", STREAM_START_EVENT: "stream start", STREAM_END_EVENT: "stream end", DOCUMENT_START_EVENT: "document start", DOCUMENT_END_EVENT: "document end", ALIAS_EVENT: "alias", SCALAR_EVENT: "scalar", SEQUENCE_START_EVENT: "sequence start", SEQUENCE_END_EVENT: "sequence end", MAPPING_START_EVENT: "mapping start", MAPPING_END_EVENT: "mapping end", TAIL_COMMENT_EVENT: "tail comment", } // String returns a string representation of the event type. func (e EventType) String() string { if e < 0 || int(e) >= len(eventStrings) { return fmt.Sprintf("unknown event %d", e) } return eventStrings[e] } // Event holds information about a parsing or emitting event. type Event struct { // The event type. Type EventType // The start and end of the event. StartMark, EndMark Mark // The document encoding (for STREAM_START_EVENT). encoding Encoding // The version directive (for DOCUMENT_START_EVENT). versionDirective *VersionDirective // The list of tag directives (for DOCUMENT_START_EVENT). tagDirectives []TagDirective // The comments HeadComment []byte LineComment []byte FootComment []byte TailComment []byte // The Anchor (for SCALAR_EVENT, SEQUENCE_START_EVENT, MAPPING_START_EVENT, ALIAS_EVENT). Anchor []byte // The Tag (for SCALAR_EVENT, SEQUENCE_START_EVENT, MAPPING_START_EVENT). Tag []byte // The scalar Value (for SCALAR_EVENT). Value []byte // Is the document start/end indicator Implicit, or the tag optional? // (for DOCUMENT_START_EVENT, DOCUMENT_END_EVENT, SEQUENCE_START_EVENT, MAPPING_START_EVENT, SCALAR_EVENT). Implicit bool // Is the tag optional for any non-plain style? (for SCALAR_EVENT). quoted_implicit bool // The Style (for SCALAR_EVENT, SEQUENCE_START_EVENT, MAPPING_START_EVENT). Style Style } // ScalarStyle returns the style of a scalar event. func (e *Event) ScalarStyle() ScalarStyle { return ScalarStyle(e.Style) } // SequenceStyle returns the style of a sequence event. func (e *Event) SequenceStyle() SequenceStyle { return SequenceStyle(e.Style) } // MappingStyle returns the style of a mapping event. func (e *Event) MappingStyle() MappingStyle { return MappingStyle(e.Style) } // GetEncoding returns the stream encoding (for STREAM_START_EVENT). func (e *Event) GetEncoding() Encoding { return e.encoding } // GetVersionDirective returns the version directive (for DOCUMENT_START_EVENT). func (e *Event) GetVersionDirective() *VersionDirective { return e.versionDirective } // GetTagDirectives returns the tag directives (for DOCUMENT_START_EVENT). func (e *Event) GetTagDirectives() []TagDirective { return e.tagDirectives } // Nodes const ( NULL_TAG = "tag:yaml.org,2002:null" // The tag !!null with the only possible value: null. BOOL_TAG = "tag:yaml.org,2002:bool" // The tag !!bool with the values: true and false. STR_TAG = "tag:yaml.org,2002:str" // The tag !!str for string values. INT_TAG = "tag:yaml.org,2002:int" // The tag !!int for integer values. FLOAT_TAG = "tag:yaml.org,2002:float" // The tag !!float for float values. TIMESTAMP_TAG = "tag:yaml.org,2002:timestamp" // The tag !!timestamp for date and time values. SEQ_TAG = "tag:yaml.org,2002:seq" // The tag !!seq is used to denote sequences. MAP_TAG = "tag:yaml.org,2002:map" // The tag !!map is used to denote mapping. // Not in original libyaml. BINARY_TAG = "tag:yaml.org,2002:binary" MERGE_TAG = "tag:yaml.org,2002:merge" DEFAULT_SCALAR_TAG = STR_TAG // The default scalar tag is !!str. DEFAULT_SEQUENCE_TAG = SEQ_TAG // The default sequence tag is !!seq. DEFAULT_MAPPING_TAG = MAP_TAG // The default mapping tag is !!map. ) golang-go.yaml-yaml-v4-4.0.0~rc5/internal/libyaml/yaml_test.go000066400000000000000000000061051521353103300242200ustar00rootroot00000000000000// Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Tests for core libyaml types. // Verifies Event, Token, and other fundamental type operations. package libyaml import ( "testing" "go.yaml.in/yaml/v4/internal/testutil/assert" ) func TestYAML(t *testing.T) { RunTestCases(t, "yaml.yaml", map[string]TestHandler{ "enum-string": runEnumStringTest, "style-accessor": runStyleAccessorTest, }) } // runEnumStringTest tests the String() method of various enum types. // //nolint:thelper // because this function is the real test func runEnumStringTest(t *testing.T, tc TestCase) { // Parse enum array: [Type, Value] if len(tc.Enum) != 2 { t.Fatalf("enum must be [Type, Value], got %v", tc.Enum) } enumType, ok := tc.Enum[0].(string) if !ok { t.Fatalf("enum type must be string, got %T", tc.Enum[0]) } // Value can be int or string constant var enumValue int switch v := tc.Enum[1].(type) { case int: enumValue = v case string: // Parse as constant - this will be resolved by the constant lookup enumValue = resolveConstant(t, v) default: t.Fatalf("enum value must be int or string, got %T", tc.Enum[1]) } var got string switch enumType { case "ScalarStyle": got = ScalarStyle(enumValue).String() case "TokenType": got = TokenType(enumValue).String() case "EventType": got = EventType(enumValue).String() case "ParserState": got = ParserState(enumValue).String() default: t.Fatalf("unknown enum type: %s", enumType) } // Want can be either a string or a single-element sequence var want string switch v := tc.Want.(type) { case string: want = v case []any: if len(v) > 0 { var ok bool want, ok = v[0].(string) if !ok { t.Fatalf("want[0] must be string, got %T", v[0]) } } else { t.Fatalf("Want slice is empty, expected at least one element") } default: t.Fatalf("want must be a string or sequence, got %T", tc.Want) } assert.Equalf(t, want, got, "%s(%d).String() = %q, want %q", enumType, enumValue, got, want) } // runStyleAccessorTest tests the style accessor methods of Event. // //nolint:thelper // because this function is the real test func runStyleAccessorTest(t *testing.T, tc TestCase) { // Parse test array: [Method, STYLE] if len(tc.StyleTest) != 2 { t.Fatalf("test must be [Method, STYLE], got %v", tc.StyleTest) } method, ok := tc.StyleTest[0].(string) if !ok { t.Fatalf("method must be string, got %T", tc.StyleTest[0]) } // Style value can be int or string constant var styleValue int switch v := tc.StyleTest[1].(type) { case int: styleValue = v case string: styleValue = resolveConstant(t, v) default: t.Fatalf("style value must be int or string, got %T", tc.StyleTest[1]) } event := Event{Style: Style(styleValue)} var got int switch method { case "ScalarStyle": got = int(event.ScalarStyle()) case "SequenceStyle": got = int(event.SequenceStyle()) case "MappingStyle": got = int(event.MappingStyle()) default: t.Fatalf("unknown accessor: %s", method) } assert.Equalf(t, styleValue, got, "Event.%s() = %v, want %v", method, got, styleValue) } golang-go.yaml-yaml-v4-4.0.0~rc5/internal/testutil/000077500000000000000000000000001521353103300221125ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc5/internal/testutil/assert/000077500000000000000000000000001521353103300234135ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc5/internal/testutil/assert/assert.go000066400000000000000000000161771521353103300252570ustar00rootroot00000000000000// Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Package assert provides assertion functions for tests. // This is an internal package that was created to provide a simple way to // write tests. // // The idea was to do not add a dependency on a testing framework. package assert import ( "errors" "fmt" "reflect" "regexp" ) // miniTB defines the minimal interface required for test assertions. type miniTB interface { Helper() Fatalf(string, ...any) } // formatSuffix builds an optional suffix from a printf-style format and args. // If msgFormat is empty, an empty string is returned. func formatSuffix(msgFormat string, args ...any) string { if msgFormat == "" { return "" } return " - " + fmt.Sprintf(msgFormat, args...) } // Equal asserts that two values are equal. // // It can be used with comparable types: numbers, strings, pointers to the same object, etc. // // For any other types, use [DeepEqual]. func Equal(tb miniTB, want, got any) { tb.Helper() Equalf(tb, want, got, "") } // Equalf asserts that two values are equal, and reports a message if they are not. func Equalf(tb miniTB, want, got any, msgFormat string, args ...any) { tb.Helper() if got != want { suffix := formatSuffix(msgFormat, args...) tb.Fatalf("got %v; want %v%s", got, want, suffix) } } // DeepEqual asserts that two values are deeply equal. // // It can be used with slices, maps, structs with slices... // // Please consider [Equal] for other types. func DeepEqual(tb miniTB, want, got any) { tb.Helper() DeepEqualf(tb, want, got, "") } // DeepEqualf asserts that two values are deeply equal, and reports a message if they are not. func DeepEqualf(tb miniTB, want, got any, msgFormat string, args ...any) { tb.Helper() if !reflect.DeepEqual(got, want) { suffix := formatSuffix(msgFormat, args...) tb.Fatalf("got %+v; want %+v%s", got, want, suffix) } } // ErrorMatches asserts that an error matches a regular expression. func ErrorMatches(tb miniTB, pattern string, err error) { tb.Helper() ErrorMatchesf(tb, pattern, err, "") } // ErrorMatchesf asserts that an error matches a regular expression, and reports a message if it does not. func ErrorMatchesf(tb miniTB, pattern string, err error, msgFormat string, args ...any) { tb.Helper() if err == nil { suffix := formatSuffix(msgFormat, args...) tb.Fatalf("got nil; want error matching %q%s", pattern, suffix) return } re, reErr := regexp.Compile(pattern) if reErr != nil { suffix := formatSuffix(msgFormat, args...) tb.Fatalf("invalid regexp %q: %v%s", pattern, reErr, suffix) return } if !re.MatchString(err.Error()) { suffix := formatSuffix(msgFormat, args...) tb.Fatalf("error %q does not match %q%s", err.Error(), pattern, suffix) } } // ErrorIs asserts that two errors are equal by using [errors.Is]. func ErrorIs(tb miniTB, got, want error) { tb.Helper() if !errors.Is(got, want) { tb.Fatalf("got %#v; want %#v", got, want) } } // errorAsNoPanic calls [errors.As], but catch possible panic and returns it as an error func errorAsNoPanic(tb miniTB, err error, target any) (ok bool, panic error) { defer func() { if r := recover(); r != nil { ok = false panic = fmt.Errorf("panic: %v", r) return } }() return errors.As(err, target), nil } // ErrorAs asserts that an error can be assigned to a target variable by using [errors.As]. func ErrorAs(tb miniTB, err error, target any) { tb.Helper() ok, panicErr := errorAsNoPanic(tb, err, target) if panicErr != nil { tb.Fatalf("%s", panicErr) return } if ok { return } reflectedType := reflect.TypeOf(target) if reflectedType.Kind() != reflect.Pointer { // this is not supposed to happen with the current implementation of [errors.As] tb.Fatalf("a pointer was expected: got: %s; want: ptr", reflectedType.Kind()) return } tb.Fatalf("got %#v; want %s", err, reflectedType.Elem()) } // NoError asserts that an error is nil. func NoError(tb miniTB, err error) { tb.Helper() NoErrorf(tb, err, "") } // NoErrorf asserts that an error is nil, and reports a message if it is not. func NoErrorf(tb miniTB, err error, msgFormat string, args ...any) { tb.Helper() if err != nil { suffix := formatSuffix(msgFormat, args...) tb.Fatalf("unexpected error: %v%s", err, suffix) } } // IsNil asserts that a value is nil. func IsNil(tb miniTB, v any) { tb.Helper() IsNilf(tb, v, "") } // IsNilf asserts that a value is nil, and reports a message if it is not. func IsNilf(tb miniTB, v any, msgFormat string, args ...any) { tb.Helper() if !isNil(v) { suffix := formatSuffix(msgFormat, args...) tb.Fatalf("got non-nil (type %T): %#v%s", v, v, suffix) } } // NotNil asserts that a value is not nil. func NotNil(tb miniTB, v any) { tb.Helper() NotNilf(tb, v, "") } // NotNilf asserts that a value is not nil, and reports a message if it is. func NotNilf(tb miniTB, v any, msgFormat string, args ...any) { tb.Helper() if isNil(v) { suffix := formatSuffix(msgFormat, args...) tb.Fatalf("got nil; want non-nil%s", suffix) } } // True asserts that a value is true. func True(tb miniTB, got bool) { tb.Helper() Truef(tb, got, "") } // Truef asserts that a value is true, and reports a message if it is not. func Truef(tb miniTB, got bool, msgFormat string, args ...any) { tb.Helper() if !got { suffix := formatSuffix(msgFormat, args...) tb.Fatalf("got false; want true%s", suffix) } } // False asserts that a value is false. func False(tb miniTB, got bool) { tb.Helper() Falsef(tb, got, "") } // Falsef asserts that a value is false, and reports a message if it is not. func Falsef(tb miniTB, got bool, msgFormat string, args ...any) { tb.Helper() if got { suffix := formatSuffix(msgFormat, args...) tb.Fatalf("got true; want false%s", suffix) } } // PanicMatches asserts that a function panics with a message matching the given pattern. func PanicMatches(tb miniTB, pattern string, f func()) { tb.Helper() PanicMatchesf(tb, pattern, f, "") } // PanicMatchesf asserts that a function panics with a message matching the given pattern, // and reports a message if it does not. func PanicMatchesf(tb miniTB, pattern string, f func(), msgFormat string, args ...any) { tb.Helper() var pan any func() { defer func() { pan = recover() }() f() }() if pan == nil { suffix := formatSuffix(msgFormat, args...) tb.Fatalf("function did not panic; want panic matching %q%s", pattern, suffix) return } var pmsg string switch x := pan.(type) { case error: pmsg = x.Error() case string: pmsg = x default: pmsg = fmt.Sprint(x) } re, reErr := regexp.Compile(pattern) if reErr != nil { suffix := formatSuffix(msgFormat, args...) tb.Fatalf("invalid regexp %q: %v%s", pattern, reErr, suffix) return } if !re.MatchString(pmsg) { suffix := formatSuffix(msgFormat, args...) tb.Fatalf("panic %q does not match %q%s", pmsg, pattern, suffix) } } // isNil checks if a value is nil, handling both direct nil and typed nil values. func isNil(v any) bool { if v == nil { return true } rv := reflect.ValueOf(v) switch rv.Kind() { case reflect.Chan, reflect.Func, reflect.Map, reflect.Pointer, reflect.Slice, reflect.Interface, reflect.UnsafePointer: return rv.IsNil() default: return false } } golang-go.yaml-yaml-v4-4.0.0~rc5/internal/testutil/assert/assert_test.go000066400000000000000000000177401521353103300263130ustar00rootroot00000000000000// Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Tests for assertion functions. // Verifies the behavior of internal test assertion helpers. package assert import ( "errors" "fmt" "io" "regexp" "strings" "testing" ) func TestAssertEqual_Success(t *testing.T) { Equal(t, 2, 2) Equal(t, "ok", "ok") } func TestAssertDeepEqual_Success(t *testing.T) { DeepEqual(t, []int{1, 2, 3}, []int{1, 2, 3}) DeepEqual(t, map[string]int{"a": 1}, map[string]int{"a": 1}) } func TestErrorMatches_Success(t *testing.T) { err := fmt.Errorf("http 404: not found") ErrorMatches(t, `http \d+: not found`, err) } func TestAssertNoError_Success(t *testing.T) { var err error NoError(t, err) } func TestIsNil_NotNil_Success(t *testing.T) { var p *int IsNil(t, p) var s []int IsNil(t, s) var w io.Writer IsNil(t, w) s2 := make([]int, 0) NotNil(t, s2) x := 0 NotNil(t, &x) } func TestPanicMatches_Success(t *testing.T) { PanicMatches(t, `boom \d+`, func() { panic("boom 123") }) PanicMatches(t, `fail xyz`, func() { panic(fmt.Errorf("fail xyz")) }) } func TestAssertTrueAndFalse_Success(t *testing.T) { True(t, true) False(t, false) } func Test_isNil_Basics(t *testing.T) { if !isNil(nil) { t.Fatalf("nil should be nil") } var p *int if !isNil(p) { t.Fatalf("nil pointer should be nil") } if isNil(0) { t.Fatalf("non-nil value reported as nil") } s := make([]int, 0) if isNil(s) { t.Fatalf("non-nil slice reported as nil") } } /************** failure-path checks **************/ type fakeTB struct { failed bool msg string } func (f *fakeTB) Helper() {} func (f *fakeTB) Fatalf(format string, args ...any) { f.failed = true f.msg = fmt.Sprintf(format, args...) } // assertFailureMessageMatches checks if fakeTB recorded a failure and that // its message matches the regexp. func assertFailureMessageMatches(t *testing.T, f *fakeTB, pattern string) { t.Helper() if !f.failed { t.Fatalf("expected failure") } re := regexp.MustCompile(pattern) if !re.MatchString(f.msg) { t.Fatalf("message does not match:\ngot: `%s`\nregexp: `%s`", f.msg, pattern) } } func assertFailureMessageContains(t *testing.T, f *fakeTB, substr string) { t.Helper() if !f.failed { t.Fatalf("expected failure") } if !strings.Contains(f.msg, substr) { t.Fatalf("message doesn't contain:\ngot: `%s`\nwant: `%s`", f.msg, substr) } } func TestAssertEqual_Fails(t *testing.T) { mock := &fakeTB{} Equal(mock, 2, 1) assertFailureMessageMatches(t, mock, `^got 1; want 2$`) } func TestAssertDeepEqual_Fails(t *testing.T) { // slice mismatch mock := &fakeTB{} DeepEqual(mock, []int{2}, []int{1}) assertFailureMessageMatches(t, mock, `^got \[1\]; want \[2\]$`) // map mismatch mock2 := &fakeTB{} DeepEqual(mock2, map[string]int{"a": 2}, map[string]int{"a": 1}) assertFailureMessageMatches(t, mock2, `^got map\[a:1\]; want map\[a:2\]$`) } func TestErrorMatches_Fails(t *testing.T) { // nil error mockTB1 := &fakeTB{} ErrorMatches(mockTB1, `x`, nil) assertFailureMessageMatches(t, mockTB1, `^got nil; want error matching "x"$`) // invalid regexp (message may include parser details; check prefix) mockTB2 := &fakeTB{} ErrorMatches(mockTB2, `(`, fmt.Errorf("x")) assertFailureMessageMatches(t, mockTB2, `^invalid regexp "`) // no match mockTB3 := &fakeTB{} ErrorMatches(mockTB3, `def`, fmt.Errorf("abc")) assertFailureMessageMatches(t, mockTB3, `^error "abc" does not match "def"$`) } func TestAssertNoError_Fails(t *testing.T) { m := &fakeTB{} NoError(m, fmt.Errorf("problem")) assertFailureMessageMatches(t, m, `^unexpected error: problem$`) } func TestErrorIs_Success(t *testing.T) { base := fmt.Errorf("base error") wrapped := fmt.Errorf("wrap: %w", base) // direct match ErrorIs(t, base, base) ErrorIs(t, wrapped, wrapped) ErrorIs(t, wrapped, base) // both nil ErrorIs(t, nil, nil) } func TestErrorIs_Fails(t *testing.T) { // mismatch mock1 := &fakeTB{} base := fmt.Errorf("base") other := fmt.Errorf("other") ErrorIs(mock1, base, other) assertFailureMessageMatches(t, mock1, `got &errors.errorString{s:"base"}; want &errors.errorString{s:"other"}`) // expected non-nil, actual nil mock2 := &fakeTB{} ErrorIs(mock2, nil, base) assertFailureMessageMatches(t, mock2, `got ; want &errors.errorString{s:"base"}`) // expected nil, actual non-nil mock3 := &fakeTB{} ErrorIs(mock3, other, nil) assertFailureMessageMatches(t, mock3, `got &errors.errorString{s:"other"}; want `) } // customErr is a custom error type for testing. type customErr struct { msg string } func (e *customErr) Error() string { return e.msg } func TestErrorAs_Success(t *testing.T) { // this simulates an error returned from a function as error var err error = &customErr{"foo"} var target *customErr ErrorAs(t, err, &target) Equal(t, "foo", target.Error()) } func TestErrorAs_Fails(t *testing.T) { tb := &fakeTB{} err := errors.New("foo") var target *customErr ErrorAs(tb, err, &target) assertFailureMessageContains(t, tb, `got &errors.errorString{s:"foo"}; want *assert.customErr`) tb = &fakeTB{} ErrorAs(tb, nil, &target) assertFailureMessageContains(t, tb, `got ; want *assert.customErr`) tb = &fakeTB{} ErrorAs(tb, err, nil) // this is invalid, a panic is expected assertFailureMessageContains(t, tb, `panic`) tb = &fakeTB{} ErrorAs(tb, err, 42) // this is invalid, a panic is expected assertFailureMessageContains(t, tb, `panic`) var a int tb = &fakeTB{} ErrorAs(tb, err, &a) // this is invalid, a panic is expected assertFailureMessageContains(t, tb, `panic`) } func TestIsNil_Fails(t *testing.T) { // non-nil slice mockTB1 := &fakeTB{} s := make([]int, 0) IsNil(mockTB1, s) assertFailureMessageMatches(t, mockTB1, `^got non-nil \(type `) // non-nil pointer mockTB2 := &fakeTB{} x := 1 IsNil(mockTB2, &x) assertFailureMessageMatches(t, mockTB2, `^got non-nil \(type `) } func TestNotNil_Fails(t *testing.T) { // nil interface mockTB1 := &fakeTB{} var w io.Writer NotNil(mockTB1, w) assertFailureMessageMatches(t, mockTB1, `^got nil; want non-nil$`) // nil pointer mockTB2 := &fakeTB{} var p *int NotNil(mockTB2, p) assertFailureMessageMatches(t, mockTB2, `^got nil; want non-nil$`) } func TestPanicMatches_Fails(t *testing.T) { // no panic mockTB1 := &fakeTB{} PanicMatches(mockTB1, `x`, func() {}) assertFailureMessageMatches(t, mockTB1, `^function did not panic; want panic matching "x"$`) // invalid regexp mockTB2 := &fakeTB{} PanicMatches(mockTB2, `(`, func() { panic("oops") }) assertFailureMessageMatches(t, mockTB2, `^invalid regexp "`) // pattern mismatch mockTB3 := &fakeTB{} PanicMatches(mockTB3, `bar`, func() { panic("foo") }) assertFailureMessageMatches(t, mockTB3, `^panic "foo" does not match "bar"$`) } func TestAssertTrueAndFalse_Fails(t *testing.T) { mock1 := &fakeTB{} True(mock1, false) assertFailureMessageMatches(t, mock1, `^got false; want true$`) mock2 := &fakeTB{} False(mock2, true) assertFailureMessageMatches(t, mock2, `^got true; want false$`) } func TestFormatSuffix_NoArgs(t *testing.T) { if got := formatSuffix(""); got != "" { t.Fatalf("expected empty suffix; got %q", got) } } func TestFormatSuffix_FormatString(t *testing.T) { got := formatSuffix("failed %s", "case") want := " - failed case" if got != want { t.Fatalf("got %q; want %q", got, want) } } func TestFormatSuffix_JustString(t *testing.T) { got := formatSuffix("hello") want := " - hello" if got != want { t.Fatalf("got %q; want %q", got, want) } } func TestFormatSuffix_AsUsedByAssertions(t *testing.T) { mockTB1 := &fakeTB{} var w io.Writer // nil interface // with format string NotNilf(mockTB1, w, "extra %s options %d foo %+v", "str", 42, map[int]bool{3: true}) assertFailureMessageMatches(t, mockTB1, `^got nil; want non-nil - extra str options 42 foo map\[3:true\]$`) // with just a string arg NotNilf(mockTB1, w, "ba-dum-tss") assertFailureMessageMatches(t, mockTB1, `^got nil; want non-nil - ba-dum-tss$`) // with no message args NotNil(mockTB1, w) assertFailureMessageMatches(t, mockTB1, `^got nil; want non-nil$`) } golang-go.yaml-yaml-v4-4.0.0~rc5/internal/testutil/datatest/000077500000000000000000000000001521353103300237235ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc5/internal/testutil/datatest/constants.go000066400000000000000000000050571521353103300262750ustar00rootroot00000000000000// Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Constant registry for symbolic test data values. // Allows test data to reference constants by name instead of magic numbers. package datatest import "fmt" // ConstantRegistry maps constant names (strings) to their integer values. // This allows test data to use symbolic constant names instead of magic numbers. type ConstantRegistry struct { constants map[string]int } // NewConstantRegistry creates a new constant registry. func NewConstantRegistry() *ConstantRegistry { return &ConstantRegistry{ constants: make(map[string]int), } } // Register registers a constant name and its integer value. // // Example: // // registry.Register("STREAM_START_EVENT", 1) // registry.Register("PLAIN_SCALAR_STYLE", 2) func (r *ConstantRegistry) Register(name string, value int) { r.constants[name] = value } // Resolve looks up a constant name and returns its value. // Returns (value, true) if found, (0, false) if not found. func (r *ConstantRegistry) Resolve(name string) (int, bool) { val, ok := r.constants[name] return val, ok } // MustResolve looks up a constant name and returns its value. // Panics if the constant is not registered. func (r *ConstantRegistry) MustResolve(name string) int { val, ok := r.Resolve(name) if !ok { panic(fmt.Sprintf("constant %q not registered", name)) } return val } // Has checks if a constant is registered. func (r *ConstantRegistry) Has(name string) bool { _, ok := r.constants[name] return ok } // MergeFrom merges constants from another registry into this one. // If there are conflicts, values from the other registry take precedence. func (r *ConstantRegistry) MergeFrom(other *ConstantRegistry) { for name, value := range other.constants { r.constants[name] = value } } // ListConstants returns a list of all registered constant names. func (r *ConstantRegistry) ListConstants() []string { names := make([]string, 0, len(r.constants)) for name := range r.constants { names = append(names, name) } return names } // ResolveIntOrString attempts to parse a value as either: // 1. An integer constant name (returns the resolved value) // 2. A direct integer value // Returns an error if neither works. func (r *ConstantRegistry) ResolveIntOrString(value any) (int, error) { switch v := value.(type) { case int: return v, nil case string: if resolved, ok := r.Resolve(v); ok { return resolved, nil } return 0, fmt.Errorf("constant %q not found", v) default: return 0, fmt.Errorf("expected int or string, got %T", value) } } golang-go.yaml-yaml-v4-4.0.0~rc5/internal/testutil/datatest/converters.go000066400000000000000000000077361521353103300264610ustar00rootroot00000000000000// Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Type converters for test data unmarshaling. // Provides custom conversion logic for test data types like IntOrStr. package datatest import "fmt" // IntOrStr can be converted from either an int or a string constant name. // It uses a ConstantRegistry to resolve string names to integer values. type IntOrStr struct { Value int Registry *ConstantRegistry // Required for string name resolution; if nil, returns error } // FromValue implements the custom converter interface used by UnmarshalStruct. func (ios *IntOrStr) FromValue(v any) error { switch val := v.(type) { case int: ios.Value = val return nil case string: registry := ios.Registry if registry == nil { return fmt.Errorf("no constant registry available for resolving %q", val) } resolved, ok := registry.Resolve(val) if !ok { return fmt.Errorf("unknown constant name: %s", val) } ios.Value = resolved return nil default: return fmt.Errorf("IntOrStr value must be int or string, got %T", v) } } // ByteInput can be converted from either a string or a sequence of hex bytes. type ByteInput []byte // FromValue implements the custom converter interface used by UnmarshalStruct. func (bi *ByteInput) FromValue(v any) error { // Try string first if strVal, ok := v.(string); ok { *bi = []byte(strVal) return nil } // Try single int (convert to single-byte array) if intVal, ok := v.(int); ok { if intVal < 0 || intVal > 255 { return fmt.Errorf("byte value out of range [0-255]: %d", intVal) } *bi = []byte{byte(intVal)} return nil } // Otherwise, it should be a sequence of integer bytes intSlice, ok := v.([]any) if !ok { return fmt.Errorf("input must be a string, int, or sequence of integers, got %T", v) } // Convert integers to bytes bytes := make([]byte, len(intSlice)) for i, val := range intSlice { intVal, ok := val.(int) if !ok { return fmt.Errorf("byte array element must be int, got %T", val) } if intVal < 0 || intVal > 255 { return fmt.Errorf("byte value out of range [0-255]: %d", intVal) } bytes[i] = byte(intVal) } *bi = bytes return nil } // Args can be converted from either a single value or an array of values. // This is useful for method arguments that can be either scalar or array. type Args []any // FromValue implements the custom converter interface used by UnmarshalStruct. func (a *Args) FromValue(v any) error { // Try array first if arrVal, ok := v.([]any); ok { *a = arrVal return nil } // Otherwise, it's a single scalar value - wrap it in a slice *a = []any{v} return nil } // StringSlice can be converted from either a single string or a slice of strings. type StringSlice []string // FromValue implements the custom converter interface used by UnmarshalStruct. func (ss *StringSlice) FromValue(v any) error { // Try string first if strVal, ok := v.(string); ok { *ss = []string{strVal} return nil } // Try slice of interface{} if slice, ok := v.([]any); ok { strs := make([]string, len(slice)) for i, item := range slice { str, ok := item.(string) if !ok { return fmt.Errorf("StringSlice element %d must be string, got %T", i, item) } strs[i] = str } *ss = strs return nil } return fmt.Errorf("StringSlice must be string or []string, got %T", v) } // IntSlice can be converted from either a single int or a slice of ints. type IntSlice []int // FromValue implements the custom converter interface used by UnmarshalStruct. func (is *IntSlice) FromValue(v any) error { // Try int first if intVal, ok := v.(int); ok { *is = []int{intVal} return nil } // Try slice of interface{} if slice, ok := v.([]any); ok { ints := make([]int, len(slice)) for i, item := range slice { intVal, ok := item.(int) if !ok { return fmt.Errorf("IntSlice element %d must be int, got %T", i, item) } ints[i] = intVal } *is = ints return nil } return fmt.Errorf("IntSlice must be int or []int, got %T", v) } golang-go.yaml-yaml-v4-4.0.0~rc5/internal/testutil/datatest/helpers.go000066400000000000000000000155771521353103300257330ustar00rootroot00000000000000// Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Helper functions for test data processing. // Includes hex decoding, struct unmarshaling, and test case normalization. package datatest import ( "encoding/hex" "fmt" "reflect" "strings" "testing" ) // HexToBytes converts a hex string to bytes. // This is useful for test data that needs to specify binary data. func HexToBytes(t *testing.T, s string) []byte { t.Helper() b, err := hex.DecodeString(s) if err != nil { t.Fatalf("invalid hex string: %s: %v", s, err) } return b } // GetField uses reflection to get a field value from a struct or pointer to struct. // This is useful for test assertions that need to check internal field values. func GetField(t *testing.T, obj any, fieldName string) any { t.Helper() v := reflect.ValueOf(obj) if v.Kind() == reflect.Ptr { v = v.Elem() } field := v.FieldByName(fieldName) if !field.IsValid() { t.Fatalf("field %s not found in %T", fieldName, obj) } return field.Interface() } // CallMethod calls a method on an object using reflection. // This is useful for test cases that need to call methods dynamically. // Returns the slice of return values from the method call as [reflect.Value] wrappers. // Callers must extract the actual values using methods like .Interface(), .Int(), .String(), etc. func CallMethod(t *testing.T, obj any, methodName string, args []any) []reflect.Value { t.Helper() v := reflect.ValueOf(obj) method := v.MethodByName(methodName) if !method.IsValid() { t.Fatalf("method %s not found on %T", methodName, obj) } var argValues []reflect.Value for _, arg := range args { argValues = append(argValues, reflect.ValueOf(arg)) } return method.Call(argValues) } // WantBool extracts a bool value from a test case's Want field. // If Want is nil, returns the defaultVal. // This is useful for test cases where the expected result is a boolean. func WantBool(t *testing.T, want any, defaultVal bool) bool { t.Helper() if want == nil { return defaultVal } boolVal, ok := want.(bool) if !ok { t.Fatalf("Want should be bool, got %T", want) } return boolVal } // WantString extracts a string value from a test case's Want field. // If Want is nil, returns the defaultVal. func WantString(t *testing.T, want any, defaultVal string) string { t.Helper() if want == nil { return defaultVal } strVal, ok := want.(string) if !ok { t.Fatalf("Want should be string, got %T", want) } return strVal } // WantInt extracts an int value from a test case's Want field. // If Want is nil, returns the defaultVal. func WantInt(t *testing.T, want any, defaultVal int) int { t.Helper() if want == nil { return defaultVal } intVal, ok := want.(int) if !ok { t.Fatalf("Want should be int, got %T", want) } return intVal } // WantSlice extracts a slice value from a test case's Want field. // If Want is nil, returns nil. func WantSlice(t *testing.T, want any) []any { t.Helper() if want == nil { return nil } sliceVal, ok := want.([]any) if !ok { t.Fatalf("Want should be []interface{}, got %T", want) } return sliceVal } // SetFieldValue sets a field value on a struct using reflection. // This is useful for test setup that needs to configure objects dynamically. func SetFieldValue(t *testing.T, obj any, fieldName string, value any) { t.Helper() v := reflect.ValueOf(obj) if v.Kind() == reflect.Ptr { v = v.Elem() } field := v.FieldByName(fieldName) if !field.IsValid() { t.Fatalf("field %s not found in %T", fieldName, obj) } if !field.CanSet() { t.Fatalf("field %s cannot be set (unexported?)", fieldName) } field.Set(reflect.ValueOf(value)) } // TrimTrailingNewline removes a trailing newline character from a string if present. // This is useful for YAML literal blocks which add a trailing newline. func TrimTrailingNewline(s string) string { if len(s) > 0 && s[len(s)-1] == '\n' { return s[:len(s)-1] } return s } // GenerateData generates test data from a generator specification. // Supports simple loops, concatenation of parts (join), and nested loops. // // Format: // // Simple loop: {loop: ["value", count]} // Join parts: {join: [{text: "..."}, {loop: ["...", count]}]} // Nested: {join: [...], loop: count} func GenerateData(spec any) ([]byte, error) { specMap, ok := spec.(map[string]any) if !ok { // If it's just a string, return it as-is if str, ok := spec.(string); ok { return []byte(str), nil } return nil, fmt.Errorf("data spec must be map or string, got %T", spec) } // Check for simple loop: {loop: ["value", count]} if loopVal, hasLoop := specMap["loop"]; hasLoop { if _, hasJoin := specMap["join"]; !hasJoin { return generateSimpleLoop(loopVal) } } // Check for join: {join: [{text: "..."}, {loop: ["...", count]}]} if joinVal, hasJoin := specMap["join"]; hasJoin { result, err := generateJoin(joinVal) if err != nil { return nil, err } // Check for loop: repeat the entire join N times if loopVal, hasLoop := specMap["loop"]; hasLoop { count, ok := loopVal.(int) if !ok { return nil, fmt.Errorf("loop count must be int, got %T", loopVal) } return []byte(strings.Repeat(string(result), count)), nil } return result, nil } return nil, fmt.Errorf("data spec must have 'loop' or 'join' field") } // generateSimpleLoop generates test data by repeating a string value n times. func generateSimpleLoop(loopVal any) ([]byte, error) { loopArr, ok := loopVal.([]any) if !ok { return nil, fmt.Errorf("loop must be array [value, count], got %T", loopVal) } if len(loopArr) != 2 { return nil, fmt.Errorf("loop must have 2 elements [value, count], got %d", len(loopArr)) } value, ok := loopArr[0].(string) if !ok { return nil, fmt.Errorf("loop value must be string, got %T", loopArr[0]) } count, ok := loopArr[1].(int) if !ok { return nil, fmt.Errorf("loop count must be int, got %T", loopArr[1]) } return []byte(strings.Repeat(value, count)), nil } // generateJoin generates test data by joining multiple strings. func generateJoin(joinVal any) ([]byte, error) { joinList, ok := joinVal.([]any) if !ok { return nil, fmt.Errorf("join must be array, got %T", joinVal) } var result strings.Builder for i, item := range joinList { itemMap, ok := item.(map[string]any) if !ok { return nil, fmt.Errorf("join item %d must be map, got %T", i, item) } // Check for text field if text, hasText := itemMap["text"]; hasText { textStr, ok := text.(string) if !ok { return nil, fmt.Errorf("join item %d text must be string, got %T", i, text) } result.WriteString(textStr) continue } // Check for loop field if loopVal, hasLoop := itemMap["loop"]; hasLoop { loopData, err := generateSimpleLoop(loopVal) if err != nil { return nil, fmt.Errorf("join item %d: %w", i, err) } result.Write(loopData) continue } return nil, fmt.Errorf("join item %d must have 'text' or 'loop' field", i) } return []byte(result.String()), nil } golang-go.yaml-yaml-v4-4.0.0~rc5/internal/testutil/datatest/loader.go000066400000000000000000000274531521353103300255330ustar00rootroot00000000000000// Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Package datatest provides utilities for data-driven testing with YAML test // files. // It extracts and generalizes the testing infrastructure originally from // internal/libyaml. package datatest import ( "fmt" "os" "reflect" "strings" ) // LoadYAMLFunc is a function type for loading YAML data. // Different packages can provide their own YAML loading implementation. type LoadYAMLFunc func([]byte) (any, error) // LoadTestCasesFromFile loads and normalizes test cases from a YAML file. // It reads the file, parses it with the provided loadYAML function, and normalizes // the type-as-key format to standard format. func LoadTestCasesFromFile(filename string, loadYAML LoadYAMLFunc) ([]map[string]any, error) { data, err := os.ReadFile(filename) if err != nil { return nil, err } rawData, err := loadYAML(data) if err != nil { return nil, err } rawCases, ok := rawData.([]any) if !ok { return nil, fmt.Errorf("expected []interface{}, got %T", rawData) } result := make([]map[string]any, 0, len(rawCases)) for _, item := range rawCases { rawCase, ok := item.(map[string]any) if !ok { continue } // Normalize type-as-key format: {test-type: {...}} -> {type: test-type, ...} normalized := NormalizeTypeAsKey(rawCase) result = append(result, normalized) } return result, nil } // UnmarshalStruct populates a struct from a map using reflection and yaml tags. // This function is exported so it can be used by other packages for data-driven testing. // // Example: // // type TestCase struct { // Name string `yaml:"name"` // Data string `yaml:"data"` // } // var tc TestCase // err := datatest.UnmarshalStruct(&tc, map[string]interface{}{ // "name": "test1", // "data": "hello", // }) func UnmarshalStruct(target any, data map[string]any) error { v := reflect.ValueOf(target) if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Struct { return fmt.Errorf("target must be pointer to struct, got %T", target) } v = v.Elem() t := v.Type() // Build map of yaml tag names to field indices (support multiple fields per tag) fieldMap := make(map[string][]int) for i := 0; i < t.NumField(); i++ { field := t.Field(i) yamlTag := field.Tag.Get("yaml") if yamlTag != "" && yamlTag != "-" { // Remove options like ",omitempty" if idx := strings.Index(yamlTag, ","); idx != -1 { yamlTag = yamlTag[:idx] } fieldMap[yamlTag] = append(fieldMap[yamlTag], i) } } // Populate fields from map for key, value := range data { fieldIndices, ok := fieldMap[key] if !ok { // Skip unknown fields continue } // Try each field with this tag until one succeeds var lastErr error for _, fieldIdx := range fieldIndices { field := v.Field(fieldIdx) if !field.CanSet() { continue } if err := setField(field, value); err != nil { lastErr = err continue } // Success, move to next key lastErr = nil break } // If all fields failed, return the last error if lastErr != nil { return fmt.Errorf("field %s: %w", key, lastErr) } } return nil } // setField sets a [reflect.Value] from an interface{} value func setField(field reflect.Value, value any) error { if !field.CanSet() { return fmt.Errorf("field cannot be set") } if value == nil { // Set zero value field.Set(reflect.Zero(field.Type())) return nil } fieldType := field.Type() // Handle custom types with FromValue method (IntOrStr, ByteInput, Args) if field.CanAddr() { addr := field.Addr() if converter, ok := addr.Interface().(interface{ FromValue(any) error }); ok { if err := converter.FromValue(value); err != nil { return err } return nil } } // Handle basic types valueRefl := reflect.ValueOf(value) valueType := valueRefl.Type() // Direct assignment if types match if valueType.AssignableTo(fieldType) { field.Set(valueRefl) return nil } // Check if conversion is possible for compatible types if valueType.ConvertibleTo(fieldType) && valueRefl.CanConvert(fieldType) { field.Set(valueRefl.Convert(fieldType)) return nil } // Handle conversions switch fieldType.Kind() { case reflect.String: if str, ok := value.(string); ok { field.SetString(str) return nil } case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: if !field.CanInt() { return fmt.Errorf("field cannot store int values") } if !valueRefl.CanInt() { return fmt.Errorf("field type %v expects an integer value, but got %T", fieldType, value) } i64 := valueRefl.Int() if field.OverflowInt(i64) { return fmt.Errorf("value %v overflows %v", value, fieldType) } field.SetInt(i64) return nil case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: if !field.CanUint() { return fmt.Errorf("field cannot store uint values") } // Check for negative integers first (most specific error case) if valueRefl.CanInt() && valueRefl.Int() < 0 { return fmt.Errorf("field type %v expects an unsigned integer, but got negative value %v of type %T", fieldType, value, value) } // Then check if value can be converted to uint if !valueRefl.CanUint() { return fmt.Errorf("field type %v expects an unsigned integer value, but got %T", fieldType, value) } u64 := valueRefl.Uint() if field.OverflowUint(u64) { return fmt.Errorf("value %v overflows %v", value, fieldType) } field.SetUint(u64) return nil case reflect.Bool: if valueRefl.Kind() != reflect.Bool { // Check if value can be converted to bool if valueRefl.Type().ConvertibleTo(fieldType) { field.Set(valueRefl.Convert(fieldType)) return nil } return fmt.Errorf("field type %v expects a boolean value, but got %T", fieldType, value) } field.SetBool(valueRefl.Bool()) return nil case reflect.Float32, reflect.Float64: if !field.CanFloat() { return fmt.Errorf("field cannot store float values") } if !valueRefl.CanFloat() { return fmt.Errorf("field type %v expects a floating-point value, but got %T", fieldType, value) } f64 := valueRefl.Float() if field.OverflowFloat(f64) { return fmt.Errorf("value %f overflows %v", f64, fieldType) } field.SetFloat(f64) return nil case reflect.Slice: return setSliceField(field, value) case reflect.Map: return setMapField(field, value) case reflect.Struct: // Recursively unmarshal nested structs if m, ok := value.(map[string]any); ok { if !field.CanAddr() { return fmt.Errorf("cannot take address of field for nested struct unmarshaling") } return UnmarshalStruct(field.Addr().Interface(), m) } case reflect.Interface: // Just set the value directly field.Set(valueRefl) return nil case reflect.Ptr: // Handle pointer types if fieldType.Elem().Kind() == reflect.Struct { m, ok := value.(map[string]any) if !ok { return fmt.Errorf("expected map for struct pointer, got %T", value) } newStruct := reflect.New(fieldType.Elem()) if err := UnmarshalStruct(newStruct.Interface(), m); err != nil { return err } field.Set(newStruct) return nil } else { // Handle pointer to basic types (like *bool, *int, *string) ptr := reflect.New(fieldType.Elem()) if err := setField(ptr.Elem(), value); err != nil { return err } field.Set(ptr) return nil } } return fmt.Errorf("cannot convert %T to %v", value, fieldType) } // NormalizeTypeAsKey converts maps with type as key to standard format. // Example: {"SCALAR_TOKEN": {"value": "x"}} -> {"type": "SCALAR_TOKEN", "value": "x"} // This function is exported so it can be used by test code that needs to normalize // YAML test data with type-as-key format. func NormalizeTypeAsKey(itemMap map[string]any) map[string]any { // Check if map has exactly one key and no "type" field if len(itemMap) == 1 { _, hasType := itemMap["type"] if !hasType { // Get the single key for key, value := range itemMap { // Check if key looks like a type constant (all uppercase with underscores) if IsTypeConstant(key) { // Check if value is a map if subMap, ok := value.(map[string]any); ok { // Create new map with "type" field for test type newMap := map[string]any{"type": key} // Merge in the sub-map fields, but preserve sub-map's "type" as "output_type" if it exists for k, v := range subMap { if k == "type" { // Sub-map has its own "type" field - preserve it as "output_type" newMap["output_type"] = v } else { newMap[k] = v } } return newMap } } } } } return itemMap } // IsTypeConstant checks if a string looks like a type constant. // Accepts: UPPERCASE_WITH_UNDERSCORES or lowercase-with-hyphens // This function is exported for use in test infrastructure. func IsTypeConstant(s string) bool { if s == "" { return false } hasUpper := false hasLower := false for _, c := range s { if c >= 'A' && c <= 'Z' { hasUpper = true } else if c >= 'a' && c <= 'z' { hasLower = true } else if !(c == '_' || c == '-' || c >= '0' && c <= '9') { return false } } // Either all uppercase (EVENT_TYPE) or has lowercase (test-type) return hasUpper || hasLower } // setSliceField sets a slice field from a value func setSliceField(field reflect.Value, value any) error { sliceVal, ok := value.([]any) if !ok { return fmt.Errorf("expected []interface{} for slice, got %T", value) } elemType := field.Type().Elem() newSlice := reflect.MakeSlice(field.Type(), len(sliceVal), len(sliceVal)) for i, item := range sliceVal { elem := newSlice.Index(i) // Handle struct elements if elemType.Kind() == reflect.Struct { var m map[string]any // Check if item is a scalar string (simplified format) if strVal, ok := item.(string); ok { // Convert scalar to map with type field m = map[string]any{"type": strVal} } else { m, ok = item.(map[string]any) if !ok { return fmt.Errorf("slice element %d: expected map for struct, got %T", i, item) } // Normalize type-as-key format m = NormalizeTypeAsKey(m) } if !elem.CanAddr() { return fmt.Errorf("slice element %d: cannot take address for struct unmarshaling", i) } if err := UnmarshalStruct(elem.Addr().Interface(), m); err != nil { return fmt.Errorf("slice element %d: %w", i, err) } continue } // Handle other types if err := setField(elem, item); err != nil { return fmt.Errorf("slice element %d: %w", i, err) } } field.Set(newSlice) return nil } // setMapField sets a map field from a value func setMapField(field reflect.Value, value any) error { mapVal, ok := value.(map[string]any) if !ok { return fmt.Errorf("expected map[string]interface{}, got %T", value) } mapType := field.Type() if mapType.Key().Kind() != reflect.String { return fmt.Errorf("only string keys supported for maps") } newMap := reflect.MakeMap(mapType) valueType := mapType.Elem() for k, v := range mapVal { keyRefl := reflect.ValueOf(k) valueRefl := reflect.New(valueType).Elem() // Handle struct values if valueType.Kind() == reflect.Struct { m, ok := v.(map[string]any) if !ok { return fmt.Errorf("map value for key %s: expected map for struct, got %T", k, v) } if !valueRefl.CanAddr() { return fmt.Errorf("map value for key %s: cannot take address for struct unmarshaling", k) } if err := UnmarshalStruct(valueRefl.Addr().Interface(), m); err != nil { return fmt.Errorf("map value for key %s: %w", k, err) } } else { if err := setField(valueRefl, v); err != nil { return fmt.Errorf("map value for key %s: %w", k, err) } } newMap.SetMapIndex(keyRefl, valueRefl) } field.Set(newMap) return nil } // LoadTestCasesFunc is a function type for loading test cases from a file. // Each package can provide its own implementation that uses its preferred YAML loader. type LoadTestCasesFunc func(filename string) ([]map[string]any, error) golang-go.yaml-yaml-v4-4.0.0~rc5/internal/testutil/datatest/registry.go000066400000000000000000000122731521353103300261270ustar00rootroot00000000000000// Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Type registry for test data type resolution. // Maps type names to Go types for dynamic test data instantiation. package datatest import ( "fmt" "reflect" ) // TypeRegistry maps type names (strings) to Go types. // This allows test data in YAML to reference types by name. type TypeRegistry struct { types map[string]reflect.Type factories map[string]func() any } // NewTypeRegistry creates a new type registry. func NewTypeRegistry() *TypeRegistry { return &TypeRegistry{ types: make(map[string]reflect.Type), factories: make(map[string]func() any), } } // Register registers a type by providing an exemplar value. // The type name can be used in YAML test data to instantiate values of this type. // // Example: // // registry.Register("string", "") // registry.Register("int", 0) // registry.Register("yaml.Node", yaml.Node{}) func (r *TypeRegistry) Register(name string, exemplar any) { r.types[name] = reflect.TypeOf(exemplar) } // RegisterFactory registers a factory function for creating instances of a type. // This is useful for parameterized types like maps and slices. // // Example: // // registry.RegisterFactory("map[string]any", func() interface{} { // return make(map[string]interface{}) // }) func (r *TypeRegistry) RegisterFactory(name string, factory func() any) { r.factories[name] = factory // Also register the type by calling the factory once if instance := factory(); instance != nil { r.types[name] = reflect.TypeOf(instance) } } // NewInstance creates a new zero-value instance of the registered type. // Returns an error if the type is not registered. func (r *TypeRegistry) NewInstance(name string) (any, error) { // Check if there's a factory first if factory, ok := r.factories[name]; ok { return factory(), nil } // Otherwise, create zero value from type typ, ok := r.types[name] if !ok { return nil, fmt.Errorf("type %q not registered", name) } // Create a new instance return reflect.New(typ).Elem().Interface(), nil } // NewPointerInstance creates a new pointer to a zero-value instance of the registered type. // This is useful for unmarshaling into struct pointers. func (r *TypeRegistry) NewPointerInstance(name string) (any, error) { // Check if there's a factory first if factory, ok := r.factories[name]; ok { instance := factory() // Return pointer to the instance ptr := reflect.New(reflect.TypeOf(instance)) ptr.Elem().Set(reflect.ValueOf(instance)) return ptr.Interface(), nil } // Otherwise, create pointer from type typ, ok := r.types[name] if !ok { return nil, fmt.Errorf("type %q not registered", name) } // Create a new pointer instance return reflect.New(typ).Interface(), nil } // GetType returns the [reflect.Type] for a registered type name. func (r *TypeRegistry) GetType(name string) (reflect.Type, bool) { typ, ok := r.types[name] return typ, ok } // Has checks if a type is registered. func (r *TypeRegistry) Has(name string) bool { _, ok := r.types[name] if !ok { _, ok = r.factories[name] } return ok } // ListTypes returns a list of all registered type names. func (r *TypeRegistry) ListTypes() []string { types := make([]string, 0, len(r.types)+len(r.factories)) seen := make(map[string]bool) for name := range r.types { types = append(types, name) seen[name] = true } for name := range r.factories { if !seen[name] { types = append(types, name) } } return types } // ValueRegistry maps string names to arbitrary constant values for test data. // This allows YAML test files to reference constants by name (e.g., "+Inf", "NaN", "MaxInt32"). // Unlike ConstantRegistry which only handles ints, this handles any type. type ValueRegistry struct { values map[string]any } // NewValueRegistry creates a new empty ValueRegistry. func NewValueRegistry() *ValueRegistry { return &ValueRegistry{ values: make(map[string]any), } } // Register registers a constant value with a name. func (r *ValueRegistry) Register(name string, value any) { r.values[name] = value } // Get retrieves a constant value by name. // Returns (value, true) if found, (nil, false) if not found. func (r *ValueRegistry) Get(name string) (any, bool) { value, ok := r.values[name] return value, ok } // Resolve recursively resolves constant names in a value. // If the value is a string that matches a registered constant name, it's replaced. // If the value is a map or slice, it recursively resolves all elements. func (r *ValueRegistry) Resolve(value any) any { // Check if it's a string constant name if str, ok := value.(string); ok { if constVal, found := r.Get(str); found { return constVal } return value } // Recursively resolve maps if m, ok := value.(map[string]any); ok { result := make(map[string]any, len(m)) for k, v := range m { result[k] = r.Resolve(v) } return result } if m, ok := value.(map[any]any); ok { result := make(map[any]any, len(m)) for k, v := range m { result[r.Resolve(k)] = r.Resolve(v) } return result } // Recursively resolve slices if s, ok := value.([]any); ok { result := make([]any, len(s)) for i, v := range s { result[i] = r.Resolve(v) } return result } return value } golang-go.yaml-yaml-v4-4.0.0~rc5/internal/testutil/datatest/runner.go000066400000000000000000000104771521353103300255740ustar00rootroot00000000000000// Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Test runner for data-driven test execution. // Manages test handlers and orchestrates YAML-based test execution. package datatest import ( "fmt" "testing" ) // TestHandler is a function that runs a single test case. // The test case is passed as a map[string]interface{}. type TestHandler func(t *testing.T, tc map[string]any) // TestRunner manages test execution with handlers for different test types. type TestRunner struct { handlers map[string]TestHandler } // NewTestRunner creates a new test runner. func NewTestRunner() *TestRunner { return &TestRunner{ handlers: make(map[string]TestHandler), } } // RegisterHandler registers a handler for a specific test type. func (r *TestRunner) RegisterHandler(testType string, handler TestHandler) { r.handlers[testType] = handler } // RunWithCases executes test cases loaded from a slice of maps. func (r *TestRunner) RunWithCases(t *testing.T, cases []map[string]any) { t.Helper() for _, tc := range cases { tc := tc // capture loop variable // Extract test name and type name, _ := tc["name"].(string) if name == "" { name = "unnamed" } testType, _ := tc["type"].(string) if testType == "" { t.Fatalf("Test case %q missing 'type' field", name) } t.Run(name, func(t *testing.T) { handler, ok := r.handlers[testType] if !ok { t.Fatalf("Unknown test type: %s", testType) } handler(t, tc) }) } } // RunTestCases is a convenience function that creates a runner and executes test cases. // The loadFunc should be provided by the calling package to load test data using its preferred YAML parser. func RunTestCases(t *testing.T, loadFunc func() ([]map[string]any, error), handlers map[string]TestHandler) { t.Helper() cases, err := loadFunc() if err != nil { t.Fatalf("Failed to load test cases: %v", err) } runner := NewTestRunner() for testType, handler := range handlers { runner.RegisterHandler(testType, handler) } runner.RunWithCases(t, cases) } // GetString extracts a string field from a test case map. func GetString(tc map[string]any, key string) (string, bool) { val, ok := tc[key] if !ok { return "", false } str, ok := val.(string) return str, ok } // GetInt extracts an int field from a test case map. func GetInt(tc map[string]any, key string) (int, bool) { val, ok := tc[key] if !ok { return 0, false } intVal, ok := val.(int) return intVal, ok } // GetBool extracts a bool field from a test case map. func GetBool(tc map[string]any, key string) (bool, bool) { val, ok := tc[key] if !ok { return false, false } boolVal, ok := val.(bool) return boolVal, ok } // GetSlice extracts a slice field from a test case map. func GetSlice(tc map[string]any, key string) ([]any, bool) { val, ok := tc[key] if !ok { return nil, false } slice, ok := val.([]any) return slice, ok } // GetMap extracts a map field from a test case map. func GetMap(tc map[string]any, key string) (map[string]any, bool) { val, ok := tc[key] if !ok { return nil, false } m, ok := val.(map[string]any) return m, ok } // RequireString extracts a string field, failing the test if not present. func RequireString(t *testing.T, tc map[string]any, key string) string { t.Helper() val, ok := GetString(tc, key) if !ok { t.Fatalf("Required field %q missing or not a string", key) } return val } // RequireInt extracts an int field, failing the test if not present. func RequireInt(t *testing.T, tc map[string]any, key string) int { t.Helper() val, ok := GetInt(tc, key) if !ok { t.Fatalf("Required field %q missing or not an int", key) } return val } // RequireSlice extracts a slice field, failing the test if not present. func RequireSlice(t *testing.T, tc map[string]any, key string) []any { t.Helper() val, ok := GetSlice(tc, key) if !ok { t.Fatalf("Required field %q missing or not a slice", key) } return val } // UnmarshalTestCase unmarshals a test case map into a struct. func UnmarshalTestCase(tc map[string]any, target any) error { return UnmarshalStruct(target, tc) } // AssertEqual is a helper for comparing expected and actual values in test handlers. func AssertEqual(t *testing.T, expected, actual any) { t.Helper() if fmt.Sprintf("%v", expected) != fmt.Sprintf("%v", actual) { t.Errorf("Expected:\n%v\nGot:\n%v", expected, actual) } } golang-go.yaml-yaml-v4-4.0.0~rc5/node.go000066400000000000000000000063341521353103300177030ustar00rootroot00000000000000package yaml import "go.yaml.in/yaml/v4/internal/libyaml" // ----------------------------------------------------------------------------- // Node-related type aliases and constants // ----------------------------------------------------------------------------- type ( // Node represents an element in the YAML document hierarchy. // While documents are typically encoded and decoded into higher level // types, such as structs and maps, Node is an intermediate representation // that allows detailed control over the content being decoded or encoded. // // It's worth noting that although Node offers access into details such as // line numbers, columns, and comments, the content when re-encoded will // not have its original textual representation preserved. // An effort is made to render the data pleasantly, and to preserve // comments near the data they describe, though. // // Values that make use of the Node type interact with the yaml package in // the same way any other type would do, by encoding and decoding yaml data // directly or indirectly into them. // // For example: // // var person struct { // Name string // Address yaml.Node // } // err := yaml.Unmarshal(data, &person) // // Or by itself: // // var person Node // err := yaml.Unmarshal(data, &person) Node = libyaml.Node // Kind represents the type of YAML node. Kind = libyaml.Kind // Style represents the formatting style of a YAML node. Style = libyaml.Style // Marshaler interface may be implemented by types to customize their // behavior when being marshaled into a YAML document. Marshaler = libyaml.Marshaler // Unmarshaler is the interface implemented by types that can unmarshal // a YAML description of themselves. Unmarshaler = libyaml.Unmarshaler // IsZeroer is used to check whether an object is zero to determine whether // it should be omitted when marshaling with the ,omitempty flag. // One notable implementation is [time.Time]. IsZeroer = libyaml.IsZeroer ) // Kind constants define the different types of YAML nodes. const ( // DocumentNode represents the root of a YAML document. DocumentNode = libyaml.DocumentNode // SequenceNode represents a YAML sequence (list). SequenceNode = libyaml.SequenceNode // MappingNode represents a YAML mapping (dictionary). MappingNode = libyaml.MappingNode // ScalarNode represents a YAML scalar value. ScalarNode = libyaml.ScalarNode // AliasNode represents a reference to an anchored node. AliasNode = libyaml.AliasNode // StreamNode represents a container for multiple YAML documents. StreamNode = libyaml.StreamNode ) // Style constants define different formatting styles for YAML nodes. const ( // TaggedStyle explicitly shows the tag on the node. TaggedStyle = libyaml.TaggedStyle // DoubleQuotedStyle uses double quotes for scalar values. DoubleQuotedStyle = libyaml.DoubleQuotedStyle // SingleQuotedStyle uses single quotes for scalar values. SingleQuotedStyle = libyaml.SingleQuotedStyle // LiteralStyle uses literal block scalar style (|). LiteralStyle = libyaml.LiteralStyle // FoldedStyle uses folded block scalar style (>). FoldedStyle = libyaml.FoldedStyle // FlowStyle uses flow style (inline) formatting. FlowStyle = libyaml.FlowStyle ) golang-go.yaml-yaml-v4-4.0.0~rc5/node_test.go000066400000000000000000000454311521353103300207430ustar00rootroot00000000000000// Copyright 2011-2019 Canonical Ltd // Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Tests for Node operations, including encoding/decoding with Node, // node manipulation, and style handling. package yaml_test import ( "bytes" "fmt" "reflect" "testing" "go.yaml.in/yaml/v4" "go.yaml.in/yaml/v4/internal/libyaml" "go.yaml.in/yaml/v4/internal/testutil/assert" "go.yaml.in/yaml/v4/internal/testutil/datatest" ) func assertNodeEqual(t *testing.T, want *yaml.Node, got *yaml.Node) { t.Helper() if reflect.DeepEqual(got, want) { // fast path return } if got.Tag != want.Tag { t.Errorf("Tag mismatch: want: %q got: %q", want.Tag, got.Tag) } if got.Kind != want.Kind { t.Errorf("Kind mismatch: want: %q got: %q", want.Kind, got.Kind) } if got.Style != want.Style { t.Errorf("Style mismatch: want: %q got: %q", want.Style, got.Style) } if got.HeadComment != want.HeadComment { t.Errorf("HeadComment mismatch: want: %#v got: %#v", want.HeadComment, got.HeadComment) } if got.LineComment != want.LineComment { t.Errorf("LineComment mismatch: want: %#v got: %#v", want.LineComment, got.LineComment) } if got.FootComment != want.FootComment { t.Errorf("FootComment mismatch: want: %#v got: %#v", want.FootComment, got.FootComment) } if got.Value != want.Value { t.Errorf("Value mismatch: want: %q got: %q", want.Value, got.Value) } if got.Anchor != want.Anchor { t.Errorf("Anchor mismatch: want: %q got: %q", want.Anchor, got.Anchor) } if got.Line != want.Line { t.Errorf("Line mismatch: want: %d got: %d", want.Line, got.Line) } if got.Column != want.Column { t.Errorf("Column mismatch: want: %d got: %d", want.Column, got.Column) } if !reflect.DeepEqual(got.Content, want.Content) { // Content differs if len(got.Content) != len(want.Content) { t.Errorf("Content length mismatch:\nwant: %d\ngot: %d", len(want.Content), len(got.Content)) } for i := 0; i < len(want.Content) && i < len(got.Content); i++ { assertNodeEqual(t, want.Content[i], got.Content[i]) } } if t.Failed() { // we already reported an error, there is no need to report it again. return } // this error message is harder to read, and is only shown if no other errors were reported. t.Errorf("nodes differ:\nwant:\n%#v\ngot:\n%#v", want, got) } var setStringTests = []struct { str string yaml string node yaml.Node }{ { "something simple", "something simple\n", yaml.Node{ Kind: yaml.ScalarNode, Value: "something simple", Tag: "!!str", }, }, { `"quoted value"`, "'\"quoted value\"'\n", yaml.Node{ Kind: yaml.ScalarNode, Value: `"quoted value"`, Tag: "!!str", }, }, { "multi\nline", "|-\n multi\n line\n", yaml.Node{ Kind: yaml.ScalarNode, Value: "multi\nline", Tag: "!!str", Style: yaml.LiteralStyle, }, }, { "123", "\"123\"\n", yaml.Node{ Kind: yaml.ScalarNode, Value: "123", Tag: "!!str", }, }, { "multi\nline\n", "|\n multi\n line\n", yaml.Node{ Kind: yaml.ScalarNode, Value: "multi\nline\n", Tag: "!!str", Style: yaml.LiteralStyle, }, }, { "\x80\x81\x82", "!!binary gIGC\n", yaml.Node{ Kind: yaml.ScalarNode, Value: "gIGC", Tag: "!!binary", }, }, } func TestSetString(t *testing.T) { t.Setenv("TZ", "UTC") for _, item := range setStringTests { item := item t.Run("", func(t *testing.T) { t.Logf("str: %q", item.str) var node yaml.Node node.SetString(item.str) assertNodeEqual(t, &item.node, &node) buf := bytes.Buffer{} enc := yaml.NewEncoder(&buf) enc.SetIndent(2) err := enc.Encode(&item.node) assert.NoError(t, err) err = enc.Close() assert.NoError(t, err) assert.Equal(t, item.yaml, buf.String()) var doc yaml.Node err = yaml.Unmarshal([]byte(item.yaml), &doc) assert.NoError(t, err) var str string err = node.Load(&str) assert.NoError(t, err) assert.Equal(t, item.str, str) }) } } var nodeEncodeDecodeTests = []struct { value any yaml string node yaml.Node }{{ "something simple", "something simple\n", yaml.Node{ Kind: yaml.ScalarNode, Value: "something simple", Tag: "!!str", }, }, { `"quoted value"`, "'\"quoted value\"'\n", yaml.Node{ Kind: yaml.ScalarNode, Style: yaml.SingleQuotedStyle, Value: `"quoted value"`, Tag: "!!str", }, }, { 123, "123", yaml.Node{ Kind: yaml.ScalarNode, Value: `123`, Tag: "!!int", }, }, { []any{1, 2}, "[1, 2]", yaml.Node{ Kind: yaml.SequenceNode, Tag: "!!seq", Content: []*yaml.Node{{ Kind: yaml.ScalarNode, Value: "1", Tag: "!!int", }, { Kind: yaml.ScalarNode, Value: "2", Tag: "!!int", }}, }, }, { map[string]any{"a": "b"}, "a: b", yaml.Node{ Kind: yaml.MappingNode, Tag: "!!map", Content: []*yaml.Node{{ Kind: yaml.ScalarNode, Value: "a", Tag: "!!str", }, { Kind: yaml.ScalarNode, Value: "b", Tag: "!!str", }}, }, }} func TestNodeEncodeDecode(t *testing.T) { for _, item := range nodeEncodeDecodeTests { item := item t.Run("", func(t *testing.T) { t.Logf("Encode/Decode test value: %#v", item.value) var v any err := item.node.Load(&v) assert.NoError(t, err) assert.DeepEqual(t, item.value, v) var n yaml.Node err = n.Dump(item.value) assert.NoError(t, err) assert.DeepEqual(t, item.node, n) }) } } func TestNodeZeroEncodeDecode(t *testing.T) { // Zero node value behaves as nil when encoding... var n yaml.Node data, err := yaml.Marshal(&n) assert.NoError(t, err) assert.Equal(t, "null\n", string(data)) // ... and decoding. v := &struct{}{} err = n.Load(&v) assert.NoError(t, err) assert.IsNil(t, v) // ... and even when looking for its tag. assert.Equal(t, "!!null", n.ShortTag()) // Kind zero is still unknown, though. n.Line = 1 _, err = yaml.Marshal(&n) assert.ErrorMatches(t, "go-yaml dump error in serializer: cannot represent node with unknown kind 0", err) err = n.Load(&v) assert.ErrorMatches(t, `go-yaml load error in constructor at L1: cannot construct node with unknown kind: '0'`, err) } func TestNodeOmitEmpty(t *testing.T) { var v struct { A int B yaml.Node `yaml:",omitempty"` } v.A = 1 data, err := yaml.Marshal(&v) assert.NoError(t, err) assert.Equal(t, "a: 1\n", string(data)) v.B.Line = 1 _, err = yaml.Marshal(&v) assert.ErrorMatches(t, "go-yaml dump error in serializer: cannot represent node with unknown kind 0", err) } // NodeInfo represents the information about a YAML node in a test-friendly format type NodeInfo struct { Kind string `yaml:"kind"` Style string `yaml:"style,omitempty"` Anchor string `yaml:"anchor,omitempty"` Tag string `yaml:"tag,omitempty"` Head string `yaml:"head,omitempty"` Line string `yaml:"line,omitempty"` Foot string `yaml:"foot,omitempty"` Text string `yaml:"text,omitempty"` Content []*NodeInfo `yaml:"content,omitempty"` } // isStandardTag checks if a tag is a standard YAML tag func isStandardTag(tag string) bool { switch tag { case "!!null", "!!bool", "!!int", "!!float", "!!str", "!!seq", "!!map": return true } return false } // parseNodeInfo converts a NodeInfo structure into a yaml.Node func parseNodeInfo(info *NodeInfo) (*yaml.Node, error) { if info == nil { return nil, fmt.Errorf("nil NodeInfo") } node := &yaml.Node{} // Parse Kind switch info.Kind { case "Document": node.Kind = yaml.DocumentNode case "Sequence": node.Kind = yaml.SequenceNode case "Mapping": node.Kind = yaml.MappingNode case "Scalar": node.Kind = yaml.ScalarNode case "Alias": node.Kind = yaml.AliasNode default: return nil, fmt.Errorf("unknown node kind: %s", info.Kind) } // Parse Style if info.Style != "" { switch info.Style { case "Double": node.Style = yaml.DoubleQuotedStyle case "Single": node.Style = yaml.SingleQuotedStyle case "Literal": node.Style = yaml.LiteralStyle case "Folded": node.Style = yaml.FoldedStyle case "Flow": node.Style = yaml.FlowStyle case "Tagged": node.Style = yaml.TaggedStyle default: return nil, fmt.Errorf("unknown style: %s", info.Style) } } // Set other fields node.Anchor = info.Anchor node.Tag = info.Tag node.HeadComment = info.Head node.LineComment = info.Line node.FootComment = info.Foot // Add TaggedStyle bit for custom tags (not standard YAML tags) if info.Tag != "" && !isStandardTag(info.Tag) && node.Style != 0 { node.Style |= yaml.TaggedStyle } // Set value for scalar nodes if node.Kind == yaml.ScalarNode { node.Value = info.Text } // Parse content for non-scalar nodes if info.Content != nil { node.Content = make([]*yaml.Node, len(info.Content)) for i, childInfo := range info.Content { childNode, err := parseNodeInfo(childInfo) if err != nil { return nil, fmt.Errorf("content[%d]: %w", i, err) } node.Content[i] = childNode } } return node, nil } // formatNodeInfo converts a yaml.Node into a NodeInfo structure for comparison func formatNodeInfo(n yaml.Node) *NodeInfo { info := &NodeInfo{ Kind: formatKindForTest(n.Kind), } if style := formatStyleForTest(n.Style); style != "" { info.Style = style } if n.Anchor != "" { info.Anchor = n.Anchor } if tag := formatTagForTest(n.Tag, n.Style); tag != "" { info.Tag = tag } if n.HeadComment != "" { info.Head = n.HeadComment } if n.LineComment != "" { info.Line = n.LineComment } if n.FootComment != "" { info.Foot = n.FootComment } if info.Kind == "Scalar" { info.Text = n.Value } else if n.Content != nil { info.Content = make([]*NodeInfo, len(n.Content)) for i, node := range n.Content { info.Content[i] = formatNodeInfo(*node) } } return info } // formatKindForTest converts a YAML node kind into its string representation. func formatKindForTest(k yaml.Kind) string { switch k { case yaml.DocumentNode: return "Document" case yaml.SequenceNode: return "Sequence" case yaml.MappingNode: return "Mapping" case yaml.ScalarNode: return "Scalar" case yaml.AliasNode: return "Alias" default: return "Unknown" } } // formatStyleForTest converts a YAML node style into its string representation. func formatStyleForTest(s yaml.Style) string { // Strip out TaggedStyle bit - it's implicit when we have a custom tag baseStyle := s &^ yaml.TaggedStyle switch baseStyle { case yaml.DoubleQuotedStyle: return "Double" case yaml.SingleQuotedStyle: return "Single" case yaml.LiteralStyle: return "Literal" case yaml.FoldedStyle: return "Folded" case yaml.FlowStyle: return "Flow" } return "" } // formatTagForTest converts a YAML tag string to its string representation. func formatTagForTest(tag string, style yaml.Style) string { // Check if the tag was explicit in the input tagWasExplicit := style&yaml.TaggedStyle != 0 // Show !!str only if it was explicit in the input switch tag { case "!!str", "!!map", "!!seq": if tagWasExplicit { return tag } return "" } // Show all other tags return tag } // runNodeTestCase executes a single node test case func runNodeTestCase(t *testing.T, tc map[string]any) { t.Helper() name := tc["name"].(string) yamlInput := tc["yaml"].(string) // Get the expected node structure nodeInfoData, ok := tc["node"] if !ok { t.Fatal("test case missing 'node' field") } // Convert the node data to NodeInfo var expectedInfo NodeInfo nodeBytes, err := yaml.Marshal(nodeInfoData) assert.NoError(t, err) err = yaml.Unmarshal(nodeBytes, &expectedInfo) assert.NoError(t, err) // Parse expected NodeInfo into yaml.Node expectedNode, err := parseNodeInfo(&expectedInfo) assert.NoError(t, err) // Check if decode/encode should be skipped decodeTest := true encodeTest := true if skipDecode, ok := tc["decode"].(bool); ok && !skipDecode { decodeTest = false } if skipEncode, ok := tc["encode"].(bool); ok && !skipEncode { encodeTest = false } if decodeTest { var actualNode yaml.Node err := yaml.Unmarshal([]byte(yamlInput), &actualNode) assert.NoError(t, err) // Compare using NodeInfo for better error messages actualInfo := formatNodeInfo(actualNode) assertNodeInfoEqual(t, &expectedInfo, actualInfo, name) } if encodeTest { // Encode the expected node with 2-space indent buf := bytes.Buffer{} enc := yaml.NewEncoder(&buf) enc.SetIndent(2) err := enc.Encode(expectedNode) assert.NoError(t, err) err = enc.Close() assert.NoError(t, err) assert.Equal(t, yamlInput, buf.String()) } } // assertNodeInfoEqual compares two NodeInfo structures and reports differences func assertNodeInfoEqual(t *testing.T, expected, actual *NodeInfo, context string) { t.Helper() if expected == nil && actual == nil { return } if expected == nil { t.Fatalf("%s: expected nil, got %+v", context, actual) return } if actual == nil { t.Fatalf("%s: expected %+v, got nil", context, expected) return } assert.Equalf(t, expected.Kind, actual.Kind, "%s: Kind mismatch", context) assert.Equalf(t, expected.Style, actual.Style, "%s: Style mismatch", context) assert.Equalf(t, expected.Anchor, actual.Anchor, "%s: Anchor mismatch", context) assert.Equalf(t, expected.Tag, actual.Tag, "%s: Tag mismatch", context) assert.Equalf(t, expected.Head, actual.Head, "%s: Head comment mismatch", context) assert.Equalf(t, expected.Line, actual.Line, "%s: Line comment mismatch", context) assert.Equalf(t, expected.Foot, actual.Foot, "%s: Foot comment mismatch", context) assert.Equalf(t, expected.Text, actual.Text, "%s: Text mismatch", context) if len(expected.Content) != len(actual.Content) { t.Fatalf("%s: Content length mismatch: expected %d, got %d", context, len(expected.Content), len(actual.Content)) } for i := range expected.Content { assertNodeInfoEqual(t, expected.Content[i], actual.Content[i], fmt.Sprintf("%s.content[%d]", context, i)) } } func TestNodeFromYAML(t *testing.T) { t.Setenv("TZ", "UTC") datatest.RunTestCases(t, func() ([]map[string]any, error) { return datatest.LoadTestCasesFromFile("testdata/node.yaml", libyaml.LoadAny) }, map[string]datatest.TestHandler{ "node-test": runNodeTestCase, }) } func TestNodeLoad(t *testing.T) { // Test basic Load functionality node := &yaml.Node{ Kind: yaml.MappingNode, Tag: "!!map", Content: []*yaml.Node{ {Kind: yaml.ScalarNode, Value: "name", Tag: "!!str"}, {Kind: yaml.ScalarNode, Value: "test", Tag: "!!str"}, }, } var result map[string]string err := node.Load(&result) assert.NoError(t, err) assert.Equal(t, "test", result["name"]) } func TestNodeLoadWithKnownFields(t *testing.T) { // Test that KnownFields option is respected node := &yaml.Node{ Kind: yaml.MappingNode, Tag: "!!map", Content: []*yaml.Node{ {Kind: yaml.ScalarNode, Value: "known", Tag: "!!str"}, {Kind: yaml.ScalarNode, Value: "value", Tag: "!!str"}, {Kind: yaml.ScalarNode, Value: "unknown", Tag: "!!str"}, {Kind: yaml.ScalarNode, Value: "other", Tag: "!!str"}, }, } type Target struct { Known string `yaml:"known"` } // Without KnownFields - should succeed var result1 Target err := node.Load(&result1) assert.NoError(t, err) assert.Equal(t, "value", result1.Known) // With KnownFields - should fail var result2 Target err = node.Load(&result2, yaml.WithKnownFields()) assert.NotNil(t, err) assert.ErrorMatches(t, ".*unknown not found.*", err) } func TestNodeLoadPreservesKnownFieldsInUnmarshaler(t *testing.T) { // This test validates the fix for Issue #460 type strictConfig struct { Name string `yaml:"name"` Port int `yaml:"port"` } // Custom unmarshaler using Load with KnownFields type Config struct { strictConfig } var unmarshalCalled bool unmarshaler := struct { Config }{} // Override UnmarshalYAML to use node.Load oldUnmarshal := func(node *yaml.Node) error { unmarshalCalled = true type plain strictConfig return node.Load((*plain)(&unmarshaler.strictConfig), yaml.WithKnownFields()) } // Valid YAML - should succeed validYAML := []byte(` name: test port: 8080 `) var validNode yaml.Node err := yaml.Unmarshal(validYAML, &validNode) assert.NoError(t, err) err = oldUnmarshal(validNode.Content[0]) assert.NoError(t, err) assert.True(t, unmarshalCalled) assert.Equal(t, "test", unmarshaler.Name) assert.Equal(t, 8080, unmarshaler.Port) // Invalid YAML with unknown field - should fail invalidYAML := []byte(` name: test port: 8080 unknown: field `) var invalidNode yaml.Node err = yaml.Unmarshal(invalidYAML, &invalidNode) assert.NoError(t, err) unmarshalCalled = false err = oldUnmarshal(invalidNode.Content[0]) assert.NotNil(t, err) assert.True(t, unmarshalCalled) assert.ErrorMatches(t, ".*unknown not found.*", err) } func TestNodeDump(t *testing.T) { // Test basic Dump functionality value := map[string]string{"name": "test"} var node yaml.Node err := node.Dump(value) assert.NoError(t, err) assert.Equal(t, yaml.MappingNode, node.Kind) assert.Equal(t, "!!map", node.Tag) assert.Equal(t, 2, len(node.Content)) } func TestNodeDumpWithOptions(t *testing.T) { // Test Dump with encoder options type Config struct { Name string `yaml:"name"` Port int `yaml:"port"` } value := Config{Name: "myapp", Port: 8080} // Dump with V4 (default) var node1 yaml.Node err := node1.Dump(value, yaml.WithV4Defaults()) assert.NoError(t, err) assert.Equal(t, yaml.MappingNode, node1.Kind) // Dump with V3 var node2 yaml.Node err = node2.Dump(value, yaml.WithV3Defaults()) assert.NoError(t, err) assert.Equal(t, yaml.MappingNode, node2.Kind) // Both should produce valid nodes with same content structure assert.Equal(t, len(node1.Content), len(node2.Content)) } func TestNodeLoadWithUniqueKeys(t *testing.T) { // Test that UniqueKeys option is respected node := &yaml.Node{ Kind: yaml.MappingNode, Tag: "!!map", Content: []*yaml.Node{ {Kind: yaml.ScalarNode, Value: "key", Tag: "!!str"}, {Kind: yaml.ScalarNode, Value: "value1", Tag: "!!str"}, {Kind: yaml.ScalarNode, Value: "key", Tag: "!!str"}, {Kind: yaml.ScalarNode, Value: "value2", Tag: "!!str"}, }, } // With UniqueKeys (default) - should fail on duplicate var result1 map[string]string err := node.Load(&result1, yaml.WithUniqueKeys()) assert.NotNil(t, err) assert.ErrorMatches(t, ".*already defined.*", err) // Without UniqueKeys - should succeed (last value wins) var result2 map[string]string err = node.Load(&result2, yaml.WithUniqueKeys(false)) assert.NoError(t, err) assert.Equal(t, "value2", result2["key"]) } func TestNodeLoadInvalidOptions(t *testing.T) { node := &yaml.Node{ Kind: yaml.ScalarNode, Value: "test", Tag: "!!str", } // Test with invalid indent option (should fail during applyOptions) var result string err := node.Load(&result, yaml.WithIndent(100)) assert.NotNil(t, err) assert.ErrorMatches(t, ".*indent must be.*", err) } func TestNodeDumpInvalidOptions(t *testing.T) { value := "test" // Test with invalid indent option var node yaml.Node err := node.Dump(value, yaml.WithIndent(100)) assert.NotNil(t, err) assert.ErrorMatches(t, ".*indent must be.*", err) } golang-go.yaml-yaml-v4-4.0.0~rc5/plugin.go000066400000000000000000000015461521353103300202540ustar00rootroot00000000000000// Copyright 2026 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 package yaml // LimitPlugin configures safety limits for YAML parsing. // // When registered, CheckDepth is called on each nesting depth increase, // and CheckAlias is called on each alias expansion to detect excessive // aliasing. // // Example usage: // // import "go.yaml.in/yaml/v4/plugin/limit" // loader := yaml.NewLoader(data, yaml.WithPlugin(limit.New(limit.AliasNone()))) type LimitPlugin interface { // CheckDepth is called when the parser increases nesting depth. // depth is the current nesting level; ctx.Kind is "flow" or "block". // Return an error to abort parsing. CheckDepth(depth int, ctx *DepthContext) error // CheckAlias is called during alias expansion. // Return an error to abort construction. CheckAlias(aliasCount, constructCount int) error } golang-go.yaml-yaml-v4-4.0.0~rc5/plugin/000077500000000000000000000000001521353103300177175ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc5/plugin/doc.go000066400000000000000000000017171521353103300210210ustar00rootroot00000000000000// Copyright 2026 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Package plugin provides official YAML plugins for go-yaml. // // Plugins extend the core YAML library with optional processing capabilities. // This package contains official plugin implementations maintained by the // go-yaml project. // // # Available Plugins // // Limit plugin (plugin/limit): // - Configurable depth and alias expansion limits // // # Usage // // Import the plugin you need and register it with WithPlugin: // // import "go.yaml.in/yaml/v4" // import "go.yaml.in/yaml/v4/plugin/limit" // // // Disable alias checking for documents with many aliases // loader := yaml.NewLoader(data, yaml.WithPlugin(limit.New(limit.AliasNone()))) // // # Third-Party Plugins // // Plugin interfaces use public types and can be implemented by external // packages. // Implement the relevant plugin interface (e.g., LimitPlugin) and register // with WithPlugin. package plugin golang-go.yaml-yaml-v4-4.0.0~rc5/plugin/limit/000077500000000000000000000000001521353103300210355ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc5/plugin/limit/plugin.go000066400000000000000000000114671521353103300226730ustar00rootroot00000000000000// Copyright 2026 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Package limit provides a configurable safety limit plugin for go-yaml. // // The limit plugin controls the maximum nesting depth and alias expansion // ratio during YAML parsing. // By default, go-yaml enforces conservative limits to prevent DoS attacks. // This plugin lets you relax or tighten those limits for your use case. // // # Usage // // import ( // "go.yaml.in/yaml/v4" // "go.yaml.in/yaml/v4/plugin/limit" // ) // // // Default limits // loader := yaml.NewLoader(data, yaml.WithPlugin(limit.New())) // // // Disable alias checking (e.g. for 11,000 programmatic aliases) // loader := yaml.NewLoader(data, yaml.WithPlugin(limit.New(limit.AliasNone()))) // // // Custom depth limit // loader := yaml.NewLoader(data, yaml.WithPlugin(limit.New(limit.DepthValue(50)))) // // # Third-Party Plugins // // You can implement [yaml.LimitPlugin] directly instead of using this package: // // type StrictLimit struct{} // func (s *StrictLimit) CheckDepth(depth int, ctx *yaml.DepthContext) error { ... } // func (s *StrictLimit) CheckAlias(aliasCount, constructCount int) error { ... } // yaml.NewLoader(data, yaml.WithPlugin(&StrictLimit{})) package limit import ( "fmt" "go.yaml.in/yaml/v4/internal/libyaml" ) // DepthContext is an alias for the type used in depth check callbacks. // See [yaml.DepthContext] for field documentation. type DepthContext = libyaml.DepthContext // Plugin implements configurable safety limits for YAML parsing. type Plugin struct { depthLimit *int depthDisabled bool depthFn func(int, *DepthContext) error aliasLimit *int aliasDisabled bool aliasFn func(int, int) error } // Option configures a [Plugin]. type Option func(*Plugin) // New creates a limit plugin with the given options. // With no options, it uses the same defaults as the bare go-yaml library. func New(opts ...Option) *Plugin { p := &Plugin{} for _, o := range opts { o(p) } return p } // DepthValue sets a maximum nesting depth (both flow and block). func DepthValue(n int) Option { return func(p *Plugin) { p.depthLimit = &n p.depthDisabled = false p.depthFn = nil } } // DepthNone disables depth checking entirely. func DepthNone() Option { return func(p *Plugin) { p.depthDisabled = true p.depthLimit = nil p.depthFn = nil } } // DepthFunc sets a custom depth check function. func DepthFunc(fn func(depth int, ctx *DepthContext) error) Option { return func(p *Plugin) { p.depthFn = fn p.depthLimit = nil p.depthDisabled = false } } // AliasValue sets a simple alias expansion count threshold. func AliasValue(n int) Option { return func(p *Plugin) { p.aliasLimit = &n p.aliasDisabled = false p.aliasFn = nil } } // AliasNone disables alias checking entirely. func AliasNone() Option { return func(p *Plugin) { p.aliasDisabled = true p.aliasLimit = nil p.aliasFn = nil } } // AliasFunc sets a custom alias check function. func AliasFunc(fn func(aliasCount, constructCount int) error) Option { return func(p *Plugin) { p.aliasFn = fn p.aliasLimit = nil p.aliasDisabled = false } } // CheckDepth implements [yaml.LimitPlugin]. func (p *Plugin) CheckDepth(depth int, ctx *DepthContext) error { if p.depthFn != nil { return p.depthFn(depth, ctx) } if p.depthDisabled { return nil } if p.depthLimit != nil { if depth > *p.depthLimit { return fmt.Errorf("exceeded max depth of %d", *p.depthLimit) } return nil } return libyaml.DefaultDepthCheck(depth, ctx) } // CheckAlias implements [yaml.LimitPlugin]. func (p *Plugin) CheckAlias(aliasCount, constructCount int) error { if p.aliasFn != nil { return p.aliasFn(aliasCount, constructCount) } if p.aliasDisabled { return nil } if p.aliasLimit != nil { if aliasCount > *p.aliasLimit { return fmt.Errorf("exceeded max alias count of %d", *p.aliasLimit) } return nil } return libyaml.DefaultAliasCheck(aliasCount, constructCount) } // NewFromYAML creates a limit plugin from a YAML config map. // Keys: "depth" (int or null), "alias" (int or null). // Null values disable the corresponding check. // Omitted keys use defaults. func NewFromYAML(cfg map[string]any) (*Plugin, error) { var opts []Option for key, val := range cfg { switch key { case "depth": if val == nil { opts = append(opts, DepthNone()) } else { n, ok := val.(int) if !ok { return nil, fmt.Errorf("limit: depth must be int or null, got %T", val) } opts = append(opts, DepthValue(n)) } case "alias": if val == nil { opts = append(opts, AliasNone()) } else { n, ok := val.(int) if !ok { return nil, fmt.Errorf("limit: alias must be int or null, got %T", val) } opts = append(opts, AliasValue(n)) } default: return nil, fmt.Errorf("limit: unknown key %q", key) } } return New(opts...), nil } golang-go.yaml-yaml-v4-4.0.0~rc5/plugin_test.go000066400000000000000000000046271521353103300213160ustar00rootroot00000000000000// Copyright 2026 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 package yaml_test import ( "bytes" "strings" "testing" "go.yaml.in/yaml/v4" "go.yaml.in/yaml/v4/plugin/limit" ) // generateAliases builds YAML with n aliases referencing a large anchor. func generateAliases(n int) []byte { var sb strings.Builder sb.WriteString("anchor: &anchor [1, 2, 3]\nrefs:\n") for i := 0; i < n; i++ { sb.WriteString("- *anchor\n") } return []byte(sb.String()) } // generateDeepNesting builds deeply nested flow YAML. func generateDeepNesting(depth int) []byte { var sb strings.Builder for i := 0; i < depth; i++ { sb.WriteString("[") } sb.WriteString("x") for i := 0; i < depth; i++ { sb.WriteString("]") } return []byte(sb.String()) } func TestWithPlugin_Limit_AliasFunc(t *testing.T) { called := false fn := func(aliasCount, constructCount int) error { called = true return nil } data := generateAliases(200) var result any err := yaml.Load(data, &result, yaml.WithPlugin(limit.New(limit.AliasFunc(fn)))) if err != nil { t.Fatalf("Expected success with custom AliasFunc, got: %v", err) } if !called { t.Error("Expected custom AliasFunc to be called") } } func TestWithPlugin_Limit_DepthFunc(t *testing.T) { called := false fn := func(depth int, ctx *yaml.DepthContext) error { called = true return nil } data := generateDeepNesting(5) var result any err := yaml.Load(data, &result, yaml.WithPlugin(limit.New(limit.DepthFunc(fn)))) if err != nil { t.Fatalf("Expected success with custom DepthFunc, got: %v", err) } if !called { t.Error("Expected custom DepthFunc to be called") } } func TestWithPlugin_UnsupportedType(t *testing.T) { data := []byte(`key: value`) var result map[string]any // Pass an unsupported type (integer) as a plugin err := yaml.Load(data, &result, yaml.WithPlugin(42)) if err == nil { t.Fatal("Expected error for unsupported plugin type, got nil") } if err.Error() != "yaml: unsupported plugin type: int" { t.Errorf("Unexpected error message: %v", err) } } func TestDefaultBehavior_HasLimit(t *testing.T) { // Bare NewLoader should have default depth limits data := generateDeepNesting(10001) loader, err := yaml.NewLoader(bytes.NewReader(data)) if err != nil { t.Fatalf("NewLoader failed: %v", err) } var result any err = loader.Load(&result) if err == nil { t.Fatal("Expected error from default depth limits, got nil") } } golang-go.yaml-yaml-v4-4.0.0~rc5/testdata/000077500000000000000000000000001521353103300202325ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc5/testdata/decode.yaml000066400000000000000000000622411521353103300223460ustar00rootroot00000000000000# Copyright 2025 The go-yaml Project Contributors # SPDX-License-Identifier: Apache-2.0 # Decode test cases for go-yaml # # Purpose: Tests YAML unmarshaling/decoding of various YAML constructs into Go values. # Validates proper type conversion, null handling, numeric formats, sequences, mappings, # anchors/aliases, and special values. # # Format: Test cases use type-as-key format: # - test-type: # name: Test case name # ... # # Test Types: # decode - Validates YAML unmarshaling into Go values # # Common Keys: # name - Test case name (string) # yaml - Input YAML string to unmarshal # type - Go type to unmarshal into (e.g., "map[string]any", "[]int") # want - Expected result after unmarshaling (YAML representation) # # Data Types: # Map types: map[string]string, map[string]any, map[string]int64, etc. # Slice types: []string, []int, []any, etc. # Numeric types: map[string]int, map[string]uint64, map[string]float64, etc. # # Special Values: # Constants like +Inf, -Inf, NaN, MaxInt64, etc. are resolved via ValueRegistry # during test execution. - decode: name: string value in map[string]string yaml: 'v: hi' type: map[string]string want: v: hi - decode: name: string value in map[string]any yaml: 'v: hi' type: map[string]any want: v: hi - decode: name: true as string in map[string]string yaml: 'v: true' type: map[string]string want: v: 'true' - decode: name: true as bool in map[string]any yaml: 'v: true' type: map[string]any want: v: true - decode: name: integer in map[string]any yaml: 'v: 10' type: map[string]any want: v: 10 - decode: name: binary integer in map[string]any yaml: 'v: 0b10' type: map[string]any want: v: 2 - decode: name: hex integer in map[string]any yaml: 'v: 0xA' type: map[string]any want: v: 10 - decode: name: large integer in map[string]int64 yaml: 'v: 4294967296' type: map[string]int64 want: v: 4294967296 - decode: name: float in map[string]any yaml: 'v: 0.1' type: map[string]any want: v: 0.1 - decode: name: float with leading dot in map[string]any yaml: 'v: .1' type: map[string]any want: v: 0.1 - decode: name: positive infinity in map[string]any yaml: 'v: .Inf' type: map[string]any want: v: '+Inf' - decode: name: negative infinity in map[string]any yaml: 'v: -.Inf' type: map[string]any want: v: '-Inf' - decode: name: negative integer in map[string]any yaml: 'v: -10' type: map[string]any want: v: -10 - decode: name: negative float in map[string]any yaml: 'v: -.1' type: map[string]any want: v: -0.1 - decode: name: negative zero in map[string]any yaml: 'v: -0' type: map[string]any want: v: '-0' - decode: name: escaped newlines in map[string]string yaml: | a: "\t\n\t\n" type: map[string]string want: a: "\t\n\t\n" - decode: name: merge key as regular key yaml: | "<<": [] type: map[string]any want: '<<': [] - decode: name: merge key value as string yaml: | foo: "<<" type: map[string]any want: foo: '<<' - decode: name: canonical float yaml: 'canonical: 6.8523e+5' type: map[string]any want: canonical: 6.8523e+5 - decode: name: exponential float with underscores yaml: 'expo: 685.230_15e+03' type: map[string]any want: expo: 685.23015e+03 - decode: name: fixed float with underscores yaml: 'fixed: 685_230.15' type: map[string]any want: fixed: 685230.15 - decode: name: negative infinity yaml: 'neginf: -.inf' type: map[string]any want: neginf: '-Inf' - decode: name: fixed float in map[string]float64 yaml: 'fixed: 685_230.15' type: map[string]float64 want: fixed: 685230.15 # Bool tests - YAML 1.2 spec - decode: name: canonical true yaml: 'canonical: true' type: map[string]any want: canonical: true - decode: name: canonical false yaml: 'canonical: false' type: map[string]any want: canonical: false - decode: name: True with capital T yaml: 'bool: True' type: map[string]any want: bool: true - decode: name: False with capital F yaml: 'bool: False' type: map[string]any want: bool: false - decode: name: TRUE all caps yaml: 'bool: TRUE' type: map[string]any want: bool: true - decode: name: FALSE all caps yaml: 'bool: FALSE' type: map[string]any want: bool: false # Backwards compatibility with YAML 1.1 bool values - decode: name: on as bool (1.1 compat) yaml: 'option: on' type: map[string]bool want: option: true - decode: name: y as bool (1.1 compat) yaml: 'option: y' type: map[string]bool want: option: true - decode: name: Off as bool (1.1 compat) yaml: 'option: Off' type: map[string]bool want: option: false - decode: name: No as bool (1.1 compat) yaml: 'option: No' type: map[string]bool want: option: false - decode: name: non-bool string ignored in bool map yaml: 'option: other' type: map[string]bool want: {} # Integer tests from spec - decode: name: canonical integer yaml: 'canonical: 685230' type: map[string]any want: canonical: 685230 - decode: name: decimal with underscores and plus yaml: 'decimal: +685_230' type: map[string]any want: decimal: 685230 - decode: name: octal with leading zero yaml: 'octal: 02472256' type: map[string]any want: octal: 685230 - decode: name: negative octal with leading zero yaml: 'octal: -02472256' type: map[string]any want: octal: -685230 - decode: name: octal with 0o prefix yaml: 'octal: 0o2472256' type: map[string]any want: octal: 685230 - decode: name: negative octal with 0o prefix yaml: 'octal: -0o2472256' type: map[string]any want: octal: -685230 - decode: name: hex with underscores yaml: 'hexa: 0x_0A_74_AE' type: map[string]any want: hexa: 685230 - decode: name: binary with underscores yaml: 'bin: 0b1010_0111_0100_1010_1110' type: map[string]any want: bin: 685230 - decode: name: negative binary yaml: 'bin: -0b101010' type: map[string]any want: bin: -42 - decode: name: binary MinInt64 yaml: 'bin: -0b1000000000000000000000000000000000000000000000000000000000000000' type: map[string]any want: bin: MinInt64 - decode: name: decimal in map[string]int yaml: 'decimal: +685_230' type: map[string]int want: decimal: 685230 # Null tests from spec - decode: name: empty value as null yaml: 'empty:' type: map[string]any want: empty: null - decode: name: tilde as null yaml: 'canonical: ~' type: map[string]any want: canonical: null - decode: name: null keyword yaml: 'english: null' type: map[string]any want: english: null # Flow sequences - decode: name: flow sequence two items yaml: 'seq: [A,B]' type: map[string]any want: seq: [A, B] - decode: name: flow sequence with trailing comma yaml: 'seq: [A,B,C,]' type: map[string][]string want: seq: [A, B, C] - decode: name: flow sequence mixed types to string slice yaml: 'seq: [A,1,C]' type: map[string][]string want: seq: [A, '1', C] - decode: name: flow sequence mixed types to int slice yaml: 'seq: [A,1,C]' type: map[string][]int want: seq: [1] - decode: name: flow sequence mixed types yaml: 'seq: [A,1,C]' type: map[string]any want: seq: [A, 1, C] - decode: name: flow sequence with colon prefixed strings yaml: 'seq: [:A,1,:C]' type: map[string]any want: seq: [':A', 1, ':C'] - decode: name: flow sequence with inline map yaml: 'seq: [:: A,1,:C]' type: map[string]any want: seq: - ':': A - 1 - ':C' # Block sequences - decode: name: block sequence two items yaml: | seq: - A - B type: map[string]any want: seq: [A, B] - decode: name: block sequence to string slice yaml: | seq: - A - B - C type: map[string][]string want: seq: [A, B, C] - decode: name: block sequence mixed types to string slice yaml: | seq: - A - 1 - C type: map[string][]string want: seq: [A, '1', C] - decode: name: block sequence mixed types to int slice yaml: | seq: - A - 1 - C type: map[string][]int want: seq: [1] - decode: name: block sequence mixed types yaml: | seq: - A - 1 - C type: map[string]any want: seq: [A, 1, C] # Literal and folded scalars - decode: name: literal scalar yaml: | scalar: | # Comment literal text type: map[string]string want: scalar: "\nliteral\n\n\ttext\n" - decode: name: folded scalar yaml: | scalar: > # Comment folded line next line * one * two last line type: map[string]string want: scalar: "\nfolded line\nnext line\n * one\n * two\n\nlast line\n" # Maps - decode: name: nested map yaml: 'a: {b: c}' type: map[any]any want: a: b: c # Quoted values - decode: name: quoted strings as keys and values yaml: '''1'': ''"2"''' type: map[any]any want: '1': '"2"' - decode: name: multiline quoted string in sequence yaml: | v: - A - 'B C' type: map[string][]string want: v: [A, "B\nC"] # Uint and cross-type conversions - decode: name: unsigned int yaml: 'v: 42' type: map[string]uint want: v: 42 - decode: name: negative to uint fails yaml: 'v: -42' type: map[string]uint want: {} - decode: name: large uint64 yaml: 'v: 4294967296' type: map[string]uint64 want: v: 4294967296 - decode: name: negative to uint64 fails yaml: 'v: -4294967296' type: map[string]uint64 want: {} # Int boundary tests - decode: name: MaxInt32 yaml: 'int_max: 2147483647' type: map[string]int want: int_max: MaxInt32 - decode: name: MinInt32 yaml: 'int_min: -2147483648' type: map[string]int want: int_min: MinInt32 - decode: name: int overflow MaxInt64+1 yaml: 'int_overflow: 9223372036854775808' type: map[string]int want: {} # Int64 boundary tests - decode: name: MaxInt64 yaml: 'int64_max: 9223372036854775807' type: map[string]int64 want: int64_max: MaxInt64 - decode: name: MaxInt64 in binary yaml: 'int64_max_base2: 0b111111111111111111111111111111111111111111111111111111111111111' type: map[string]int64 want: int64_max_base2: MaxInt64 - decode: name: MinInt64 yaml: 'int64_min: -9223372036854775808' type: map[string]int64 want: int64_min: MinInt64 - decode: name: negative MaxInt64 in binary yaml: 'int64_neg_base2: -0b111111111111111111111111111111111111111111111111111111111111111' type: map[string]int64 want: int64_neg_base2: -9223372036854775807 - decode: name: int64 overflow MaxInt64+1 yaml: 'int64_overflow: 9223372036854775808' type: map[string]int64 want: {} # Uint boundary tests - decode: name: uint min yaml: 'uint_min: 0' type: map[string]uint want: uint_min: 0 - decode: name: MaxUint32 yaml: 'uint_max: 4294967295' type: map[string]uint want: uint_max: MaxUint32 - decode: name: uint underflow yaml: 'uint_underflow: -1' type: map[string]uint want: {} - decode: name: uint64 min yaml: 'uint64_min: 0' type: map[string]uint want: uint64_min: 0 - decode: name: MaxUint64 yaml: 'uint64_max: 18446744073709551615' type: map[string]uint64 want: uint64_max: MaxUint64 - decode: name: MaxUint64 in binary yaml: 'uint64_max_base2: 0b1111111111111111111111111111111111111111111111111111111111111111' type: map[string]uint64 want: uint64_max_base2: MaxUint64 - decode: name: MaxInt64 as uint64 yaml: 'uint64_maxint64: 9223372036854775807' type: map[string]uint64 want: uint64_maxint64: MaxInt64 - decode: name: uint64 underflow yaml: 'uint64_underflow: -1' type: map[string]uint64 want: {} # Float32 boundary tests - decode: name: MaxFloat32 yaml: 'float32_max: 3.40282346638528859811704183484516925440e+38' type: map[string]float32 want: float32_max: 3.4028235e+38 - decode: name: MaxUint64 as float32 yaml: 'float32_maxuint64: 18446744073709551615' type: map[string]float32 want: float32_maxuint64: 1.8446744e+19 - decode: name: MaxUint64+1 as float32 yaml: 'float32_maxuint64+1: 18446744073709551616' type: map[string]float32 want: float32_maxuint64+1: 1.8446744e+19 # Float64 boundary tests - decode: name: MaxFloat64 yaml: 'float64_max: 1.797693134862315708145274237317043567981e+308' type: map[string]float64 want: float64_max: MaxFloat64 - decode: name: SmallestNonzeroFloat64 yaml: 'float64_nonzero: 4.940656458412465441765687928682213723651e-324' type: map[string]float64 want: float64_nonzero: SmallestNonzeroFloat64 - decode: name: MaxUint64 as float64 yaml: 'float64_maxuint64: 18446744073709551615' type: map[string]float64 want: float64_maxuint64: 1.8446744073709552e+19 - decode: name: MaxUint64+1 as float64 yaml: 'float64_maxuint64+1: 18446744073709551616' type: map[string]float64 want: float64_maxuint64+1: 1.8446744073709552e+19 # Overflow cases - decode: name: int32 overflow yaml: 'v: 4294967297' type: map[string]int32 want: {} - decode: name: int8 overflow yaml: 'v: 128' type: map[string]int8 want: {} # Explicit tags - decode: name: explicit float tag on string yaml: "v: !!float '1.1'" type: map[string]any want: v: 1.1 - decode: name: explicit float tag on zero yaml: 'v: !!float 0' type: map[string]any want: v: 0.0 - decode: name: explicit float tag on negative one yaml: 'v: !!float -1' type: map[string]any want: v: -1.0 - decode: name: explicit null tag on empty string yaml: "v: !!null ''" type: map[string]any want: v: null - decode: name: TAG directive with int yaml: | %TAG !y! tag:yaml.org,2002: --- v: !y!int '1' type: map[string]any want: v: 1 - decode: name: non-specific tag yaml: 'v: ! test' type: map[string]any want: v: test # !!binary tags - decode: name: decoding !!binary into any yaml: 'v: !!binary MTIzCg==' type: map[string]any want: v: "123\n" - decode: name: decoding !!binary into string yaml: 'v: !!binary MTIzCg==' type: map[string]string want: v: "123\n" - decode: name: decoding !!binary into []byte yaml: 'v: !!binary MTIzCg==' type: map[string][]byte want: v: [0x31, 0x32, 0x33, 0x0A] # Anchors and aliases - decode: name: anchor as key with alias yaml: | foo: &bar bar *bar : quz type: map[string]any want: foo: bar bar: quz - decode: name: anchor with special characters yaml: | foo: &b./ar bar *b./ar : quz type: map[string]any want: foo: bar bar: quz - decode: name: alias reuse (yaml-test-suite 3GZX) yaml: | First occurrence: &anchor Foo Second occurrence: *anchor Override anchor: &anchor Bar Reuse anchor: *anchor type: map[string]any want: First occurrence: Foo Second occurrence: Foo Override anchor: Bar Reuse anchor: Bar # Flow sequences with special syntax - decode: name: flow sequence with colon-prefixed values yaml: 'seq: [:A,1,:C]' type: map[string]any want: seq: [':A', 1, ':C'] - decode: name: 'flow sequence with nested map using ::' yaml: 'seq: [:: A,1,:C]' type: map[string]any want: seq: - {':': A} - 1 - ':C' # String edge cases - decode: name: string with underscore after minus yaml: 'a: -b_c' type: map[string]any want: a: -b_c - decode: name: string with underscore after plus yaml: 'a: +b_c' type: map[string]any want: a: +b_c - decode: name: string starting with digit and underscore yaml: 'a: 50cent_of_dollar' type: map[string]any want: a: '50cent_of_dollar' # URLs and special strings - decode: name: URL in flow mapping yaml: 'a: {b: https://github.com/go-yaml/yaml}' type: map[string]any want: a: b: https://github.com/go-yaml/yaml - decode: name: URL in flow sequence yaml: 'a: [https://github.com/go-yaml/yaml]' type: map[string]any want: a: [https://github.com/go-yaml/yaml] - decode: name: angle brackets in string yaml: 'a: ' type: map[string]string want: a: - decode: name: base 60 float as string yaml: | a: 1:1 type: map[string]string want: a: '1:1' # Scientific notation - decode: name: scientific notation lowercase e yaml: | a: 123456e1 type: map[string]any want: a: 1.23456e+06 - decode: name: scientific notation uppercase E yaml: | a: 123456E1 type: map[string]any want: a: 1.23456e+06 # Unicode and encoding - decode: name: unicode line breaks yaml: | %YAML 1.1 --- !!str "Generic line break (no glyph)\n\ Generic line break (glyphed)\n\ Line separator\u2028\ Paragraph separator\u2029" type: string want: "Generic line break (no glyph)\nGeneric line break (glyphed)\nLine separator\u2028Paragraph separator\u2029" # Quoted multiline strings - decode: name: quoted multiline string in sequence yaml: | v: - A - 'B C' type: map[string][]string want: v: [A, "B\nC"] # Document directives - decode: name: document with trailing garbage yaml: | --- hello ... }not yaml type: string want: hello # Struct field mapping tests - decode: name: struct field Hello maps to yaml key hello yaml: 'hello: world' type: testStructHello want: hello: world - decode: name: nested struct yaml: 'a: {b: c}' type: testStructA_NestedB want: a: b: c - decode: name: nested struct pointer yaml: 'a: {b: c}' type: testStructA_NestedBPtr want: a: b: c - decode: name: struct with map field yaml: 'a: {b: c}' type: testStructA_MapStringString want: a: b: c - decode: name: struct with pointer to map yaml: 'a: {b: c}' type: testStructA_MapStringStringPtr want: a: b: c - decode: name: struct with empty map field yaml: 'a:' type: testStructA_MapStringString want: a: {} - decode: name: struct with int field yaml: 'a: 1' type: testStructA_Int want: a: 1 - decode: name: struct with float64 field from int yaml: 'a: 1' type: testStructA_Float64 want: a: 1 - decode: name: struct with int field from float yaml: 'a: 1.0' type: testStructA_Int want: a: 1 - decode: name: struct with uint field from float yaml: 'a: 1.0' type: testStructA_Uint want: a: 1 - decode: name: struct with int slice field yaml: 'a: [1, 2]' type: testStructA_IntSlice want: a: [1, 2] - decode: name: struct with int array field yaml: 'a: [1, 2]' type: testStructA_IntArray2 want: a: [1, 2] - decode: name: struct field mismatch ignored yaml: 'a: 1' type: testStructB_Int want: b: 0 - decode: name: struct with bool field YAML 1.1 YES yaml: 'a: YES' type: testStructA_Bool want: a: true - decode: name: struct with four int fields from anchors yaml: | a: &x 1 b: &y 2 c: *x d: *y type: testStructABCD_Int want: a: 1 b: 2 c: 1 d: 2 - decode: name: struct field B with anchor list yaml: | a: &a [1, 2] b: *a type: testStructB_IntSlice want: b: [1, 2] - decode: name: struct with empty slice field yaml: 'a: []' type: testStructA_IntSliceEmpty want: a: [] # Struct tests with yaml tags - decode: name: yaml tag field rename (B to a) yaml: 'a: 1' type: testStructB_Int_TagA want: a: 1 - decode: name: yaml tag ignored field yaml: | a: 1 b: 2 type: testStructAB_Int_BIgnored want: a: 1 - decode: name: inline struct field yaml: | a: 1 b: 2 c: 3 type: testStructA_Int_InlineB want: a: 1 b: 2 c: 3 - decode: name: inline struct pointer field yaml: | a: 1 b: 2 c: 3 type: testStructA_Int_InlineBPtr want: a: 1 b: 2 c: 3 - decode: name: inline struct pointer field nil yaml: 'a: 1' type: testStructA_Int_InlineBPtr want: a: 1 - decode: name: inline nested struct pointer yaml: | a: 1 c: 3 d: 4 type: testStructA_Int_InlineDPtr want: a: 1 c: 3 d: 4 - decode: name: inline map field yaml: | a: 1 b: 2 c: 3 type: testStructA_Int_InlineMapStringInt want: a: 1 b: 2 c: 3 # Anchor and alias tests - decode: name: anchor and alias basic yaml: | foo: &bar bar *bar : quz type: map[string]any want: foo: bar bar: quz - decode: name: anchor and alias with special characters yaml: | foo: &b./ar bar *b./ar : quz type: map[string]any want: foo: bar bar: quz # Null value handling - decode: name: null into map[string]string becomes empty string yaml: | foo: null type: map[string]string want: foo: '' - decode: name: null into map[string]any stays nil yaml: | foo: null type: map[string]any want: foo: null # Tilde as null - decode: name: tilde into map[string]string becomes empty string yaml: | foo: ~ type: map[string]string want: foo: '' - decode: name: tilde into map[string]any stays nil yaml: | foo: ~ type: map[string]any want: foo: null # Quoted timestamp strings - decode: name: quoted timestamp string stays as string yaml: | a: "2015-01-01" type: map[string]any want: a: '2015-01-01' # C1 control characters - decode: name: C1 control character U+0080 yaml: "\u0080" type: string want: "\u0080" - decode: name: C1 control character U+009F yaml: "\u009F" type: string want: "\u009F" # CRLF line endings - decode: name: CRLF line endings yaml: "a: b\r\nc:\r\n- d\r\n- e\r\n" type: map[string]any want: a: b c: - d - e # Question mark in values - decode: name: question mark in middle of key and value yaml: | foo: {ba?r: a?bc} type: map[string]any want: foo: ba?r: a?bc - decode: name: question mark at start of key and value yaml: | foo: {?bar: ?abc} type: map[string]any want: foo: '?bar': '?abc' - decode: name: question mark at end of key and value yaml: | foo: {bar?: abc?} type: map[string]any want: foo: bar?: abc? - decode: name: explicit key indicator with space yaml: | foo: {? key: value} type: map[string]any want: foo: key: value # TextUnmarshaler success cases - decode: name: Scalar string into TextUnmarshaler (value field) yaml: 'a: hello world' type: testStructA_TextUnmarshaler want: a: value: hello world - decode: name: Scalar string into TextUnmarshaler (pointer field) yaml: 'a: hello world' type: testStructA_TextUnmarshalerPtr want: a: value: hello world - decode: name: Scalar string into TextUnmarshaler (pointer-pointer field) yaml: 'a: hello world' type: testStructA_TextUnmarshalerPtrPtr want: a: value: hello world - decode: name: Null into TextUnmarshaler (value field) yaml: 'a: null' type: testStructA_TextUnmarshaler want: a: value: '' - decode: name: Null into TextUnmarshaler (pointer field) yaml: 'a: null' type: testStructA_TextUnmarshalerPtr want: a: null - decode: name: Null into TextUnmarshaler (pointer-pointer field) yaml: 'a: null' type: testStructA_TextUnmarshalerPtrPtr want: a: null - decode: name: Aliases and TextUnmarshaler yaml: | a: &foo hello world b: *foo type: testStructAB_TextUnmarshaler want: a: value: hello world b: value: hello world golang-go.yaml-yaml-v4-4.0.0~rc5/testdata/encode.yaml000066400000000000000000000447171521353103300223700ustar00rootroot00000000000000# Copyright 2025 The go-yaml Project Contributors # SPDX-License-Identifier: Apache-2.0 # Encode test cases for go-yaml # # Purpose: Tests YAML marshaling/encoding of Go values into YAML output. # Validates proper formatting, quoting, type representation, null handling, # special values, and YAML syntax correctness. # # Format: Test cases use type-as-key format: # - test-type: # name: Test case name # ... # # Test Types: # encode - Validates YAML marshaling of Go values # # Common Keys: # name - Test case name (string) # data - Go value to marshal (YAML representation) # type - Go type of the value (e.g., "map[string]any", "[]string") # want - Expected YAML output (string, typically using | for multiline) # # Data Types: # Map types: map[string]string, map[string]any, map[string]uint64, etc. # Slice types: []string, []int, []any, map[string][]string, etc. # Numeric types: map[string]int, map[string]uint, map[string]float64, etc. # # Special Values: # Constants like +Inf, -Inf, NaN are resolved via ValueRegistry during test # execution. These are represented as string tokens in the data field and # replaced with actual Go values (math.Inf, math.NaN) before marshaling. # # Note: # The test framework unmarshals the 'data' into the specified 'type', # then marshals it back to YAML and compares with 'want'. This ensures the # value can be properly constructed from YAML representation. # Basic string values - encode: name: string value in map[string]string data: v: hi type: map[string]string want: | v: hi - encode: name: string value in map[string]any data: v: hi type: map[string]any want: | v: hi # String values that look like booleans - encode: name: string "true" in map[string]string data: v: 'true' type: map[string]string want: | v: "true" - encode: name: string "false" in map[string]string data: v: 'false' type: map[string]string want: | v: "false" # Boolean values - encode: name: bool true in map[string]any data: v: true type: map[string]any want: | v: true - encode: name: bool false in map[string]any data: v: false type: map[string]any want: | v: false # Integer values - encode: name: positive integer in map[string]any data: v: 10 type: map[string]any want: | v: 10 - encode: name: negative integer in map[string]any data: v: -10 type: map[string]any want: | v: -10 - encode: name: uint value in map[string]uint data: v: 42 type: map[string]uint want: | v: 42 - encode: name: large int64 in map[string]any data: v: 4294967296 type: map[string]any want: | v: 4294967296 - encode: name: large int64 in map[string]int64 data: v: 4294967296 type: map[string]int64 want: | v: 4294967296 - encode: name: large uint64 in map[string]uint64 data: v: 4294967296 type: map[string]uint64 want: | v: 4294967296 # String that looks like a number - encode: name: string "10" in map[string]any data: v: '10' type: map[string]any want: | v: "10" # Float values - encode: name: float 0.1 in map[string]any data: v: 0.1 type: map[string]any want: | v: 0.1 - encode: name: explicit float64 0.1 data: v: 0.1 type: map[string]any want: | v: 0.1 - encode: name: float32 0.99 data: v: 0.99 type: map[string]any want: | v: 0.99 - encode: name: negative float in map[string]any data: v: -0.1 type: map[string]any want: | v: -0.1 # Special float values - encode: name: positive infinity data: v: +Inf type: map[string]any want: | v: .inf - encode: name: negative infinity data: v: -Inf type: map[string]any want: | v: -.inf - encode: name: NaN data: v: NaN type: map[string]any want: | v: .nan # Null and empty string - encode: name: null value in map[string]any data: v: null type: map[string]any want: | v: null - encode: name: empty string in map[string]any data: v: '' type: map[string]any want: | v: "" # String slices - encode: name: simple string slice data: v: [A, B] type: map[string][]string want: | v: - A - B - encode: name: string slice with multiline element data: v: ['A', "B\nC"] type: map[string][]string want: | v: - A - |- B C # Complex nested structure - encode: name: nested mixed slice with map data: v: ['A', 1, {B: [2, 3]}] type: map[string][]any want: | v: - A - 1 - B: - 2 - 3 # Issue #65: Multiline strings starting with newline - encode: name: string starting with newline in map data: v: "\nhi" type: map[string]any want: | v: |- hi - encode: name: nested structure with leading newline data: v: - v1: "\nhi" type: map[string][]map[string]any want: | v: - v1: |- hi # Nested maps - encode: name: nested map data: a: b: c type: map[string]any want: | a: b: c # Special string values requiring quoting - encode: name: dash string requiring quotes data: a: '-' type: map[string]any want: | a: '-' # Special characters and edge cases - encode: name: angle brackets in string data: a: '' type: map[string]string want: | a: - encode: name: colon-separated string (base 60 float compat) data: a: '1:1' type: map[string]string want: | a: '1:1' - encode: name: null byte in string data: a: "\x00" type: map[string]string want: | a: "\0" - encode: name: unicode string data: a: '你好' type: map[string]string want: | a: 你好 - encode: name: timestamp-like string gets quoted data: a: '2015-02-24T18:19:39Z' type: map[string]string want: | a: "2015-02-24T18:19:39Z" - encode: name: string containing colon-space data: a: 'b: c' type: map[string]string want: | a: 'b: c' - encode: name: string with hash comment data: a: 'Hello #comment' type: map[string]string want: | a: 'Hello #comment' - encode: name: unicode string with hash comment data: a: '你好 #comment' type: map[string]string want: | a: '你好 #comment' # Nested structures - encode: name: deeply nested maps and sequences data: a: b: - c: 1 d: 2 type: map[string]any want: | a: b: - c: 1 d: 2 # Tab handling - encode: name: string with only tabs and newlines (quoted) data: a: "\t\n\t\n" type: map[string]string want: | a: "\t\n\t\n" # Merge key edge cases - encode: name: merge key as regular key data: '<<': [] type: map[string]any want: | '<<': [] - encode: name: merge key as value data: foo: '<<' type: map[string]any want: | foo: '<<' # Long strings - encode: name: long string no wrapping data: a: 'abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 1234567890 abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 1234567890 ' type: map[string]string want: | a: 'abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 1234567890 abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 1234567890 ' # Struct encoding tests - encode: name: empty struct data: {} type: testStructEmpty want: | {} - encode: name: struct field Hello data: hello: world type: testStructHello want: | hello: world - encode: name: nested struct encoding data: a: b: c type: testStructA_NestedB want: | a: b: c - encode: name: nested struct pointer encoding data: a: b: c type: testStructA_NestedBPtr want: | a: b: c - encode: name: nested struct pointer null data: a: null type: testStructA_NestedBPtr want: | a: null - encode: name: struct int field data: a: 1 type: testStructA_Int want: | a: 1 - encode: name: struct int slice field data: a: [1, 2] type: testStructA_IntSlice want: | a: - 1 - 2 - encode: name: struct int array field data: a: [1, 2] type: testStructA_IntArray2 want: | a: - 1 - 2 - encode: name: struct bool field data: a: true type: testStructA_Bool want: | a: true - encode: name: struct string field true data: a: 'true' type: testStructA_String want: | a: "true" - encode: name: struct string field off data: a: off type: testStructA_String want: | a: 'off' # Struct tests with yaml tags - encode: name: yaml tag field rename (B to a) data: a: 1 type: testStructB_Int_TagA want: | a: 1 # Scalar style encoding tests (simple cases without tabs/unicode) - encode: name: scalar style - just newline (double quoted) data: "\n" type: string want: | "\n" - encode: name: scalar style - hello newline world (literal) data: "hello\nworld" type: string want: | |- hello world - encode: name: scalar style - simple hello (plain) data: hello type: string want: | hello - encode: name: scalar style - number-like 123 (quoted) data: '123' type: string want: | "123" - encode: name: scalar style - boolean-like true (quoted) data: 'true' type: string want: | "true" - encode: name: scalar style - long multiline text (literal) data: "This is a longer string\nwith multiple lines\nthat should use literal style" type: string want: | |- This is a longer string with multiple lines that should use literal style - encode: name: scalar style - multiline starting with space (literal with indent) data: " This starts with space\nand is long enough\nfor literal style" type: string want: | |4- This starts with space and is long enough for literal style - encode: name: scalar style - a newline (literal 2 chars) data: "a\n" type: string want: | | a - encode: name: scalar style - a newline b (literal 3 chars) data: "a\nb" type: string want: | |- a b - encode: name: scalar style - space a newline (literal with content) data: " a\n" type: string want: | |4 a # Whitespace-only strings (all simple cases) - encode: name: whitespace only - just newline (double quoted) data: "\n" type: string want: | "\n" - encode: name: whitespace only - two newlines (double quoted) data: "\n\n" type: string want: | "\n\n" - encode: name: whitespace only - space newline (double quoted) data: " \n" type: string want: | " \n" - encode: name: whitespace only - tab newline (double quoted) data: "\t\n" type: string want: | "\t\n" - encode: name: whitespace only - space newline space (double quoted) data: " \n " type: string want: | " \n " - encode: name: whitespace only - newline space newline (double quoted) data: "\n \n" type: string want: | "\n \n" - encode: name: whitespace only - tab space newline tab (double quoted) data: "\t \n\t" type: string want: | "\t \n\t" - encode: name: whitespace only - multiple spaces newline spaces (double quoted) data: " \n " type: string want: | " \n " - encode: name: whitespace only - three newlines (double quoted) data: "\n\n\n" type: string want: | "\n\n\n" - encode: name: whitespace only - space tab newline space tab (double quoted) data: " \t\n \t" type: string want: | " \t\n \t" # Whitespace with content (simple cases without leading tabs) - encode: name: whitespace with content - hello newline (literal) data: "hello\n" type: string want: | | hello - encode: name: whitespace with content - space hello newline (literal with indent) data: " hello\n" type: string want: | |4 hello - encode: name: whitespace with content - space newline hello (double quoted short) data: " \nhello" type: string want: | " \nhello" - encode: name: whitespace with content - spaces hello spaces newline (double quoted trailing) data: " hello \n" type: string want: | " hello \n" - encode: name: whitespace with content - hello spaces newline (double quoted trailing) data: "hello \n" type: string want: | "hello \n" - encode: name: whitespace with content - hello newline spaces (double quoted trailing) data: "hello\n " type: string want: | "hello\n " # WithQuotePreference tests - QuoteSingle (v4 default) - encode-opts: name: QuoteSingle - string "true" data: v: 'true' type: map[string]string opts: required-quotes: single want: | v: 'true' - encode-opts: name: QuoteSingle - string "false" data: v: 'false' type: map[string]string opts: required-quotes: single want: | v: 'false' - encode-opts: name: QuoteSingle - string "null" data: v: 'null' type: map[string]string opts: required-quotes: single want: | v: 'null' - encode-opts: name: QuoteSingle - string "123" data: v: '123' type: map[string]string opts: required-quotes: single want: | v: '123' - encode-opts: name: QuoteSingle - leading whitespace data: v: ' hello' type: map[string]string opts: required-quotes: single want: | v: ' hello' - encode-opts: name: QuoteSingle - trailing whitespace data: v: 'hello ' type: map[string]string opts: required-quotes: single want: | v: 'hello ' - encode-opts: name: QuoteSingle - syntax character (colon-space) data: v: ': hello' type: map[string]string opts: required-quotes: single want: | v: ': hello' - encode-opts: name: QuoteSingle - dash string data: v: '-' type: map[string]string opts: required-quotes: single want: | v: '-' # WithQuotePreference tests - QuoteDouble - encode-opts: name: QuoteDouble - string "true" data: v: 'true' type: map[string]string opts: required-quotes: double want: | v: "true" - encode-opts: name: QuoteDouble - string "false" data: v: 'false' type: map[string]string opts: required-quotes: double want: | v: "false" - encode-opts: name: QuoteDouble - string "null" data: v: 'null' type: map[string]string opts: required-quotes: double want: | v: "null" - encode-opts: name: QuoteDouble - string "123" data: v: '123' type: map[string]string opts: required-quotes: double want: | v: "123" - encode-opts: name: QuoteDouble - leading whitespace data: v: ' hello' type: map[string]string opts: required-quotes: double want: | v: " hello" - encode-opts: name: QuoteDouble - trailing whitespace data: v: 'hello ' type: map[string]string opts: required-quotes: double want: | v: "hello " - encode-opts: name: QuoteDouble - syntax character (colon-space) data: v: ': hello' type: map[string]string opts: required-quotes: double want: | v: ": hello" - encode-opts: name: QuoteDouble - dash string data: v: '-' type: map[string]string opts: required-quotes: double want: | v: "-" # WithQuotePreference tests - QuoteLegacy (v3 behavior) # Legacy: bool-like strings (representer) → double quotes # Legacy: whitespace strings (emitter) → single quotes - encode-opts: name: QuoteLegacy - string "true" (double from representer) data: v: 'true' type: map[string]string opts: required-quotes: legacy want: | v: "true" - encode-opts: name: QuoteLegacy - string "false" (double from representer) data: v: 'false' type: map[string]string opts: required-quotes: legacy want: | v: "false" - encode-opts: name: QuoteLegacy - string "null" (double from representer) data: v: 'null' type: map[string]string opts: required-quotes: legacy want: | v: "null" - encode-opts: name: QuoteLegacy - string "123" (double from representer) data: v: '123' type: map[string]string opts: required-quotes: legacy want: | v: "123" - encode-opts: name: QuoteLegacy - leading whitespace (single from emitter) data: v: ' hello' type: map[string]string opts: required-quotes: legacy want: | v: ' hello' - encode-opts: name: QuoteLegacy - trailing whitespace (single from emitter) data: v: 'hello ' type: map[string]string opts: required-quotes: legacy want: | v: 'hello ' - encode-opts: name: QuoteLegacy - syntax character (single from emitter) data: v: ': hello' type: map[string]string opts: required-quotes: legacy want: | v: ': hello' - encode-opts: name: QuoteLegacy - dash string (single from emitter) data: v: '-' type: map[string]string opts: required-quotes: legacy want: | v: '-' # Plain strings should remain unquoted regardless of quote preference - encode-opts: name: QuoteSingle - plain string stays plain data: v: hello type: map[string]string opts: required-quotes: single want: | v: hello - encode-opts: name: QuoteDouble - plain string stays plain data: v: hello type: map[string]string opts: required-quotes: double want: | v: hello golang-go.yaml-yaml-v4-4.0.0~rc5/testdata/fuzz_crashers.yaml000066400000000000000000000045151521353103300240130ustar00rootroot00000000000000# Copyright 2025 The go-yaml Project Contributors # SPDX-License-Identifier: Apache-2.0 # Fuzz crasher test cases for go-yaml # # Purpose: Tests malformed YAML inputs discovered through fuzzing that previously # caused crashes or panics. Ensures the parser handles edge cases gracefully without # crashing, even for invalid or pathological input. # # Format: Test cases use type-as-key format: # - test-type: # name: Test case name # ... # # Test Types: # fuzz-crasher - Verifies that unmarshaling malformed input doesn't crash # # Common Keys: # name - Test case name (string) # yaml - Malformed YAML input to test (string) # # Note: # These tests verify the parser doesn't crash or panic when processing # malformed input. They may produce errors, but should handle them gracefully. - fuzz-crasher: name: Runtime error index out of range (null escape) yaml: "\"\\0\\\r\n" - fuzz-crasher: name: Should not happen - flow sequence in mapping yaml: " 0: [\n] 0" - fuzz-crasher: name: Should not happen - double question marks yaml: "? ? \"\n\" 0" - fuzz-crasher: name: Should not happen - flow mapping in list yaml: " - {\n000}0" - fuzz-crasher: name: Should not happen - nested flow sequence yaml: "0:\n 0: [0\n] 0" - fuzz-crasher: name: Should not happen - quoted value in list with trailing yaml: " - \"\n000\"0" - fuzz-crasher: name: Should not happen - quoted value in list with trailing quotes yaml: " - \"\n000\"\"" - fuzz-crasher: name: Should not happen - nested flow mapping in list yaml: "0:\n - {\n000}0" - fuzz-crasher: name: Should not happen - nested quoted value in list with trailing yaml: "0:\n - \"\n000\"0" - fuzz-crasher: name: Should not happen - nested quoted value in list with trailing quotes yaml: "0:\n - \"\n000\"\"" - fuzz-crasher: name: Runtime error index out of range (BOM with space) yaml: " \uFEFF\n" - fuzz-crasher: name: Runtime error index out of range (BOM with question) yaml: "? \uFEFF\n" - fuzz-crasher: name: Runtime error index out of range (BOM in key) yaml: "? \uFEFF:\n" - fuzz-crasher: name: Runtime error index out of range (BOM in value) yaml: "0: \uFEFF\n" - fuzz-crasher: name: Runtime error index out of range (BOM in both) yaml: "? \uFEFF: \uFEFF\n" golang-go.yaml-yaml-v4-4.0.0~rc5/testdata/fuzz_json_roundtrip.yaml000066400000000000000000000032461521353103300252600ustar00rootroot00000000000000# Copyright 2025 The go-yaml Project Contributors # SPDX-License-Identifier: Apache-2.0 # JSON roundtrip fuzz test seed corpus for go-yaml # # Purpose: Seed inputs for FuzzEncodeFromJSON. These are valid JSON values that # are tested for YAML roundtrip consistency: Marshal → Unmarshal → Marshal # should produce identical YAML output. # # Format: Test cases use type-as-key format: # - test-type: # name: Test case name # ... # # Test Type: # json-roundtrip - Valid JSON input for roundtrip testing # # Keys: # name - Test case name (string) # json - JSON input to test (string) # # Note: # These test cases are loaded as the seed corpus for Go's native fuzzing. # When run without -fuzz, only these seeds are tested. With -fuzz, the fuzzer # generates additional test cases based on these seeds. - json-roundtrip: name: null value json: "null" - json-roundtrip: name: empty string json: '""' - json-roundtrip: name: zero json: "0" - json-roundtrip: name: boolean true json: "true" - json-roundtrip: name: boolean false json: "false" - json-roundtrip: name: empty object json: "{}" - json-roundtrip: name: empty array json: "[]" - json-roundtrip: name: nested empty array json: "[[]]" - json-roundtrip: name: object with empty array value json: '{"a":[]}' - json-roundtrip: name: object with empty object value json: '{"a":{}}' - json-roundtrip: name: negative zero json: "-0" - json-roundtrip: name: negative zero with decimals json: "-0.000" - json-roundtrip: name: newline string json: '"\n"' - json-roundtrip: name: tab string json: '"\t"' golang-go.yaml-yaml-v4-4.0.0~rc5/testdata/limit.yaml000066400000000000000000000103341521353103300222350ustar00rootroot00000000000000# Copyright 2025 The go-yaml Project Contributors # SPDX-License-Identifier: Apache-2.0 # Limit test cases for go-yaml # # Purpose: Tests YAML parser limits for deeply nested structures, excessive aliasing, # and large documents. Validates that the parser properly enforces depth limits, # handles large documents efficiently, and detects excessive resource usage. # # Format: Test cases use type-as-key format: # - test-type: # name: Test case name # ... # # Test Types: # limit-error - Tests that should fail with an error at resource limits # limit-pass - Tests that should successfully parse at resource limits # # Common Keys: # name - Test case name (string) # data - YAML input specification (either direct string or generator spec) # want - Expected value: # - For limit-error: error message string # - For limit-pass: {type: sequence|mapping} to validate top-level structure # # Data Specification: # Direct YAML string (rare for limit tests): # data: "yaml content" # # Generated data using 'join' (concatenate parts): # data: # join: # - text: "string" # Literal string to include # - loop: ["string", N] # Repeat string N times # loop: M # Optional: repeat entire join M times # # Generated data using 'loop' (simple repeat): # data: # loop: ["string", N] # Repeat string N times # # Limits Tested: # - Max depth: 10000 levels (flow, indent) # - Aliasing: Excessive alias expansion detection # - Document size: Various sizes (1kb, 10kb, 100kb, 1000kb) - limit-error: name: 1000kb of maps with 100 aliases data: join: - text: '{a: &a [{a}' - loop: [',{a}', 262044] # 1000*1024/4-100 - text: '], b: &b [*a' - loop: [',*a', 99] - text: ']}' want: 'go-yaml load error in constructor at L[0-9]+.C[0-9]+: document contains excessive aliasing' - limit-error: name: 1000kb of deeply nested slices data: loop: ['[', 1024000] # 1000*1024 want: 'go-yaml load error in scanner \(while increasing flow level\) at L1.C10001: exceeded max depth of 10000' - limit-error: name: 1000kb of deeply nested maps data: join: - text: 'x: ' - loop: ['{', 1024000] # 1000*1024 want: 'go-yaml load error in scanner \(while increasing flow level\) at L1.C10004: exceeded max depth of 10000' - limit-error: name: 1000kb of deeply nested indents data: loop: ['- ', 1024000] # 1000*1024 want: 'go-yaml load error in scanner \(while increasing indent level\) at -L1.C20001: exceeded max depth of 10000' - limit-pass: name: 1000kb of 1000-indent lines data: join: - loop: ['- ', 1000] - text: "\n" loop: 512 # 1024/2 want: {type: sequence} - limit-pass: name: 1kb of maps data: join: - text: 'a: &a [{a}' - loop: [',{a}', 255] # 1*1024/4-1 - text: ']' want: {type: mapping} - limit-pass: name: 10kb of maps data: join: - text: 'a: &a [{a}' - loop: [',{a}', 2559] # 10*1024/4-1 - text: ']' want: {type: mapping} - limit-pass: name: 100kb of maps data: join: - text: 'a: &a [{a}' - loop: [',{a}', 25599] # 100*1024/4-1 - text: ']' want: {type: mapping} - limit-pass: name: 1000kb of maps data: join: - text: 'a: &a [{a}' - loop: [',{a}', 255999] # 1000*1024/4-1 - text: ']' want: {type: mapping} - limit-pass: name: 1000kb slice nested at max-depth data: join: - loop: ['[', 10000] - text: '1' - loop: [',1', 491999] # 1000*1024/2-20000-1 - loop: [']', 10000] want: {type: sequence} - limit-pass: name: 1000kb slice nested in maps at max-depth data: join: - text: "{a,b:\n" - loop: [' {a,b:', 9998] # 10000-2 - text: ' [1' - loop: [',1', 451999] # 1000*1024/2-6*10000-1 - text: ']' - loop: ['}', 9999] # 10000-1 want: {type: mapping} - limit-pass: name: 1000kb of 10000-nested lines data: join: - text: '- ' - loop: ['[', 10000] - loop: [']', 10000] - text: "\n" loop: 51 # 1000*1024/20000 want: {type: sequence} golang-go.yaml-yaml-v4-4.0.0~rc5/testdata/node.yaml000066400000000000000000001537341521353103300220600ustar00rootroot00000000000000# Copyright 2025 The go-yaml Project Contributors # SPDX-License-Identifier: Apache-2.0 # Node test cases for go-yaml # # Purpose: Tests YAML node structure, including Kind, Style, Tag, Value, and nested # Content. Validates comment preservation (HeadComment, LineComment, FootComment) # and node-level encoding/decoding. # # Format: Test cases use type-as-key format: # - node-test: # name: Test case name # yaml: YAML input string # node: Expected node structure (as NodeInfo) # decode: false # Optional: skip decode test # encode: false # Optional: skip encode test # # Test Types: # node-test - Validates YAML node structure and round-trip encoding/decoding # # Common Keys: # name - Test case name (string) # yaml - Input YAML string to parse # node - Expected node structure in NodeInfo format # decode - Set to false to skip decode test (default: true) # encode - Set to false to skip encode test (default: true) # # NodeInfo Format: # kind - Node kind: Document, Sequence, Mapping, Scalar, Alias # style - Node style: Double, Single, Literal, Folded, Flow (optional) # anchor - Anchor name (optional) # tag - YAML tag like !!null, !!str, !!int (optional) # head - HeadComment text (optional) # line - LineComment text (optional) # foot - FootComment text (optional) # text - Scalar value (for Scalar nodes only) # content - Array of child nodes (for Document, Sequence, Mapping) - node-test: name: null value yaml: 'null ' node: kind: Document content: - kind: Scalar tag: '!!null' text: 'null' - node-test: name: plain string yaml: 'foo ' node: kind: Document content: - kind: Scalar text: foo - node-test: name: double quoted string yaml: '"foo" ' node: kind: Document content: - kind: Scalar style: Double text: foo - node-test: name: single quoted string yaml: '''foo'' ' node: kind: Document content: - kind: Scalar style: Single text: foo - node-test: name: boolean true yaml: 'true ' node: kind: Document content: - kind: Scalar tag: '!!bool' text: 'true' - node-test: name: negative integer yaml: '-10 ' node: kind: Document content: - kind: Scalar tag: '!!int' text: '-10' - node-test: name: large integer yaml: '4294967296 ' node: kind: Document content: - kind: Scalar tag: '!!int' text: '4294967296' - node-test: name: float yaml: '0.1000 ' node: kind: Document content: - kind: Scalar tag: '!!float' text: '0.1000' - node-test: name: empty single quoted string yaml: ''''' ' node: kind: Document content: - kind: Scalar style: Single text: '' - node-test: name: double quoted with escapes yaml: '"\t\n" ' node: kind: Document content: - kind: Scalar style: Double text: "\t\n" - node-test: name: literal style multiline yaml: "|\n foo\n bar\n" node: kind: Document content: - kind: Scalar style: Literal text: 'foo bar ' - node-test: name: explicit str tag yaml: '!!str 123 ' decode: true encode: false node: kind: Document content: - kind: Scalar tag: '!!str' text: '123' - node-test: name: negative zero yaml: '-0 ' node: kind: Document content: - kind: Scalar tag: '!!float' text: '-0' - node-test: name: mapping with sequence of mappings yaml: "a:\n - b: c\n d: e\n" node: kind: Document content: - kind: Mapping content: - kind: Scalar text: a - kind: Sequence content: - kind: Mapping content: - kind: Scalar text: b - kind: Scalar text: c - kind: Scalar text: d - kind: Scalar text: e - node-test: name: flow mapping nested in mapping yaml: 'foo: {b?r: a?bc} ' decode: true encode: false node: kind: Document content: - kind: Mapping content: - kind: Scalar text: foo - kind: Mapping style: Flow content: - kind: Scalar text: b?r - kind: Scalar text: a?bc - node-test: name: line comment on key yaml: "a: # AI\n - b\nc:\n - d\n" node: kind: Document content: - kind: Mapping content: - kind: Scalar text: a line: '# AI' - kind: Sequence content: - kind: Scalar text: b - kind: Scalar text: c - kind: Sequence content: - kind: Scalar text: d - node-test: name: simple anchor and alias decode: true encode: false yaml: 'a: &x 1 b: *x ' node: kind: Document content: - kind: Mapping content: - kind: Scalar text: a - kind: Scalar anchor: x tag: '!!int' text: '1' - kind: Scalar text: b - kind: Alias - node-test: name: anchor on sequence decode: true encode: false yaml: 'a: &anchor [1, 2] b: *anchor ' node: kind: Document content: - kind: Mapping content: - kind: Scalar text: a - kind: Sequence style: Flow anchor: anchor content: - kind: Scalar tag: '!!int' text: '1' - kind: Scalar tag: '!!int' text: '2' - kind: Scalar text: b - kind: Alias - node-test: name: flow sequence with mixed types yaml: '[1, true, foo] ' node: kind: Document content: - kind: Sequence style: Flow content: - kind: Scalar tag: '!!int' text: '1' - kind: Scalar tag: '!!bool' text: 'true' - kind: Scalar text: foo - node-test: name: folded multiline string decode: true encode: false yaml: ">\n foo\n bar\n" node: kind: Document content: - kind: Scalar style: Folded text: 'foo bar ' - node-test: name: block sequence with scalars yaml: '- one - two - three ' node: kind: Document content: - kind: Sequence content: - kind: Scalar text: one - kind: Scalar text: two - kind: Scalar text: three - node-test: name: empty mapping value yaml: 'a: ' node: kind: Document content: - kind: Mapping content: - kind: Scalar text: a - kind: Scalar tag: '!!null' text: '' - node-test: name: mapping with empty string value yaml: 'a: '''' ' node: kind: Document content: - kind: Mapping content: - kind: Scalar text: a - kind: Scalar style: Single - node-test: name: flow mapping with multiple entries yaml: '{a: 1, b: 2} ' node: kind: Document content: - kind: Mapping style: Flow content: - kind: Scalar text: a - kind: Scalar tag: '!!int' text: '1' - kind: Scalar text: b - kind: Scalar tag: '!!int' text: '2' - node-test: name: head comment on mapping key yaml: "a:\n # HM\n b: c\n" decode: true encode: false node: kind: Document content: - kind: Mapping content: - kind: Scalar text: a - kind: Mapping content: - kind: Scalar head: '# HM' text: b - kind: Scalar text: c - node-test: name: literal style preserves trailing newline yaml: "|\n foo\n" node: kind: Document content: - kind: Scalar style: Literal text: 'foo ' - node-test: name: literal style strips final newlines yaml: "|-\n foo\n" node: kind: Document content: - kind: Scalar style: Literal text: foo - node-test: name: double quoted with newline escape yaml: '"foo\nbar" ' node: kind: Document content: - kind: Scalar style: Double text: 'foo bar' - node-test: name: single quoted with doubled single quote yaml: '''foo''''bar'' ' node: kind: Document content: - kind: Scalar style: Single text: foo'bar - node-test: name: sequence of sequences yaml: '- [a, b] - [c, d] ' node: kind: Document content: - kind: Sequence content: - kind: Sequence style: Flow content: - kind: Scalar text: a - kind: Scalar text: b - kind: Sequence style: Flow content: - kind: Scalar text: c - kind: Scalar text: d - node-test: name: mapping of mappings yaml: 'a: {b: 1} c: {d: 2} ' node: kind: Document content: - kind: Mapping content: - kind: Scalar text: a - kind: Mapping style: Flow content: - kind: Scalar text: b - kind: Scalar tag: '!!int' text: '1' - kind: Scalar text: c - kind: Mapping style: Flow content: - kind: Scalar text: d - kind: Scalar tag: '!!int' text: '2' - node-test: name: all comment types on scalar yaml: '# One # Two true # Three # Four # Five ' node: kind: Document content: - kind: Scalar tag: '!!bool' text: 'true' head: '# One # Two' line: '# Three' foot: '# Four # Five' - node-test: name: unicode in comments yaml: '# š true # š ' node: kind: Document content: - kind: Scalar tag: '!!bool' text: 'true' head: '# š' line: '# š' - node-test: name: custom tag on scalar yaml: '!tag:something 123 ' node: kind: Document content: - kind: Scalar tag: '!tag:something' text: '123' - node-test: name: custom tag on empty mapping yaml: '!tag:something {} ' node: kind: Document content: - kind: Mapping style: Flow tag: '!tag:something' - node-test: name: custom tag on empty sequence yaml: '!tag:something [] ' node: kind: Document content: - kind: Sequence style: Flow tag: '!tag:something' - node-test: name: merge key as quoted string with sequence yaml: '"<<": [] ' node: kind: Document content: - kind: Mapping content: - kind: Scalar style: Double text: '<<' - kind: Sequence style: Flow - node-test: name: merge key as value yaml: 'foo: "<<" ' node: kind: Document content: - kind: Mapping content: - kind: Scalar text: foo - kind: Scalar style: Double text: '<<' - node-test: name: simple two element sequence yaml: '- a - b ' node: kind: Document content: - kind: Sequence content: - kind: Scalar text: a - kind: Scalar text: b - node-test: name: nested block sequences yaml: "- a\n- - b\n - c\n" node: kind: Document content: - kind: Sequence content: - kind: Scalar text: a - kind: Sequence content: - kind: Scalar text: b - kind: Scalar text: c - node-test: name: flow sequence with strings yaml: '[a, b] ' node: kind: Document content: - kind: Sequence style: Flow content: - kind: Scalar text: a - kind: Scalar text: b - node-test: name: block sequence with nested flow sequence yaml: '- a - [b, c] ' node: kind: Document content: - kind: Sequence content: - kind: Scalar text: a - kind: Sequence style: Flow content: - kind: Scalar text: b - kind: Scalar text: c - node-test: name: positive infinity yaml: '.inf ' node: kind: Document content: - kind: Scalar tag: '!!float' text: '.inf' - node-test: name: explicit null yaml: '~ ' node: kind: Document content: - kind: Scalar tag: '!!null' text: '~' - node-test: name: flow sequence with all comment types yaml: '# H1 [la, lb] # I # F1 ' node: kind: Document content: - kind: Sequence style: Flow head: '# H1' line: '# I' foot: '# F1' content: - kind: Scalar text: la - kind: Scalar text: lb - node-test: name: boolean false yaml: 'false ' node: kind: Document content: - kind: Scalar tag: '!!bool' text: 'false' - node-test: name: uppercase NULL yaml: 'NULL ' node: kind: Document content: - kind: Scalar tag: '!!null' text: 'NULL' - node-test: name: empty flow mapping yaml: '{} ' node: kind: Document content: - kind: Mapping style: Flow - node-test: name: empty flow sequence yaml: '[] ' node: kind: Document content: - kind: Sequence style: Flow - node-test: name: positive integer yaml: '42 ' node: kind: Document content: - kind: Scalar tag: '!!int' text: '42' - node-test: name: zero yaml: '0 ' node: kind: Document content: - kind: Scalar tag: '!!int' text: '0' - node-test: name: folded style keep chomping yaml: ">+\n foo\n" decode: true encode: false node: kind: Document content: - kind: Scalar style: Folded text: 'foo ' - node-test: name: simple key-value mapping yaml: 'a: b ' node: kind: Document content: - kind: Mapping content: - kind: Scalar text: a - kind: Scalar text: b - node-test: name: mapping with integer value yaml: 'a: 1 ' node: kind: Document content: - kind: Mapping content: - kind: Scalar text: a - kind: Scalar tag: '!!int' text: '1' - node-test: name: mapping with null value implicit yaml: 'a: ' node: kind: Document content: - kind: Mapping content: - kind: Scalar text: a - kind: Scalar tag: '!!null' text: '' - node-test: name: double quoted with tab escape yaml: '"\t" ' node: kind: Document content: - kind: Scalar style: Double text: "\t" - node-test: name: two-level nested mapping yaml: "a:\n b: c\n" node: kind: Document content: - kind: Mapping content: - kind: Scalar text: a - kind: Mapping content: - kind: Scalar text: b - kind: Scalar text: c - node-test: name: mapping with multiple keys yaml: 'a: 1 b: 2 c: 3 ' node: kind: Document content: - kind: Mapping content: - kind: Scalar text: a - kind: Scalar tag: '!!int' text: '1' - kind: Scalar text: b - kind: Scalar tag: '!!int' text: '2' - kind: Scalar text: c - kind: Scalar tag: '!!int' text: '3' - node-test: name: float with exponent yaml: '1.23e+2 ' node: kind: Document content: - kind: Scalar tag: '!!float' text: '1.23e+2' - node-test: name: negative float yaml: '-3.14 ' node: kind: Document content: - kind: Scalar tag: '!!float' text: '-3.14' - node-test: name: uppercase TRUE yaml: 'TRUE ' node: kind: Document content: - kind: Scalar tag: '!!bool' text: 'TRUE' - node-test: name: uppercase FALSE yaml: 'FALSE ' node: kind: Document content: - kind: Scalar tag: '!!bool' text: 'FALSE' - node-test: name: three-element sequence yaml: '- a - b - c ' node: kind: Document content: - kind: Sequence content: - kind: Scalar text: a - kind: Scalar text: b - kind: Scalar text: c - node-test: name: sequence with integers yaml: '- 1 - 2 - 3 ' node: kind: Document content: - kind: Sequence content: - kind: Scalar tag: '!!int' text: '1' - kind: Scalar tag: '!!int' text: '2' - kind: Scalar tag: '!!int' text: '3' - node-test: name: flow sequence with integers yaml: '[1, 2, 3] ' node: kind: Document content: - kind: Sequence style: Flow content: - kind: Scalar tag: '!!int' text: '1' - kind: Scalar tag: '!!int' text: '2' - kind: Scalar tag: '!!int' text: '3' - node-test: name: quoted key with spaces yaml: '"key with spaces": value ' node: kind: Document content: - kind: Mapping content: - kind: Scalar style: Double text: key with spaces - kind: Scalar text: value - node-test: name: flow mapping with flow sequence value yaml: '{a: [1, 2]} ' node: kind: Document content: - kind: Mapping style: Flow content: - kind: Scalar text: a - kind: Sequence style: Flow content: - kind: Scalar tag: '!!int' text: '1' - kind: Scalar tag: '!!int' text: '2' - node-test: name: flow sequence with flow mapping yaml: '[{a: 1}] ' node: kind: Document content: - kind: Sequence style: Flow content: - kind: Mapping style: Flow content: - kind: Scalar text: a - kind: Scalar tag: '!!int' text: '1' - node-test: name: block mapping with flow value yaml: 'a: [1, 2] ' node: kind: Document content: - kind: Mapping content: - kind: Scalar text: a - kind: Sequence style: Flow content: - kind: Scalar tag: '!!int' text: '1' - kind: Scalar tag: '!!int' text: '2' - node-test: name: block mapping with flow mapping value yaml: 'a: {b: c} ' node: kind: Document content: - kind: Mapping content: - kind: Scalar text: a - kind: Mapping style: Flow content: - kind: Scalar text: b - kind: Scalar text: c - node-test: name: three-level nested mapping yaml: "a:\n b:\n c: d\n" node: kind: Document content: - kind: Mapping content: - kind: Scalar text: a - kind: Mapping content: - kind: Scalar text: b - kind: Mapping content: - kind: Scalar text: c - kind: Scalar text: d - node-test: name: empty double quoted string yaml: '"" ' node: kind: Document content: - kind: Scalar style: Double text: '' - node-test: name: mapping with empty key yaml: '"": value ' node: kind: Document content: - kind: Mapping content: - kind: Scalar style: Double text: '' - kind: Scalar text: value # Values with spaces - node-test: name: mapping with multi-word value yaml: "a: b c\n" node: kind: Document content: - kind: Mapping content: [{kind: Scalar, text: a}, {kind: Scalar, text: 'b c'}] # More nested structures - node-test: name: sequence in sequence in mapping decode: true encode: false yaml: "a:\n- - b\n" node: kind: Document content: - kind: Mapping content: - {kind: Scalar, text: a} - kind: Sequence content: - kind: Sequence content: [{kind: Scalar, text: b}] # Strings that look like other types - node-test: name: string that looks like number yaml: "\"123\"\n" node: kind: Document content: - {kind: Scalar, style: Double, text: '123'} - node-test: name: string that looks like boolean yaml: "\"true\"\n" node: kind: Document content: - {kind: Scalar, style: Double, text: 'true'} # Hex and octal numbers - node-test: name: hex integer yaml: "0x1a\n" node: kind: Document content: - {kind: Scalar, tag: '!!int', text: '0x1a'} - node-test: name: octal integer yaml: "0o10\n" node: kind: Document content: - {kind: Scalar, tag: '!!int', text: '0o10'} # More flow collections - node-test: name: nested flow mappings yaml: "{a: {b: c}}\n" node: kind: Document content: - kind: Mapping style: Flow content: - {kind: Scalar, text: a} - kind: Mapping style: Flow content: [{kind: Scalar, text: b}, {kind: Scalar, text: c}] - node-test: name: nested flow sequences yaml: "[[a, b]]\n" node: kind: Document content: - kind: Sequence style: Flow content: - kind: Sequence style: Flow content: [{kind: Scalar, text: a}, {kind: Scalar, text: b}] # Comments on flow collections - node-test: name: flow mapping with comment yaml: "{a: b} # comment\n" node: kind: Document content: - kind: Mapping style: Flow line: '# comment' content: [{kind: Scalar, text: a}, {kind: Scalar, text: b}] # Multiple mappings in document - node-test: name: two top-level mappings yaml: "a: 1\nb: 2\n" node: kind: Document content: - kind: Mapping content: - {kind: Scalar, text: a} - {kind: Scalar, tag: '!!int', text: '1'} - {kind: Scalar, text: b} - {kind: Scalar, tag: '!!int', text: '2'} # Colon in values - node-test: name: colon in quoted value yaml: "url: \"http://example.com\"\n" node: kind: Document content: - kind: Mapping content: - {kind: Scalar, text: url} - {kind: Scalar, style: Double, text: 'http://example.com'} # Leading/trailing spaces - node-test: name: value with leading space yaml: "a: \" b\"\n" node: kind: Document content: - kind: Mapping content: - {kind: Scalar, text: a} - {kind: Scalar, style: Double, text: ' b'} - node-test: name: value with trailing space yaml: "a: \"b \"\n" node: kind: Document content: - kind: Mapping content: - {kind: Scalar, text: a} - {kind: Scalar, style: Double, text: 'b '} # Sequences of mappings - node-test: name: sequence of simple mappings yaml: "- a: 1\n- b: 2\n" node: kind: Document content: - kind: Sequence content: - kind: Mapping content: [{kind: Scalar, text: a}, {kind: Scalar, tag: '!!int', text: '1'}] - kind: Mapping content: [{kind: Scalar, text: b}, {kind: Scalar, tag: '!!int', text: '2'}] # Mappings with flow sequence values - node-test: name: two mappings with flow sequence values yaml: "a: [1]\nb: [2]\n" node: kind: Document content: - kind: Mapping content: - {kind: Scalar, text: a} - kind: Sequence style: Flow content: [{kind: Scalar, tag: '!!int', text: '1'}] - {kind: Scalar, text: b} - kind: Sequence style: Flow content: [{kind: Scalar, tag: '!!int', text: '2'}] # Empty sequence in mapping - node-test: name: mapping with empty flow sequence yaml: "a: []\n" node: kind: Document content: - kind: Mapping content: - {kind: Scalar, text: a} - kind: Sequence style: Flow # Empty mapping in sequence - node-test: name: sequence with empty flow mapping yaml: "[{}]\n" node: kind: Document content: - kind: Sequence style: Flow content: - kind: Mapping style: Flow # Single element sequences - node-test: name: single element block sequence yaml: "- a\n" node: kind: Document content: - kind: Sequence content: [{kind: Scalar, text: a}] - node-test: name: single element flow sequence yaml: "[a]\n" node: kind: Document content: - kind: Sequence style: Flow content: [{kind: Scalar, text: a}] # Single element mappings - node-test: name: single element flow mapping yaml: "{a: 1}\n" node: kind: Document content: - kind: Mapping style: Flow content: [{kind: Scalar, text: a}, {kind: Scalar, tag: '!!int', text: '1'}] # Different null representations - node-test: name: tilde null yaml: "~\n" node: kind: Document content: - {kind: Scalar, tag: '!!null', text: '~'} - node-test: name: lowercase null yaml: "null\n" node: kind: Document content: - {kind: Scalar, tag: '!!null', text: 'null'} - node-test: name: uppercase Null yaml: "Null\n" node: kind: Document content: - {kind: Scalar, tag: '!!null', text: 'Null'} # Mixed types in sequences - node-test: name: sequence with mixed types yaml: "- 1\n- foo\n- true\n" node: kind: Document content: - kind: Sequence content: - {kind: Scalar, tag: '!!int', text: '1'} - {kind: Scalar, text: foo} - {kind: Scalar, tag: '!!bool', text: 'true'} # Mixed types in mappings - node-test: name: mapping with different value types yaml: "int: 1\nstr: foo\nbool: true\n" node: kind: Document content: - kind: Mapping content: - {kind: Scalar, text: int} - {kind: Scalar, tag: '!!int', text: '1'} - {kind: Scalar, text: str} - {kind: Scalar, text: foo} - {kind: Scalar, text: bool} - {kind: Scalar, tag: '!!bool', text: 'true'} # Deeply nested flow - node-test: name: deeply nested flow mapping yaml: "{a: {b: {c: 1}}}\n" node: kind: Document content: - kind: Mapping style: Flow content: - {kind: Scalar, text: a} - kind: Mapping style: Flow content: - {kind: Scalar, text: b} - kind: Mapping style: Flow content: [{kind: Scalar, text: c}, {kind: Scalar, tag: '!!int', text: '1'}] # Four-level nested mapping - node-test: name: four-level nested mapping yaml: "a:\n b:\n c:\n d: e\n" node: kind: Document content: - kind: Mapping content: - {kind: Scalar, text: a} - kind: Mapping content: - {kind: Scalar, text: b} - kind: Mapping content: - {kind: Scalar, text: c} - kind: Mapping content: [{kind: Scalar, text: d}, {kind: Scalar, text: e}] # Literal style variations - node-test: name: literal with single line yaml: "|\n single\n" node: kind: Document content: - {kind: Scalar, style: Literal, text: "single\n"} - node-test: name: literal with empty lines decode: true encode: false yaml: "|\n line1\n \n line2\n" node: kind: Document content: - {kind: Scalar, style: Literal, text: "line1\n\nline2\n"} # Numbers edge cases - node-test: name: leading zeros preserved in string yaml: "\"007\"\n" node: kind: Document content: - {kind: Scalar, style: Double, text: '007'} - node-test: name: plus sign integer yaml: "+42\n" node: kind: Document content: - {kind: Scalar, tag: '!!int', text: '+42'} # Scientific notation - node-test: name: scientific notation lowercase e yaml: "1.23e2\n" node: kind: Document content: - {kind: Scalar, tag: '!!float', text: '1.23e2'} - node-test: name: scientific notation uppercase E yaml: "1.23E2\n" node: kind: Document content: - {kind: Scalar, tag: '!!float', text: '1.23E2'} - node-test: name: scientific notation negative exponent yaml: "1.23e-2\n" node: kind: Document content: - {kind: Scalar, tag: '!!float', text: '1.23e-2'} # Very large/small numbers - node-test: name: very large integer yaml: "9223372036854775807\n" node: kind: Document content: - {kind: Scalar, tag: '!!int', text: '9223372036854775807'} # Boolean variations already covered TRUE/FALSE, add lowercase - node-test: name: lowercase true yaml: "true\n" node: kind: Document content: - {kind: Scalar, tag: '!!bool', text: 'true'} - node-test: name: lowercase false yaml: "false\n" node: kind: Document content: - {kind: Scalar, tag: '!!bool', text: 'false'} # Quoted numbers - node-test: name: single quoted number yaml: "'123'\n" node: kind: Document content: - {kind: Scalar, style: Single, text: '123'} # Flow collections with trailing comma (if valid) - node-test: name: flow sequence multiple elements yaml: "[a, b, c]\n" node: kind: Document content: - kind: Sequence style: Flow content: [{kind: Scalar, text: a}, {kind: Scalar, text: b}, {kind: Scalar, text: c}] - node-test: name: flow mapping multiple elements yaml: "{a: 1, b: 2, c: 3}\n" node: kind: Document content: - kind: Mapping style: Flow content: - {kind: Scalar, text: a} - {kind: Scalar, tag: '!!int', text: '1'} - {kind: Scalar, text: b} - {kind: Scalar, tag: '!!int', text: '2'} - {kind: Scalar, text: c} - {kind: Scalar, tag: '!!int', text: '3'} # Indented block scalars - node-test: name: literal with extra indentation decode: true encode: false yaml: "|\n indented\n" node: kind: Document content: - {kind: Scalar, style: Literal, text: "indented\n"} # Mapping key variations - node-test: name: integer as key yaml: "1: one\n" node: kind: Document content: - kind: Mapping content: [{kind: Scalar, tag: '!!int', text: '1'}, {kind: Scalar, text: one}] - node-test: name: boolean as key yaml: "true: yes\n" node: kind: Document content: - kind: Mapping content: [{kind: Scalar, tag: '!!bool', text: 'true'}, {kind: Scalar, text: yes}] # Sequences with mixed nesting - node-test: name: sequence with mapping and scalars yaml: "- a\n- b: c\n- d\n" node: kind: Document content: - kind: Sequence content: - {kind: Scalar, text: a} - kind: Mapping content: [{kind: Scalar, text: b}, {kind: Scalar, text: c}] - {kind: Scalar, text: d} # Mappings with mixed value types - node-test: name: mapping with scalar and sequence values yaml: "a: b\nc:\n - d\n" node: kind: Document content: - kind: Mapping content: - {kind: Scalar, text: a} - {kind: Scalar, text: b} - {kind: Scalar, text: c} - kind: Sequence content: [{kind: Scalar, text: d}] - node-test: name: mapping with scalar and mapping values yaml: "a: b\nc:\n d: e\n" node: kind: Document content: - kind: Mapping content: - {kind: Scalar, text: a} - {kind: Scalar, text: b} - {kind: Scalar, text: c} - kind: Mapping content: [{kind: Scalar, text: d}, {kind: Scalar, text: e}] # Special characters in plain scalars - node-test: name: plain scalar with dash yaml: "foo-bar\n" node: kind: Document content: - {kind: Scalar, text: foo-bar} - node-test: name: plain scalar with underscore yaml: "foo_bar\n" node: kind: Document content: - {kind: Scalar, text: foo_bar} - node-test: name: plain scalar with dot yaml: "foo.bar\n" node: kind: Document content: - {kind: Scalar, text: foo.bar} # Numbers with underscores (if supported) - node-test: name: number with underscores yaml: "1_000_000\n" node: kind: Document content: - {kind: Scalar, tag: '!!int', text: '1_000_000'} # Zero variants - node-test: name: float zero yaml: "0.0\n" node: kind: Document content: - {kind: Scalar, tag: '!!float', text: '0.0'} # Whitespace-only differences - node-test: name: mapping with extra blank line decode: true encode: false yaml: "a: b\n\nc: d\n" node: kind: Document content: - kind: Mapping content: - {kind: Scalar, text: a} - {kind: Scalar, text: b} - {kind: Scalar, text: c} - {kind: Scalar, text: d} # Single quoted with escapes - node-test: name: single quoted with literal single quote yaml: "'don''t'\n" node: kind: Document content: - {kind: Scalar, style: Single, text: "don't"} # Double quoted with various escapes - node-test: name: double quoted with backslash yaml: "\"foo\\\\bar\"\n" node: kind: Document content: - {kind: Scalar, style: Double, text: 'foo\bar'} - node-test: name: double quoted with quote yaml: "\"foo\\\"bar\"\n" node: kind: Document content: - {kind: Scalar, style: Double, text: 'foo"bar'} # Complex flow structures - node-test: name: flow mapping in flow sequence yaml: "[{a: 1}, {b: 2}]\n" node: kind: Document content: - kind: Sequence style: Flow content: - kind: Mapping style: Flow content: [{kind: Scalar, text: a}, {kind: Scalar, tag: '!!int', text: '1'}] - kind: Mapping style: Flow content: [{kind: Scalar, text: b}, {kind: Scalar, tag: '!!int', text: '2'}] - node-test: name: flow sequence in flow mapping yaml: "{a: [1, 2], b: [3, 4]}\n" node: kind: Document content: - kind: Mapping style: Flow content: - {kind: Scalar, text: a} - kind: Sequence style: Flow content: [{kind: Scalar, tag: '!!int', text: '1'}, {kind: Scalar, tag: '!!int', text: '2'}] - {kind: Scalar, text: b} - kind: Sequence style: Flow content: [{kind: Scalar, tag: '!!int', text: '3'}, {kind: Scalar, tag: '!!int', text: '4'}] # Edge case numbers - node-test: name: negative zero float yaml: "-0.0\n" node: kind: Document content: - {kind: Scalar, tag: '!!float', text: '-0.0'} - node-test: name: integer zero yaml: "0\n" node: kind: Document content: - {kind: Scalar, tag: '!!int', text: '0'} # More string edge cases - node-test: name: empty plain scalar becomes null decode: true encode: false yaml: "a:\nb: c\n" node: kind: Document content: - kind: Mapping content: - {kind: Scalar, text: a} - {kind: Scalar, tag: '!!null', text: ''} - {kind: Scalar, text: b} - {kind: Scalar, text: c} # Multiline plain scalars - node-test: name: plain scalar on single line yaml: "this is a long string\n" node: kind: Document content: - {kind: Scalar, text: 'this is a long string'} # Keys with special values - node-test: name: null as mapping key yaml: "null: value\n" node: kind: Document content: - kind: Mapping content: [{kind: Scalar, tag: '!!null', text: 'null'}, {kind: Scalar, text: value}] - node-test: name: empty string as key yaml: "\"\": value\n" node: kind: Document content: - kind: Mapping content: [{kind: Scalar, style: Double, text: ''}, {kind: Scalar, text: value}] # Floats with trailing zeros - node-test: name: float with trailing zero yaml: "1.0\n" node: kind: Document content: - {kind: Scalar, tag: '!!float', text: '1.0'} - node-test: name: float with many decimals yaml: "3.141592653589793\n" node: kind: Document content: - {kind: Scalar, tag: '!!float', text: '3.141592653589793'} # Mixed sequences - node-test: name: sequence of different collection types yaml: "- [a]\n- {b: c}\n" node: kind: Document content: - kind: Sequence content: - kind: Sequence style: Flow content: [{kind: Scalar, text: a}] - kind: Mapping style: Flow content: [{kind: Scalar, text: b}, {kind: Scalar, text: c}] # More complex nested structures - node-test: name: mapping with sequence containing mappings yaml: "items:\n - name: foo\n value: 1\n - name: bar\n value: 2\n" node: kind: Document content: - kind: Mapping content: - {kind: Scalar, text: items} - kind: Sequence content: - kind: Mapping content: [{kind: Scalar, text: name}, {kind: Scalar, text: foo}, {kind: Scalar, text: value}, {kind: Scalar, tag: '!!int', text: '1'}] - kind: Mapping content: [{kind: Scalar, text: name}, {kind: Scalar, text: bar}, {kind: Scalar, text: value}, {kind: Scalar, tag: '!!int', text: '2'}] # Different integer formats (hex, octal already covered) - node-test: name: binary integer yaml: "0b1010\n" node: kind: Document content: - {kind: Scalar, tag: '!!int', text: '0b1010'} # More special float values - node-test: name: positive infinity yaml: ".inf\n" node: kind: Document content: - {kind: Scalar, tag: '!!float', text: '.inf'} - node-test: name: negative infinity yaml: "-.inf\n" node: kind: Document content: - {kind: Scalar, tag: '!!float', text: '-.inf'} - node-test: name: not a number yaml: ".nan\n" node: kind: Document content: - {kind: Scalar, tag: '!!float', text: '.nan'} # Complex key-value patterns - node-test: name: complex key simple value yaml: "[a, b]: value\n" decode: true encode: false node: kind: Document content: - kind: Mapping content: - kind: Sequence style: Flow content: [{kind: Scalar, text: a}, {kind: Scalar, text: b}] - {kind: Scalar, text: value} # Plain scalars that could be ambiguous - node-test: name: plain scalar starting with dash yaml: "\"- foo\"\n" node: kind: Document content: - {kind: Scalar, style: Double, text: '- foo'} - node-test: name: plain scalar starting with bracket yaml: "\"[foo]\"\n" node: kind: Document content: - {kind: Scalar, style: Double, text: '[foo]'} - node-test: name: plain scalar starting with brace yaml: "\"{foo}\"\n" node: kind: Document content: - {kind: Scalar, style: Double, text: '{foo}'} # Sequences and mappings with comments on elements - node-test: name: sequence with line comments yaml: "- a # comment a\n- b # comment b\n" node: kind: Document content: - kind: Sequence content: - {kind: Scalar, text: a, line: '# comment a'} - {kind: Scalar, text: b, line: '# comment b'} - node-test: name: mapping with line comments on values yaml: "a: 1 # first\nb: 2 # second\n" node: kind: Document content: - kind: Mapping content: - {kind: Scalar, text: a} - {kind: Scalar, tag: '!!int', text: '1', line: '# first'} - {kind: Scalar, text: b} - {kind: Scalar, tag: '!!int', text: '2', line: '# second'} # More nested flow - node-test: name: deeply nested flow sequence yaml: "[[[a]]]\n" node: kind: Document content: - kind: Sequence style: Flow content: - kind: Sequence style: Flow content: - kind: Sequence style: Flow content: [{kind: Scalar, text: a}] # More FootComment tests - node-test: name: scalar with foot comment yaml: "foo\n# foot\n" node: kind: Document content: - {kind: Scalar, text: foo, foot: '# foot'} # Quoted special characters - node-test: name: double quoted with carriage return yaml: "\"foo\\rbar\"\n" node: kind: Document content: - {kind: Scalar, style: Double, text: "foo\rbar"} # Multiple levels of flow nesting - node-test: name: flow mapping with nested sequences yaml: "{a: [1, 2], b: [3, 4]}\n" node: kind: Document content: - kind: Mapping style: Flow content: - {kind: Scalar, text: a} - kind: Sequence style: Flow content: [{kind: Scalar, tag: '!!int', text: '1'}, {kind: Scalar, tag: '!!int', text: '2'}] - {kind: Scalar, text: b} - kind: Sequence style: Flow content: [{kind: Scalar, tag: '!!int', text: '3'}, {kind: Scalar, tag: '!!int', text: '4'}] # Literal and folded variations - node-test: name: literal with chomping indicator keep decode: true encode: false yaml: "|+\n content\n" node: kind: Document content: - {kind: Scalar, style: Literal, text: "content\n"} - node-test: name: literal with chomping indicator strip yaml: "|-\n content\n" node: kind: Document content: - {kind: Scalar, style: Literal, text: content} - node-test: name: folded with chomping indicator strip decode: true encode: false yaml: ">-\n content\n" node: kind: Document content: - {kind: Scalar, style: Folded, text: content} # Collections as mapping keys - node-test: name: mapping as mapping key decode: true encode: false yaml: "{a: b}: value\n" node: kind: Document content: - kind: Mapping content: - kind: Mapping style: Flow content: [{kind: Scalar, text: a}, {kind: Scalar, text: b}] - {kind: Scalar, text: value} # More empty value cases - node-test: name: two keys with empty values yaml: "a:\nb:\n" node: kind: Document content: - kind: Mapping content: - {kind: Scalar, text: a} - {kind: Scalar, tag: '!!null', text: ''} - {kind: Scalar, text: b} - {kind: Scalar, tag: '!!null', text: ''} # Numbers in scientific notation edge cases - node-test: name: scientific notation with large exponent yaml: "1e100\n" node: kind: Document content: - {kind: Scalar, tag: '!!float', text: '1e100'} # Multiline strings in flow collections - node-test: name: flow mapping with quoted multiline value yaml: "{a: \"line1\\nline2\"}\n" node: kind: Document content: - kind: Mapping style: Flow content: - {kind: Scalar, text: a} - {kind: Scalar, style: Double, text: "line1\nline2"} # Sequences with null elements - node-test: name: sequence with null elements yaml: "- null\n- ~\n-\n" node: kind: Document content: - kind: Sequence content: - {kind: Scalar, tag: '!!null', text: 'null'} - {kind: Scalar, tag: '!!null', text: '~'} - {kind: Scalar, tag: '!!null', text: ''} # Mix of inline and block in same document - node-test: name: block mapping then flow mapping yaml: "a: b\nc: {d: e}\n" node: kind: Document content: - kind: Mapping content: - {kind: Scalar, text: a} - {kind: Scalar, text: b} - {kind: Scalar, text: c} - kind: Mapping style: Flow content: [{kind: Scalar, text: d}, {kind: Scalar, text: e}] - node-test: name: block sequence then flow sequence yaml: "- a\n- [b, c]\n" node: kind: Document content: - kind: Sequence content: - {kind: Scalar, text: a} - kind: Sequence style: Flow content: [{kind: Scalar, text: b}, {kind: Scalar, text: c}] # Special mapping key cases - node-test: name: quoted number as key yaml: "\"123\": value\n" node: kind: Document content: - kind: Mapping content: - {kind: Scalar, style: Double, text: '123'} - {kind: Scalar, text: value} - node-test: name: quoted boolean as key yaml: "\"true\": value\n" node: kind: Document content: - kind: Mapping content: - {kind: Scalar, style: Double, text: 'true'} - {kind: Scalar, text: value} # Edge cases with whitespace - node-test: name: value with only spaces quoted yaml: "a: \" \"\n" node: kind: Document content: - kind: Mapping content: - {kind: Scalar, text: a} - {kind: Scalar, style: Double, text: ' '} # More complex comments - node-test: name: head comment on second mapping key decode: true encode: false yaml: "a: 1\n# head\nb: 2\n" node: kind: Document content: - kind: Mapping content: - {kind: Scalar, text: a} - {kind: Scalar, tag: '!!int', text: '1'} - {kind: Scalar, text: b, head: '# head'} - {kind: Scalar, tag: '!!int', text: '2'} # Nested empty collections - node-test: name: empty mapping in mapping yaml: "a: {}\n" node: kind: Document content: - kind: Mapping content: - {kind: Scalar, text: a} - kind: Mapping style: Flow - node-test: name: empty sequence in sequence yaml: "- []\n" node: kind: Document content: - kind: Sequence content: - kind: Sequence style: Flow # More integer edge cases - node-test: name: negative integer yaml: "-42\n" node: kind: Document content: - {kind: Scalar, tag: '!!int', text: '-42'} - node-test: name: large negative integer yaml: "-9223372036854775808\n" node: kind: Document content: - {kind: Scalar, tag: '!!int', text: '-9223372036854775808'} # String that looks like a tag - node-test: name: quoted string resembling tag yaml: "\"!!str\"\n" node: kind: Document content: - {kind: Scalar, style: Double, text: '!!str'} # More nesting combinations - node-test: name: five-level nested mapping yaml: "a:\n b:\n c:\n d:\n e: f\n" node: kind: Document content: - kind: Mapping content: - {kind: Scalar, text: a} - kind: Mapping content: - {kind: Scalar, text: b} - kind: Mapping content: - {kind: Scalar, text: c} - kind: Mapping content: - {kind: Scalar, text: d} - kind: Mapping content: [{kind: Scalar, text: e}, {kind: Scalar, text: f}] # Final edge cases to reach completion - node-test: name: sequence of empty mappings yaml: "- {}\n- {}\n" node: kind: Document content: - kind: Sequence content: - kind: Mapping style: Flow - kind: Mapping style: Flow - node-test: name: mapping of empty sequences yaml: "a: []\nb: []\n" node: kind: Document content: - kind: Mapping content: - {kind: Scalar, text: a} - kind: Sequence style: Flow - {kind: Scalar, text: b} - kind: Sequence style: Flow - node-test: name: deeply nested sequences yaml: "- - - a\n" node: kind: Document content: - kind: Sequence content: - kind: Sequence content: - kind: Sequence content: [{kind: Scalar, text: a}] - node-test: name: mapping value as empty quoted string yaml: "a: \"\"\n" node: kind: Document content: - kind: Mapping content: - {kind: Scalar, text: a} - {kind: Scalar, style: Double, text: ''} - node-test: name: flow mapping with spaces around colon decode: true encode: false yaml: "{a : b}\n" node: kind: Document content: - kind: Mapping style: Flow content: [{kind: Scalar, text: a}, {kind: Scalar, text: b}] - node-test: name: flow sequence with extra spaces decode: true encode: false yaml: "[ a , b ]\n" node: kind: Document content: - kind: Sequence style: Flow content: [{kind: Scalar, text: a}, {kind: Scalar, text: b}] - node-test: name: plain scalar with numbers and letters yaml: "abc123\n" node: kind: Document content: - {kind: Scalar, text: abc123} - node-test: name: plain scalar with mixed case yaml: "FooBar\n" node: kind: Document content: - {kind: Scalar, text: FooBar} - node-test: name: mapping with numeric string keys and values yaml: "\"1\": \"2\"\n" node: kind: Document content: - kind: Mapping content: - {kind: Scalar, style: Double, text: '1'} - {kind: Scalar, style: Double, text: '2'} - node-test: name: three empty sequences yaml: "- []\n- []\n- []\n" node: kind: Document content: - kind: Sequence content: - kind: Sequence style: Flow - kind: Sequence style: Flow - kind: Sequence style: Flow - node-test: name: complex mixed nesting yaml: "- a: 1\n b:\n - c\n" node: kind: Document content: - kind: Sequence content: - kind: Mapping content: - {kind: Scalar, text: a} - {kind: Scalar, tag: '!!int', text: '1'} - {kind: Scalar, text: b} - kind: Sequence content: [{kind: Scalar, text: c}] - node-test: name: alternating scalars and collections yaml: "- foo\n- [bar]\n- baz\n- {qux: 1}\n" node: kind: Document content: - kind: Sequence content: - {kind: Scalar, text: foo} - kind: Sequence style: Flow content: [{kind: Scalar, text: bar}] - {kind: Scalar, text: baz} - kind: Mapping style: Flow content: [{kind: Scalar, text: qux}, {kind: Scalar, tag: '!!int', text: '1'}] # Merge key encoding tests. # An implicit !!merge tag (Tag set but no TaggedStyle) is elided on output # so callers that hand-build a node with Value: "<<" get clean `<<:` syntax # (yq issue #2677). An explicit !!merge (TaggedStyle set) is preserved. - node-test: name: merge key with flow mapping value yaml: "<<: {a: 1}\n" decode: false node: kind: Document content: - kind: Mapping content: - kind: Scalar tag: '!!merge' text: '<<' - kind: Mapping style: Flow content: - {kind: Scalar, text: a} - {kind: Scalar, tag: '!!int', text: '1'} - node-test: name: merge key in nested mapping yaml: "foo:\n <<: {a: 1}\n" decode: false node: kind: Document content: - kind: Mapping content: - kind: Scalar text: foo - kind: Mapping content: - kind: Scalar tag: '!!merge' text: '<<' - kind: Mapping style: Flow content: - {kind: Scalar, text: a} - {kind: Scalar, tag: '!!int', text: '1'} - node-test: name: merge key with explicit tag preserved yaml: "!!merge <<: {a: 1}\n" decode: false node: kind: Document content: - kind: Mapping content: - kind: Scalar tag: '!!merge' style: Tagged text: '<<' - kind: Mapping style: Flow content: - {kind: Scalar, text: a} - {kind: Scalar, tag: '!!int', text: '1'} - node-test: name: bare merge key with untagged scalar yaml: "<<: {a: 1}\n" decode: false node: kind: Document content: - kind: Mapping content: - kind: Scalar text: '<<' - kind: Mapping style: Flow content: - {kind: Scalar, text: a} - {kind: Scalar, tag: '!!int', text: '1'} golang-go.yaml-yaml-v4-4.0.0~rc5/testdata/parser_events.yaml000066400000000000000000000027461521353103300240070ustar00rootroot00000000000000# Copyright 2025 The go-yaml Project Contributors # SPDX-License-Identifier: Apache-2.0 # Parser events test cases for go-yaml parser # # Purpose: Tests the YAML parser's ability to generate correct event sequences # from YAML input. Events represent the structural elements of YAML documents # (document start/end, mappings, sequences, scalars, etc.). # # Format: Test cases use type-as-key format: # - test-type: # name: Test case name # ... # # Test Types: # parser-events - Verifies parser event output matches expected event sequence # # Common Keys: # name - Test case name (string) # yaml - Input YAML to parse (string) # want - Expected event sequence (string with events on separate lines) # # Event Format: # Events are represented as text with specific prefixes: # +STR - Stream start # -STR - Stream end # +DOC - Document start # -DOC - Document end # +MAP - Mapping start # -MAP - Mapping end # +SEQ - Sequence start # -SEQ - Sequence end # =VAL - Scalar value (followed by :value) # =ALI - Alias reference (followed by :*anchor) - parser-events: name: ImplicitDocumentStart yaml: | a: b want: | +STR +DOC +MAP =VAL :a =VAL :b -MAP -DOC -STR - parser-events: name: ExplicitDocumentStart yaml: | --- a: b want: | +STR +DOC --- +MAP =VAL :a =VAL :b -MAP -DOC -STR golang-go.yaml-yaml-v4-4.0.0~rc5/testdata/plugin.yaml000066400000000000000000000051141521353103300224150ustar00rootroot00000000000000# Copyright 2026 The go-yaml Project Contributors # SPDX-License-Identifier: Apache-2.0 # Plugin test cases for go-yaml # # Purpose: Tests YAML-based plugin configuration via OptsYAML. # Validates that plugins can be configured from YAML and enforce # their limits correctly. # # Test Types: # plugin-pass - Tests that should succeed with the given plugin config # plugin-error - Tests that should fail with a specific error message # # Common Keys: # name - Test case name (string) # plugin - Plugin configuration (passed to OptsYAML) # data - YAML input specification (string or generator spec) # want - Expected error message (for plugin-error tests) - plugin-pass: name: depth 50 allows nesting of 20 plugin: limit: depth: 50 data: join: - loop: ['[', 20] - text: 'x' - loop: [']', 20] - plugin-error: name: depth 5 rejects nesting of 20 plugin: limit: depth: 5 data: join: - loop: ['[', 20] - text: 'x' - loop: [']', 20] want: 'go-yaml load error in scanner (while increasing flow level) at L1.C6: exceeded max depth of 5' - plugin-pass: name: depth null disables depth checking (10001 nesting) plugin: limit: depth: data: join: - loop: ['[', 10001] - text: 'x' - loop: [']', 10001] - plugin-pass: name: alias null disables alias checking (11000 aliases) plugin: limit: alias: data: join: - text: 'anchor: &anchor [1, 2, 3]' - text: "\nrefs:\n" - loop: ["- *anchor\n", 11000] - plugin-error: name: alias 5 rejects excessive aliases plugin: limit: alias: 5 data: join: - text: 'anchor: &anchor [1, 2, 3]' - text: "\nrefs:\n" - loop: ["- *anchor\n", 200] want: 'go-yaml load error in constructor at L1.C18: exceeded max alias count of 5' - plugin-pass: name: alias 10000 allows moderate aliases plugin: limit: alias: 10000 data: join: - text: 'anchor: &anchor [1, 2, 3]' - text: "\nrefs:\n" - loop: ["- *anchor\n", 200] - plugin-pass: name: default limits (bare limits key) plugin: limit: data: join: - loop: ['[', 20] - text: 'x' - loop: [']', 20] - plugin-error: name: default limits reject deep nesting (10001) plugin: limit: data: join: - loop: ['[', 10001] - text: 'x' - loop: [']', 10001] want: 'go-yaml load error in scanner (while increasing flow level) at L1.C10001: exceeded max depth of 10000' golang-go.yaml-yaml-v4-4.0.0~rc5/testdata/stream.yaml000066400000000000000000000061511521353103300224140ustar00rootroot00000000000000# SPDX-License-Identifier: Apache-2.0 # # StreamNode test cases for go-yaml # # Purpose: Tests YAML stream node functionality including stream boundaries, # directives, and encoding information. Validates that WithStreamNodes() option # correctly returns interleaved StreamNode and DocumentNode patterns. # # Format: Test cases use type-as-key format: # - stream-test: # name: Test case name # yaml: YAML input string # with: # Optional: loader options # no-stream-nodes: true # Disable stream nodes # want: List of expected nodes with their properties # # Test Types: # stream-test - Validates stream node patterns and metadata # # Common Keys: # name - Test case name (string) # yaml - Input YAML string to parse # with - Optional loader options (map) # no-stream-nodes - Disable stream nodes (default: false) # want - List of expected nodes with kind and optional properties # # Node Format in 'want': # kind: - Node kind: Stream or Document (required) # stream: - Stream metadata (for Stream nodes only, nested structure): # encoding: - Encoding name: UTF-8, UTF-16LE, UTF-16BE # version: - Version string like "1.1" # tag-directives: - List of tag directive objects # Empty stream returns single StreamNode - stream-test: name: empty stream yaml: '' want: - kind: Stream stream: encoding: UTF-8 # Single document returns [Stream, Doc, Stream] - stream-test: name: single document yaml: 'key: value' want: - kind: Stream stream: encoding: UTF-8 - kind: Document - kind: Stream stream: encoding: UTF-8 # Multiple documents return interleaved pattern - stream-test: name: multi document yaml: | --- key1: value1 --- key2: value2 want: - kind: Stream stream: encoding: UTF-8 - kind: Document - kind: Stream stream: encoding: UTF-8 - kind: Document - kind: Stream stream: encoding: UTF-8 # YAML version directive captured on first StreamNode - stream-test: name: version directive yaml: | %YAML 1.1 %TAG ! tag:example.com,2000:app/ --- key: value want: - kind: Stream stream: encoding: UTF-8 version: "1.1" tag-directives: - handle: "!" prefix: "tag:example.com,2000:app/" - kind: Document - kind: Stream stream: encoding: UTF-8 # Encoding is set on all StreamNodes - stream-test: name: encoding check yaml: 'key: value' want: - kind: Stream stream: encoding: UTF-8 - kind: Document - kind: Stream stream: encoding: UTF-8 # Backward compatibility - without stream nodes option - stream-test: name: backward compat yaml: | --- a: 1 --- b: 2 with: no-stream-nodes: true want: - kind: Document - kind: Document # Explicitly disabled stream nodes - stream-test: name: disabled yaml: 'key: value' with: no-stream-nodes: true want: - kind: Document golang-go.yaml-yaml-v4-4.0.0~rc5/testdata/unmarshal_errors.yaml000066400000000000000000000144411521353103300245100ustar00rootroot00000000000000# Copyright 2025 The go-yaml Project Contributors # SPDX-License-Identifier: Apache-2.0 # Unmarshal error test cases for go-yaml # # Purpose: Tests that the YAML unmarshaler correctly detects and reports errors # for invalid YAML input, malformed syntax, and type conversion failures. # # Format: Test cases use type-as-key format: # - test-type: # name: Test case name # ... # # Test Types: # unmarshal-error - Verifies that unmarshaling produces the expected error message # # Common Keys: # name - Test case name (string) # yaml - Invalid or malformed YAML input (string) # want - Expected error message (string) # # Note: # All tests in this file expect unmarshaling to fail. The 'want' field contains # the exact error message that should be produced. - unmarshal-error: name: Invalid float value yaml: 'v: !!float ''error''' want: 'go-yaml load error in resolver at : cannot construct !!str `error` as a !!float' - unmarshal-error: name: Incomplete flow sequence yaml: 'v: [A,' want: 'go-yaml load error in parser (while parsing a flow node) at L2.C1: did not find expected node content' - unmarshal-error: name: Incomplete flow sequence in list yaml: | v: - [A, want: 'go-yaml load error in parser (while parsing a flow node) at L3.C1: did not find expected node content' - unmarshal-error: name: Invalid alias syntax yaml: | a: - b: *, want: 'go-yaml load error in scanner (while scanning an alias) at L2.C6-C7: did not find expected alphabetic or numeric character' - unmarshal-error: name: Unknown anchor referenced yaml: "a: *b\n" want: "go-yaml load error in composer at L1.C4: unknown anchor 'b' referenced" - unmarshal-error: name: Anchor contains itself yaml: | a: &a b: *a want: "go-yaml load error in composer at L2.C6: anchor 'a' value contains itself" - unmarshal-error: name: Block sequence in invalid context yaml: 'value: -' want: 'go-yaml load error in scanner at L1.C8: block sequence entries are not allowed in this context' - unmarshal-error: name: Invalid base64 in binary yaml: 'a: !!binary ==' want: 'go-yaml load error in constructor at L1.C4: !!binary value contains invalid base64 data' - unmarshal-error: name: Slice as map key yaml: '{[.]}' want: "go-yaml load error in constructor at L1.C2: cannot use '[]interface {}{\".\"}' as a map key; try decoding into yaml.Node" - unmarshal-error: name: Map as map key yaml: '{{.}}' want: "go-yaml load error in constructor at L1.C2: cannot use 'map[string]interface {}{\".\":interface {}(nil)}' as a map key; try decoding into yaml.Node" - unmarshal-error: name: Forward reference to anchor yaml: | b: *a a: &a {c: 1} want: "go-yaml load error in composer at L1.C4: unknown anchor 'a' referenced" - unmarshal-error: name: Invalid TAG directive yaml: | %TAG !%79! tag:yaml.org,2002: --- v: !%79!int '1' want: 'go-yaml load error in scanner (while scanning a %TAG directive) at L1.C1-C7: did not find expected whitespace' - unmarshal-error: name: Missing colon in mapping (multiline) yaml: | a: 1: b 2: want: 'go-yaml load error in scanner at L4.C4: mapping values are not allowed in this context' - unmarshal-error: name: Missing colon in mapping (inline) yaml: | a: 1 b: 2 c 2 d: 3 want: "go-yaml load error in scanner (while scanning a simple key) at L3.C1-L4.C1: could not find expected ':'" - unmarshal-error: name: Missing colon after comment and list (Issue 665) yaml: | # - { want: 'go-yaml load error in parser (while parsing a flow node) at L4.C1: did not find expected node content' - unmarshal-error: name: Incomplete UTF-8 sequence (Issue 666) yaml: "0: [:!00 \xEF" want: "go-yaml load error in parser (while parsing a flow sequence) at L1.C4-L2.C1: did not find expected ',' or ']'" - unmarshal-error: name: Anchor with colon (Issue 109) yaml: | foo: &bar: bar *bar: : quz want: 'go-yaml load error in scanner at L1.C10: mapping values are not allowed in this context' - unmarshal-error: name: Excessive aliasing yaml: | a: &a [00,00,00,00,00,00,00,00,00] b: &b [*a,*a,*a,*a,*a,*a,*a,*a,*a] c: &c [*b,*b,*b,*b,*b,*b,*b,*b,*b] d: &d [*c,*c,*c,*c,*c,*c,*c,*c,*c] e: &e [*d,*d,*d,*d,*d,*d,*d,*d,*d] f: &f [*e,*e,*e,*e,*e,*e,*e,*e,*e] g: &g [*f,*f,*f,*f,*f,*f,*f,*f,*f] h: &h [*g,*g,*g,*g,*g,*g,*g,*g,*g] i: &i [*h,*h,*h,*h,*h,*h,*h,*h,*h] want: 'go-yaml load error in constructor at L2.C17: document contains excessive aliasing' # TextUnmarshaler validation errors - unmarshal-error: name: Mapping into TextUnmarshaler (value field) type: testStructA_TextUnmarshaler yaml: 'a: {foo: bar}' want: 'yaml: construct errors: line 1: cannot construct !!map into yaml_test.simpleTextUnmarshaler (TextUnmarshaler)' - unmarshal-error: name: Sequence into TextUnmarshaler (value field) type: testStructA_TextUnmarshaler yaml: 'a: [foo, bar]' want: 'yaml: construct errors: line 1: cannot construct !!seq into yaml_test.simpleTextUnmarshaler (TextUnmarshaler)' - unmarshal-error: name: Mapping into TextUnmarshaler (pointer field) type: testStructA_TextUnmarshalerPtr yaml: 'a: {foo: bar}' want: 'yaml: construct errors: line 1: cannot construct !!map into yaml_test.simpleTextUnmarshaler (TextUnmarshaler)' - unmarshal-error: name: Sequence into TextUnmarshaler (pointer field) type: testStructA_TextUnmarshalerPtr yaml: 'a: [foo, bar]' want: 'yaml: construct errors: line 1: cannot construct !!seq into yaml_test.simpleTextUnmarshaler (TextUnmarshaler)' - unmarshal-error: name: Mapping into TextUnmarshaler (pointer-pointer field) type: testStructA_TextUnmarshalerPtrPtr yaml: 'a: {foo: bar}' want: 'yaml: construct errors: line 1: cannot construct !!map into yaml_test.simpleTextUnmarshaler (TextUnmarshaler)' - unmarshal-error: name: Sequence into TextUnmarshaler (pointer-pointer field) type: testStructA_TextUnmarshalerPtrPtr yaml: 'a: [foo, bar]' want: 'yaml: construct errors: line 1: cannot construct !!seq into yaml_test.simpleTextUnmarshaler (TextUnmarshaler)' golang-go.yaml-yaml-v4-4.0.0~rc5/util/000077500000000000000000000000001521353103300173765ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc5/util/check-commit-messages000077500000000000000000000071261521353103300235020ustar00rootroot00000000000000#!/usr/bin/env bash # Copyright 2025 The go-yaml Project Contributors # SPDX-License-Identifier: Apache-2.0 # https://www.shellcheck.net/wiki/SC1091 # shellcheck disable=1091 source "$(dirname "${BASH_SOURCE[0]}")"/common.bash || exit require-bash-4.4+ usage() ( cat <<-... Usage: $0 | : A range of commits in the form hash..hash : A file containing a list of commit hashes, one per line" ... ) main() ( require-commands git head sed case $# in 0) usage; exit ;; 1) range_or_file=$1 ;; *) die \ 'Error: Too many arguments.' \ '' \ "$(usage)" ;; esac # Determine input type range_or_file=${1:-HEAD} if [[ -f $range_or_file ]]; then message=$(< "$range_or_file") validate-commit-message "$range_or_file" "$message" || die "Commit message in $range_or_file is invalid." else ok=true commits=$(git rev-list "$range_or_file") for commit in $commits; do message=$(git log --format=%B -n 1 "$commit") validate-commit-message "$commit" "$message" || ok=false done $ok || die 'At least one commit message is invalid.' fi ) validate-commit-message() ( commit_or_file=$1 message=$2 subject=$(head -n1 <<<"$message") length=${#subject} errors=() error_lines=() [[ $subject =~ ^(feat|fix|docs|style|refactor|perf|test|chore)(\(.*\))?: ]] && errors+=('Line 1: Do not use conventional commit format for subject') # subject should not start with square brackets [[ $subject =~ ^\[.*\] ]] && errors+=('Line 1: Subject should not start with square brackets') [[ $subject =~ ^[A-Z] ]] || errors+=('Line 1: Subject should start with a capital letter') [[ $subject == *. ]] && errors+=('Line 1: Subject should not end with a period') [[ $subject == *' '* ]] && errors+=('Line 1: Subject should not contain consecutive spaces') [[ $subject == *' ' ]] && errors+=('Line 1: Subject should not have trailing space(s)') [[ $length -ge 20 ]] || errors+=("Line 1: Subject should be longer than 20 characters (current: $length)") [[ $length -le 50 ]] || errors+=("Line 1: Subject should be shorter than 50 characters (current: $length)") [[ ${#errors[*]} -eq 0 ]] || error_lines+=(1) if [[ $(sed -n '2p' <<<"$message") ]]; then errors+=('Line 2: Subject and body should be separated by a single blank line') error_lines+=(2) fi body=$(sed -n '3,$p' <<<"$message") i=3 while IFS= read -r line; do if [[ $line == *' ' ]]; then errors+=("Line $i: Body should not have trailing space(s)") (( ${#error_lines[@]} )) && [[ ${error_lines[-1]} == "$i" ]] || error_lines+=("$i") fi lower=${line,,} if [[ $lower =~ co-authored-by:.*(claude|anthropic|copilot|codex|openai|aider|gemini) ]]; then errors+=("Line $i: Co-authored-by AI attribution is not currently allowed in commit messages") (( ${#error_lines[@]} )) && [[ ${error_lines[-1]} == "$i" ]] || error_lines+=("$i") fi ((i++)) done <<<"$body" # Return if no errors: [[ ${#errors[@]} -eq 0 ]] && return # Report errors to stderr: ( echo -e "${RED}Error: '$commit_or_file' has an invalid commit message:$RESET\n" # read the message and add the line number in front of each line, and use # warn_color to display a line with an error based on line_with_errors i=1 while IFS= read -r line && [[ ${#error_lines[*]} -gt 0 ]]; do COLOR=$RESET if [[ $i == "${error_lines[0]}" ]]; then COLOR="\e[1;33m" # Bold yellow error_lines=("${error_lines[@]:1}") fi echo -e "${COLOR}Line $(printf '%2d' $i): $line$RESET" ((i++)) done <<<"$message" echo printf -- '* %s\n' "${errors[@]}" echo ) >&2 return 1 ) main "$@" golang-go.yaml-yaml-v4-4.0.0~rc5/util/common.bash000066400000000000000000000015051521353103300215260ustar00rootroot00000000000000# Copyright 2025 The go-yaml Project Contributors # SPDX-License-Identifier: Apache-2.0 # Use strict settings: set -euo pipefail # ANSI color code vars: RED="\e[1;31m" RESET="\e[0m" require-bash-4.4+() { if ! shopt -s compat43 2>/dev/null; then local bash version bash=$(command -v bash) version=$("$bash" -c "echo \$BASH_VERSION") cat <<-... >&2 Error: 'bash' version 4.4+ is required to be first in your PATH. You currently have: $bash $version ... exit 1 fi # inherit_errexit is important and introduced in 4.4 shopt -s inherit_errexit } require-commands() ( for cmd; do command -v "$cmd" >/dev/null || die "Error: $cmd is not installed or available in the PATH." done ) # General error function: die() { echo -e "$RED$1$RESET" >&2 shift for line; do echo -e "$line" done >&2 exit 1 } golang-go.yaml-yaml-v4-4.0.0~rc5/util/test-count000077500000000000000000000035631521353103300214400ustar00rootroot00000000000000#!/usr/bin/env bash # Copyright 2025 The go-yaml Project Contributors # SPDX-License-Identifier: Apache-2.0 # Show test count statistics for go-yaml set -euo pipefail # shellcheck disable=SC1091 source "$(dirname "$0")/common.bash" main() ( echo '=== TEST COUNT SUMMARY ===' echo # Count YAML test cases echo 'Data-driven YAML test cases:' decode_count=$(grep -c '^- decode:' testdata/decode.yaml) encode_count=$(grep -c '^- encode:' testdata/encode.yaml) error_count=$(grep -c '^- unmarshal-error:' testdata/unmarshal_errors.yaml) yaml_total=$((decode_count + encode_count + error_count)) printf ' %-22s %3d\n' 'decode.yaml:' "$decode_count" printf ' %-22s %3d\n' 'encode.yaml:' "$encode_count" printf ' %-22s %3d\n' 'unmarshal_errors.yaml:' "$error_count" printf ' %-22s %3d\n' 'Subtotal:' "$yaml_total" echo # Count Go test cases echo 'Go hardcoded test cases:' unmarshal_count=$(awk '/^var unmarshalTests/,/^}$/{if(/^ {$/){count++}}END{print count}' decode_test.go) marshal_count=$(awk '/^var marshalTests/,/^}$/{if(/^ {$/){count++}}END{print count}' encode_test.go) go_total=$((unmarshal_count + marshal_count)) printf ' %-22s %3d\n' 'unmarshalTests:' "$unmarshal_count" printf ' %-22s %3d\n' 'marshalTests:' "$marshal_count" printf ' %-22s %3d\n' 'Subtotal:' "$go_total" echo # Total test cases local case_total case_total=$((yaml_total + go_total)) printf '%-24s %3d\n' 'Test case total:' "$case_total" echo # Count test executions (requires running tests) echo 'Test executions (running tests to count...):' test_output=$(go test -v . ./internal/... -vet=off 2>&1) run_count=$(<<< "$test_output" grep -c '^=== RUN' || true) pass_count=$(<<< "$test_output" grep -c 'PASS:' || true) printf ' %-22s %4d\n' 'Unit test runs:' "$run_count" printf ' %-22s %4d\n' 'Unit test passes:' "$pass_count" echo ) main "$@" golang-go.yaml-yaml-v4-4.0.0~rc5/util/yaml-test-suite000077500000000000000000000016471521353103300224020ustar00rootroot00000000000000#!/usr/bin/env bash # Copyright 2025 The go-yaml Project Contributors # SPDX-License-Identifier: Apache-2.0 set -euo pipefail main() ( type=$1 if [[ $type == all ]]; then export RUNALL=1 elif [[ $type == fail ]]; then export RUNFAILING=1 else exit 1 fi results=$( go test ./yts -count=1 -v | awk '/ --- (PASS|FAIL): / {print $2, $3}' ) || true known_count=$(grep -c '' yts/known-failing-tests) pass_count=$(grep -c '^PASS:' <<<"$results") fail_count=$(grep -c '^FAIL:' <<<"$results") echo "PASS: $pass_count" echo "FAIL: $fail_count (known: $known_count)" if [[ $type == fail ]] && [[ $pass_count -gt 0 ]]; then echo "ERROR: Found passing tests among expected failures:" grep '^PASS:' "$results" exit 1 fi if [[ $fail_count != "$known_count" ]]; then echo "ERROR: FAIL count ($fail_count) differs from expected $known_count" exit 1 fi ) main "$@" golang-go.yaml-yaml-v4-4.0.0~rc5/yaml.go000066400000000000000000000622671521353103300177270ustar00rootroot00000000000000// Copyright 2011-2019 Canonical Ltd // Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Package yaml implements YAML support for the Go language. // // Source code and other details for the project are available at GitHub: // // https://github.com/yaml/go-yaml // // This file contains: // - Version preset functions (WithV2Defaults, WithV3Defaults, WithV4Defaults) // - Options API (WithIndent, WithKnownFields, etc.) // - Type and constant re-exports from internal/libyaml // - Helper functions for struct field handling // - Load/Dump API (Load, Dump, Loader, Dumper) // - Classic APIs (Decoder, Encoder, Unmarshal, Marshal) package yaml import ( "errors" "fmt" "io" "go.yaml.in/yaml/v4/internal/libyaml" "go.yaml.in/yaml/v4/plugin/limit" ) //----------------------------------------------------------------------------- // Version presets //----------------------------------------------------------------------------- // Usage: // yaml.Dump(&data, yaml.WithV3Defaults()) // yaml.Dump(&data, yaml.WithV3Defaults(), yaml.WithIndent(2), yaml.WithCompactSeqIndent()) // WithV2Defaults returns V2-compatible default options. func WithV2Defaults() Option { return Options( WithIndent(2), WithCompactSeqIndent(false), WithLineWidth(80), WithUnicode(true), WithUniqueKeys(true), WithQuotePreference(QuoteLegacy), WithPlugin(limit.New()), ) } // WithV3Defaults returns V3-compatible default options. func WithV3Defaults() Option { return Options( WithIndent(4), WithCompactSeqIndent(false), WithLineWidth(80), WithUnicode(true), WithUniqueKeys(true), WithQuotePreference(QuoteLegacy), WithPlugin(limit.New()), ) } // WithV4Defaults returns the current V4 default options. func WithV4Defaults() Option { return Options( WithIndent(2), WithCompactSeqIndent(true), WithLineWidth(80), WithUnicode(true), WithUniqueKeys(true), WithQuotePreference(QuoteSingle), WithPlugin(limit.New()), ) } //----------------------------------------------------------------------------- // Options //----------------------------------------------------------------------------- // Option allows configuring YAML loading and dumping operations. type Option = libyaml.Option // Option configuration functions var ( // WithIndent sets the number of spaces to use for indentation when // dumping YAML content. // // Valid values are 2-9. Common choices: 2 (compact), 4 (readable). WithIndent = libyaml.WithIndent // WithCompactSeqIndent configures whether the sequence indicator '- ' is // considered part of the indentation when dumping YAML content. // // If compact is true, '- ' is treated as part of the indentation. // If compact is false, '- ' is not treated as part of the indentation. // When called without arguments, defaults to true. WithCompactSeqIndent = libyaml.WithCompactSeqIndent // WithKnownFields enables or disables strict field checking during YAML // loading. // // When enabled, loading will return an error if the YAML input contains // fields that do not correspond to any fields in the target struct. // When called without arguments, defaults to true. WithKnownFields = libyaml.WithKnownFields // WithSingleDocument configures the Loader to only process the first // document in a YAML stream. After the first document is loaded, // subsequent calls to Load will return [io.EOF]. // // When called without arguments, defaults to true. // // This is useful when you expect exactly one document and want behavior // similar to Unmarshal. WithSingleDocument = libyaml.WithSingleDocument // WithStreamNodes enables returning stream boundary nodes when loading // YAML. // // When enabled, Loader.Load returns an interleaved sequence of // StreamNode and DocumentNode values: // // [StreamNode, DocNode, StreamNode, DocNode, ..., StreamNode] // // StreamNodes contain metadata about the stream including: // - Encoding (UTF-8, UTF-16LE, UTF-16BE) // - YAML version directive (%YAML) // - Tag directives (%TAG) // - Position information (Line, Column) // // An empty YAML stream returns a single StreamNode. // When called without arguments, defaults to true. // // The default is false. WithStreamNodes = libyaml.WithStreamNodes // WithAllDocuments enables multi-document mode for Load and Dump // operations. // // When used with Load, the target must be a pointer to a slice. // All documents in the YAML stream will be decoded into the slice. // Zero documents results in an empty slice (no error). // // When used with Dump, the input must be a slice. // Each element will be encoded as a separate YAML document // with "---" separators. // // When called without arguments, defaults to true. // // The default is false (single-document mode). WithAllDocuments = libyaml.WithAllDocuments // WithLineWidth sets the preferred line width for YAML output. // // When encoding long strings, the encoder will attempt to wrap them at // this width using literal block style (|). Set to -1 or 0 for unlimited // width. // // The default is 80 characters. WithLineWidth = libyaml.WithLineWidth // WithUnicode controls whether non-ASCII characters are allowed in YAML // output. // // When true, non-ASCII characters appear as-is (e.g., "café"). // When false, non-ASCII characters are escaped (e.g., "caf\u00e9"). // When called without arguments, defaults to true. // // The default is true. WithUnicode = libyaml.WithUnicode // WithUniqueKeys enables or disables duplicate key detection during YAML // loading. // // When enabled, loading will return an error if the YAML input contains // duplicate keys in any mapping. This is a security feature that prevents // key override attacks. // When called without arguments, defaults to true. // // The default is true. WithUniqueKeys = libyaml.WithUniqueKeys // WithCanonical forces canonical YAML output format. // // When enabled, the encoder outputs strictly canonical YAML with explicit // tags for all values. This produces verbose output primarily useful for // debugging and YAML spec compliance testing. // When called without arguments, defaults to true. // // The default is false. WithCanonical = libyaml.WithCanonical // WithLineBreak sets the line ending style for YAML output. // // Available options: // - LineBreakLN: Unix-style \n (default) // - LineBreakCR: Old Mac-style \r // - LineBreakCRLN: Windows-style \r\n // // The default is LineBreakLN. WithLineBreak = libyaml.WithLineBreak // WithExplicitStart controls whether document start markers (---) are // always emitted. // // When true, every document begins with an explicit "---" marker. // When false (default), the marker is omitted for the first document. // When called without arguments, defaults to true. WithExplicitStart = libyaml.WithExplicitStart // WithExplicitEnd controls whether document end markers (...) are always // emitted. // // When true, every document ends with an explicit "..." marker. // When false (default), the marker is omitted. // When called without arguments, defaults to true. WithExplicitEnd = libyaml.WithExplicitEnd // WithFlowSimpleCollections controls whether simple collections use flow // style. // // When true, sequences and mappings containing only scalar values (no // nested collections) are rendered in flow style if they fit within the // line width. // Example: {name: test, count: 42} or [a, b, c] // When called without arguments, defaults to true. // // When false (default), all collections use block style. WithFlowSimpleCollections = libyaml.WithFlowSimpleCollections // WithQuotePreference sets the preferred quote style for strings that // require quoting. // // This option only affects strings that require quoting per the YAML spec. // Plain strings that don't need quoting remain unquoted regardless of this // setting. Quoting is required for: // - Strings that look like other YAML types (true, false, null, 123, etc.) // - Strings with leading/trailing whitespace // - Strings containing special YAML syntax characters // - Empty strings in certain contexts // // Quote styles: // - QuoteSingle: Use single quotes (v4 default) // - QuoteDouble: Use double quotes // - QuoteLegacy: Legacy v2/v3 behavior (mixed quoting) WithQuotePreference = libyaml.WithQuotePreference ) // Options combines multiple options into a single Option. // This is useful for creating option presets or combining version defaults // with custom options. // // Example: // // opts := yaml.Options(yaml.WithV4Defaults(), yaml.WithIndent(3)) // yaml.Dump(&data, opts) func Options(opts ...Option) Option { return libyaml.CombineOptions(opts...) } // DepthKind represents the type of nesting (flow or block). type DepthKind = libyaml.DepthKind // DepthKind constants for nesting depth checks. const ( DepthKindFlow = libyaml.DepthKindFlow DepthKindBlock = libyaml.DepthKindBlock ) // DepthContext holds context about a nesting depth check. type DepthContext = libyaml.DepthContext // WithPlugin registers one or more plugins for YAML processing. // // Plugins extend the YAML library with custom processing logic. // Each plugin implements one or more plugin interfaces. // Currently supported plugin types: // - LimitPlugin: Controls depth and alias expansion limits // // Example: // // import "go.yaml.in/yaml/v4/plugin/limit" // loader := yaml.NewLoader(data, yaml.WithPlugin(limit.New(limit.AliasNone()))) // // Plugins use public types and can be implemented by external packages. func WithPlugin(plugins ...any) Option { return func(o *libyaml.Options) error { for _, p := range plugins { registered := false if lp, ok := p.(LimitPlugin); ok { o.DepthCheck = lp.CheckDepth o.AliasCheck = lp.CheckAlias registered = true } // Future plugin types add cases here (non-exclusive if) if !registered { return fmt.Errorf("yaml: unsupported plugin type: %T", p) } } return nil } } // OptsYAML parses a YAML string containing option settings and returns // an Option that can be combined with other options using Options(). // // The YAML string can specify any of these fields: // - indent (int) // - compact-seq-indent (bool) // - line-width (int) // - unicode (bool) // - canonical (bool) // - line-break (string: ln, cr, crln) // - explicit-start (bool) // - explicit-end (bool) // - flow-simple-coll (bool) // - known-fields (bool) // - single-document (bool) // - unique-keys (bool) // - plugin (map of plugin name to config) // // The plugin field configures plugins by name. Each key is a plugin // name and the value is its configuration map (or null for defaults). // Currently supported: "limit" with keys "depth" and "alias" (int // or null to disable). // // Only fields specified in the YAML will override other options when // combined. Unspecified fields won't affect other options. // // Example: // // opts, err := yaml.OptsYAML(` // indent: 3 // known-fields: true // plugin: // limit: // depth: 50 // `) // yaml.Dump(&data, yaml.Options(V4, opts)) func OptsYAML(yamlStr string) (Option, error) { var cfg struct { Indent *int `yaml:"indent"` CompactSeqIndent *bool `yaml:"compact-seq-indent"` LineWidth *int `yaml:"line-width"` Unicode *bool `yaml:"unicode"` Canonical *bool `yaml:"canonical"` LineBreak *string `yaml:"line-break"` ExplicitStart *bool `yaml:"explicit-start"` ExplicitEnd *bool `yaml:"explicit-end"` FlowSimpleCollections *bool `yaml:"flow-simple-coll"` KnownFields *bool `yaml:"known-fields"` SingleDocument *bool `yaml:"single-document"` UniqueKeys *bool `yaml:"unique-keys"` Plugin map[string]any `yaml:"plugin"` } if err := Load([]byte(yamlStr), &cfg, WithKnownFields()); err != nil { return nil, err } // Build options only for fields that were set var optList []Option if cfg.Indent != nil { optList = append(optList, WithIndent(*cfg.Indent)) } if cfg.CompactSeqIndent != nil { optList = append(optList, WithCompactSeqIndent(*cfg.CompactSeqIndent)) } if cfg.LineWidth != nil { optList = append(optList, WithLineWidth(*cfg.LineWidth)) } if cfg.Unicode != nil { optList = append(optList, WithUnicode(*cfg.Unicode)) } if cfg.ExplicitStart != nil { optList = append(optList, WithExplicitStart(*cfg.ExplicitStart)) } if cfg.ExplicitEnd != nil { optList = append(optList, WithExplicitEnd(*cfg.ExplicitEnd)) } if cfg.FlowSimpleCollections != nil { optList = append(optList, WithFlowSimpleCollections(*cfg.FlowSimpleCollections)) } if cfg.KnownFields != nil { optList = append(optList, WithKnownFields(*cfg.KnownFields)) } if cfg.SingleDocument != nil && *cfg.SingleDocument { optList = append(optList, WithSingleDocument()) } if cfg.UniqueKeys != nil { optList = append(optList, WithUniqueKeys(*cfg.UniqueKeys)) } if cfg.Canonical != nil { optList = append(optList, WithCanonical(*cfg.Canonical)) } if cfg.LineBreak != nil { switch *cfg.LineBreak { case "ln": optList = append(optList, WithLineBreak(LineBreakLN)) case "cr": optList = append(optList, WithLineBreak(LineBreakCR)) case "crln": optList = append(optList, WithLineBreak(LineBreakCRLN)) default: return nil, errors.New("yaml: invalid line-break value (use ln, cr, or crln)") } } for name, val := range cfg.Plugin { switch name { case "limit": var cfgMap map[string]any switch v := val.(type) { case nil: cfgMap = map[string]any{} case map[string]any: cfgMap = v default: return nil, fmt.Errorf("yaml: plugin %q value must be a mapping or null", name) } p, err := limit.NewFromYAML(cfgMap) if err != nil { return nil, err } optList = append(optList, WithPlugin(p)) default: return nil, fmt.Errorf("yaml: unknown plugin %q", name) } } return Options(optList...), nil } //----------------------------------------------------------------------------- // Type and constant re-exports //----------------------------------------------------------------------------- // Re-export stream-related types type ( Stream = libyaml.Stream VersionDirective = libyaml.StreamVersionDirective // TagDirective represents a YAML %TAG directive for stream nodes. TagDirective = libyaml.StreamTagDirective // Encoding represents the character encoding of a YAML stream. Encoding = libyaml.Encoding ) // Encoding constants for YAML stream encoding const ( // EncodingAny lets the parser choose the encoding. EncodingAny = libyaml.ANY_ENCODING // EncodingUTF8 is the default UTF-8 encoding. EncodingUTF8 = libyaml.UTF8_ENCODING // EncodingUTF16LE is UTF-16-LE encoding with BOM. EncodingUTF16LE = libyaml.UTF16LE_ENCODING // EncodingUTF16BE is UTF-16-BE encoding with BOM. EncodingUTF16BE = libyaml.UTF16BE_ENCODING ) // Stage identifies the processing stage where an error occurred during YAML // loading or dumping. type Stage = libyaml.Stage // Stage constants for YAML processing pipeline. const ( // Load stages ReaderStage = libyaml.ReaderStage // Input reading and encoding ScannerStage = libyaml.ScannerStage // Tokenization ParserStage = libyaml.ParserStage // Event stream parsing ComposerStage = libyaml.ComposerStage // Node tree construction ResolverStage = libyaml.ResolverStage // Tag resolution ConstructorStage = libyaml.ConstructorStage // Go value construction // Dump stages RepresenterStage = libyaml.RepresenterStage // Go value to Node tree SerializerStage = libyaml.SerializerStage // Node tree to events EmitterStage = libyaml.EmitterStage // Events to YAML bytes WriterStage = libyaml.WriterStage // Output writing ) // Mark represents a position in the YAML document. type Mark = libyaml.Mark // Error types for YAML loading and dumping type ( // LoadError represents an error encountered while decoding a YAML document. // // It contains details about the location in the document where the error // occurred, as well as the processing stage that generated it. LoadError = libyaml.LoadError // LoadErrors is returned when one or more fields cannot be properly decoded. // // It contains multiple *[LoadError] instances with details about each error. LoadErrors = libyaml.LoadErrors // DumpError represents an error that occurred while dumping a YAML document. // // It identifies the processing stage where the error occurred and provides // an optional underlying cause via Unwrap. DumpError = libyaml.DumpError // TypeError is a legacy error type retained for compatibility. // // Deprecated: Use [LoadErrors] instead. // //nolint:staticcheck // we are using deprecated TypeError for compatibility TypeError = libyaml.TypeError ) // NewLoadError creates a LoadError with an underlying cause error. // The cause is accessible via Unwrap for use with [errors.Is] and [errors.As]. var NewLoadError = libyaml.NewLoadError // NewDumpError creates a DumpError with an underlying cause error. // The cause is accessible via Unwrap for use with [errors.Is] and [errors.As]. var NewDumpError = libyaml.NewDumpError // LineBreak represents the line ending style for YAML output. type LineBreak = libyaml.LineBreak // Line break constants for different platforms. const ( LineBreakLN = libyaml.LN_BREAK // Unix-style \n (default) LineBreakCR = libyaml.CR_BREAK // Old Mac-style \r LineBreakCRLN = libyaml.CRLN_BREAK // Windows-style \r\n ) // QuoteStyle represents the quote style to use when quoting is required. type QuoteStyle = libyaml.QuoteStyle // Quote style constants for required quoting. const ( QuoteSingle = libyaml.QuoteSingle // Prefer single quotes (v4 default) QuoteDouble = libyaml.QuoteDouble // Prefer double quotes QuoteLegacy = libyaml.QuoteLegacy // Legacy v2/v3 behavior ) //----------------------------------------------------------------------------- // Load/Dump API //----------------------------------------------------------------------------- // Advanced streaming API types type ( // Loader reads and loads YAML values from an input stream with // configurable options. Loader = libyaml.Loader // Dumper writes YAML values to an output stream with configurable options. Dumper = libyaml.Dumper ) // NewLoader returns a new Loader that reads from r with the given options. func NewLoader(r io.Reader, opts ...Option) (*Loader, error) { return libyaml.NewLoader(r, opts...) } // NewDumper returns a new Dumper that writes to w with the given options. func NewDumper(w io.Writer, opts ...Option) (*Dumper, error) { return libyaml.NewDumper(w, opts...) } // Load loads YAML document(s) with the given options. func Load(in []byte, out any, opts ...Option) error { return libyaml.Load(in, out, opts...) } // Dump encodes a value to YAML with the given options. func Dump(in any, opts ...Option) (out []byte, err error) { return libyaml.Dump(in, opts...) } //----------------------------------------------------------------------------- // Classic APIs //----------------------------------------------------------------------------- // A Decoder reads and decodes YAML values from an input stream. type Decoder struct { loader *Loader } // NewDecoder returns a new decoder that reads from r. // // The decoder introduces its own buffering and may read // data from r beyond the YAML values requested. func NewDecoder(r io.Reader) *Decoder { // NewLoader won't return error with WithV3Defaults() and withFromLegacy loader, _ := NewLoader(r, WithV3Defaults(), withFromLegacy()) return &Decoder{loader: loader} } // KnownFields ensures that the keys in decoded mappings to // exist as fields in the struct being decoded into. func (dec *Decoder) KnownFields(enable bool) { dec.loader.SetKnownFields(enable) } // Decode reads the next YAML-encoded value from its input // and stores it in the value pointed to by v. // // See the documentation for Unmarshal for details about the // conversion of YAML into a Go value. func (dec *Decoder) Decode(v any) error { return dec.loader.Load(v) } // An Encoder writes YAML values to an output stream. type Encoder struct { dumper *Dumper } // NewEncoder returns a new encoder that writes to w. // The Encoder should be closed after use to flush all data // to w. func NewEncoder(w io.Writer) *Encoder { // NewDumper won't return an error when using WithV3Defaults() dumper, _ := NewDumper(w, WithV3Defaults()) return &Encoder{dumper: dumper} } // Encode writes the YAML encoding of v to the stream. // If multiple items are encoded to the stream, the // second and subsequent document will be preceded // with a "---" document separator, but the first will not. // // See the documentation for Marshal for details about the conversion of Go // values to YAML. func (e *Encoder) Encode(v any) error { return e.dumper.Dump(v) } // SetIndent changes the used indentation used when encoding. func (e *Encoder) SetIndent(spaces int) { e.dumper.SetIndent(spaces) } // CompactSeqIndent makes it so that '- ' is considered part of the indentation. func (e *Encoder) CompactSeqIndent() { e.dumper.SetCompactSeqIndent(true) } // DefaultSeqIndent makes it so that '- ' is not considered part of the indentation. func (e *Encoder) DefaultSeqIndent() { e.dumper.SetCompactSeqIndent(false) } // Close closes the encoder by writing any remaining data. // It does not write a stream terminating string "...". func (e *Encoder) Close() error { return e.dumper.Close() } // Unmarshal decodes the first document found within the in byte slice // and assigns decoded values into the out value. // // Maps and pointers (to a struct, string, int, etc) are accepted as out // values. If an internal pointer within a struct is not initialized, // the yaml package will initialize it if necessary for unmarshalling // the provided data. The out parameter must not be nil. // // The type of the decoded values should be compatible with the respective // values in out. If one or more values cannot be decoded due to a type // mismatches, decoding continues partially until the end of the YAML // content, and a *yaml.LoadErrors is returned with details for all // missed values. // // Struct fields are only unmarshalled if they are exported (have an // upper case first letter), and are unmarshalled using the field name // lowercased as the default key. Custom keys may be defined via the // "yaml" name in the field tag: the content preceding the first comma // is used as the key, and the following comma-separated options are // used to tweak the marshaling process (see Marshal). // Conflicting names result in a runtime error. // // For example: // // type T struct { // F int `yaml:"a,omitempty"` // B int // } // var t T // yaml.Construct([]byte("a: 1\nb: 2"), &t) // // See the documentation of Marshal for the format of tags and a list of // supported tag options. func Unmarshal(in []byte, out any) (err error) { return Load(in, out, WithV3Defaults(), withFromLegacy()) } // withFromLegacy is a private option that indicates this call is from // a legacy API (Unmarshal/Decoder). It enables Unmarshaler interface // checking and allows trailing content for backward compatibility. func withFromLegacy() Option { return func(o *libyaml.Options) error { o.FromLegacy = true return nil } } // Marshal serializes the value provided into a YAML document. The structure // of the generated document will reflect the structure of the value itself. // Maps and pointers (to struct, string, int, etc) are accepted as the in value. // // Struct fields are only marshaled if they are exported (have an upper case // first letter), and are marshaled using the field name lowercased as the // default key. Custom keys may be defined via the "yaml" name in the field // tag: the content preceding the first comma is used as the key, and the // following comma-separated options are used to tweak the marshaling process. // Conflicting names result in a runtime error. // // The field tag format accepted is: // // `(...) yaml:"[][,[,]]" (...)` // // The following flags are currently supported: // // omitempty Only include the field if it's not set to the zero // value for the type or to empty slices or maps. // Zero valued structs will be omitted if all their public // fields are zero, unless they implement an IsZero // method (see the IsZeroer interface type), in which // case the field will be excluded if IsZero returns true. // // flow Marshal using a flow style (useful for structs, // sequences and maps). // // inline Inline the field, which must be a struct or a map, // causing all of its fields or keys to be processed as if // they were part of the outer struct. For maps, keys must // not conflict with the yaml keys of other struct fields. // See doc/inline-tags.md for detailed examples and use cases. // // In addition, if the key is "-", the field is ignored. // // For example: // // type T struct { // F int `yaml:"a,omitempty"` // B int // } // yaml.Marshal(&T{B: 2}) // Returns "b: 2\n" // yaml.Marshal(&T{F: 1}} // Returns "a: 1\nb: 0\n" func Marshal(in any) (out []byte, err error) { // Use WithV3Defaults() with unlimited line width to match legacy DefaultOptions return Dump(in, WithV3Defaults(), WithLineWidth(-1)) } golang-go.yaml-yaml-v4-4.0.0~rc5/yaml_test.go000066400000000000000000002421201521353103300207520ustar00rootroot00000000000000// Copyright 2011-2019 Canonical Ltd // Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Tests for YAML marshal/unmarshal functionality, including struct tags, // type conversions, anchors/aliases, and edge cases. package yaml_test import ( "bytes" "encoding" "encoding/json" "errors" "fmt" "io" "math" "net" "os" "reflect" "strconv" "strings" "testing" "time" "go.yaml.in/yaml/v4" "go.yaml.in/yaml/v4/internal/libyaml" "go.yaml.in/yaml/v4/internal/testutil/assert" "go.yaml.in/yaml/v4/internal/testutil/datatest" ) // negativeZero represents -0.0 for YAML test cases // this is needed because Go constants cannot express -0.0 // https://staticcheck.dev/docs/checks/#SA4026 var negativeZero = math.Copysign(0.0, -1.0) var unmarshalIntTest = 123 // archSafeInt returns v as int if it fits in the architecture's int type, // otherwise returns int64. func archSafeInt(v int64) any { if strconv.IntSize == 64 || math.MinInt32 <= v && v <= math.MaxInt32 { return int(v) // int is safe } // on 32-bit systems, and v overflows int, we need to return an int64 return v } // Named struct types for data-driven tests type ( testStructHello struct{ Hello string } testStructA_Int struct{ A int } testStructA_Float64 struct{ A float64 } testStructA_Uint struct{ A uint } testStructA_Bool struct{ A bool } testStructA_IntSlice struct{ A []int } testStructA_IntArray2 struct{ A [2]int } testStructA_MapStringString struct{ A map[string]string } testStructA_MapStringStringPtr struct{ A *map[string]string } testStructB_Int struct{ B int } nestedStructB struct{ B string } testStructA_NestedB struct{ A nestedStructB } testStructA_NestedBPtr struct{ A *nestedStructB } testStructABCD_Int struct{ A, B, C, D int } testStructB_IntSlice struct{ B []int } testStructA_IntSliceEmpty struct{ A []int } testStructA_String struct{ A string } testStructEmpty struct{} ) // Types with yaml struct tags type ( testStructB_Int_TagA struct { B int `yaml:"a"` } testStructAB_Int_BIgnored struct { A int B int `yaml:"-"` } testStructA_Int_InlineB struct { A int C inlineB `yaml:",inline"` } testStructA_Int_InlineBPtr struct { A int C *inlineB `yaml:",inline"` } testStructA_Int_InlineDPtr struct { A int C *inlineD `yaml:",inline"` } testStructA_Int_InlineMapStringInt struct { A int C map[string]int `yaml:",inline"` } ) // simpleTextUnmarshaler is a simple type implementing [encoding.TextUnmarshaler] // for testing TextUnmarshaler validation. type simpleTextUnmarshaler struct { Value string } func (s *simpleTextUnmarshaler) UnmarshalText(text []byte) error { s.Value = string(text) return nil } // Test types for TextUnmarshaler validation type ( testStructA_TextUnmarshaler struct { A simpleTextUnmarshaler } testStructA_TextUnmarshalerPtr struct { A *simpleTextUnmarshaler } testStructA_TextUnmarshalerPtrPtr struct { A **simpleTextUnmarshaler } testStructAB_TextUnmarshaler struct { A simpleTextUnmarshaler B simpleTextUnmarshaler } ) type textUnmarshalerWithYAMLUnmarshaler []string var _ interface { encoding.TextUnmarshaler yaml.Unmarshaler } = &textUnmarshalerWithYAMLUnmarshaler{} func (ty *textUnmarshalerWithYAMLUnmarshaler) UnmarshalText(text []byte) error { panic("UnmarshalText called on type with UnmarshalYAML") } func (ty *textUnmarshalerWithYAMLUnmarshaler) UnmarshalYAML(node *yaml.Node) error { return node.Decode((*[]string)(ty)) } func TestTextUnmarshalerWithYAMLUnmarshaler(t *testing.T) { var target textUnmarshalerWithYAMLUnmarshaler const input = `[foo, bar]` // NOTE: also verified with [yaml.Unmarshal] — no shortcut bypasses // Constructor since PR #310, so this is a regression test for // [libyaml.Constructor.Construct] via both paths. err := yaml.NewDecoder(strings.NewReader(input)).Decode(&target) assert.NoError(t, err) assert.DeepEqual(t, textUnmarshalerWithYAMLUnmarshaler{"foo", "bar"}, target) } // Type and value registries for data-driven tests var ( decodeTypes = datatest.NewTypeRegistry() decodeValues = datatest.NewValueRegistry() ) func init() { // Register basic map types decodeTypes.RegisterFactory("map[string]string", func() any { return make(map[string]string) }) decodeTypes.RegisterFactory("map[string]any", func() any { return make(map[string]any) }) decodeTypes.RegisterFactory("map[string]int64", func() any { return make(map[string]int64) }) decodeTypes.RegisterFactory("map[string]float64", func() any { return make(map[string]float64) }) decodeTypes.RegisterFactory("map[string][]byte", func() any { return make(map[string][]byte) }) decodeTypes.RegisterFactory("map[any]any", func() any { return make(map[any]any) }) // Register slice types decodeTypes.RegisterFactory("[]string", func() any { return []string{} }) decodeTypes.RegisterFactory("[]int", func() any { return []int{} }) decodeTypes.RegisterFactory("[]any", func() any { return []any{} }) // Register primitive types decodeTypes.RegisterFactory("string", func() any { return "" }) // Register map types with slice values decodeTypes.RegisterFactory("map[string][]string", func() any { return make(map[string][]string) }) decodeTypes.RegisterFactory("map[string][]int", func() any { return make(map[string][]int) }) // Register additional map types decodeTypes.RegisterFactory("map[string]bool", func() any { return make(map[string]bool) }) decodeTypes.RegisterFactory("map[string]int", func() any { return make(map[string]int) }) decodeTypes.RegisterFactory("map[any]string", func() any { return make(map[any]string) }) decodeTypes.RegisterFactory("map[string]uint", func() any { return make(map[string]uint) }) decodeTypes.RegisterFactory("map[string]uint64", func() any { return make(map[string]uint64) }) decodeTypes.RegisterFactory("map[string]int32", func() any { return make(map[string]int32) }) decodeTypes.RegisterFactory("map[string]int8", func() any { return make(map[string]int8) }) decodeTypes.RegisterFactory("map[string]float32", func() any { return make(map[string]float32) }) // Register struct types decodeTypes.Register("testStructHello", testStructHello{}) decodeTypes.Register("testStructA_Int", testStructA_Int{}) decodeTypes.Register("testStructA_Float64", testStructA_Float64{}) decodeTypes.Register("testStructA_Uint", testStructA_Uint{}) decodeTypes.Register("testStructA_Bool", testStructA_Bool{}) decodeTypes.Register("testStructA_IntSlice", testStructA_IntSlice{}) decodeTypes.Register("testStructA_IntArray2", testStructA_IntArray2{}) decodeTypes.Register("testStructA_MapStringString", testStructA_MapStringString{}) decodeTypes.Register("testStructA_MapStringStringPtr", testStructA_MapStringStringPtr{}) decodeTypes.Register("testStructB_Int", testStructB_Int{}) decodeTypes.Register("testStructA_NestedB", testStructA_NestedB{}) decodeTypes.Register("testStructA_NestedBPtr", testStructA_NestedBPtr{}) decodeTypes.Register("testStructABCD_Int", testStructABCD_Int{}) decodeTypes.Register("testStructB_IntSlice", testStructB_IntSlice{}) decodeTypes.Register("testStructA_IntSliceEmpty", testStructA_IntSliceEmpty{}) decodeTypes.Register("testStructA_String", testStructA_String{}) decodeTypes.Register("testStructEmpty", testStructEmpty{}) // Register struct types with yaml tags decodeTypes.Register("testStructB_Int_TagA", testStructB_Int_TagA{}) decodeTypes.Register("testStructAB_Int_BIgnored", testStructAB_Int_BIgnored{}) decodeTypes.Register("testStructA_Int_InlineB", testStructA_Int_InlineB{}) decodeTypes.Register("testStructA_Int_InlineBPtr", testStructA_Int_InlineBPtr{}) decodeTypes.Register("testStructA_Int_InlineDPtr", testStructA_Int_InlineDPtr{}) decodeTypes.Register("testStructA_Int_InlineMapStringInt", testStructA_Int_InlineMapStringInt{}) // Register TextUnmarshaler test types decodeTypes.Register("testStructA_TextUnmarshaler", testStructA_TextUnmarshaler{}) decodeTypes.Register("testStructA_TextUnmarshalerPtr", testStructA_TextUnmarshalerPtr{}) decodeTypes.Register("testStructA_TextUnmarshalerPtrPtr", testStructA_TextUnmarshalerPtrPtr{}) decodeTypes.Register("testStructAB_TextUnmarshaler", testStructAB_TextUnmarshaler{}) // Register math constants decodeValues.Register("+Inf", math.Inf(+1)) decodeValues.Register("-Inf", math.Inf(-1)) decodeValues.Register("NaN", math.NaN()) decodeValues.Register("-0", negativeZero) // Register math limit constants decodeValues.Register("MaxInt32", int(math.MaxInt32)) decodeValues.Register("MinInt32", int(math.MinInt32)) decodeValues.Register("MaxInt64", int64(math.MaxInt64)) decodeValues.Register("MinInt64", int64(math.MinInt64)) decodeValues.Register("MaxUint32", uint(math.MaxUint32)) decodeValues.Register("MaxUint64", uint64(math.MaxUint64)) decodeValues.Register("MaxFloat32", math.MaxFloat32) decodeValues.Register("MaxFloat64", math.MaxFloat64) decodeValues.Register("SmallestNonzeroFloat32", math.SmallestNonzeroFloat32) decodeValues.Register("SmallestNonzeroFloat64", math.SmallestNonzeroFloat64) } var unmarshalTests = []struct { data string value any }{ { "", (*struct{})(nil), }, { "{}", &struct{}{}, }, // Simple values. { "123", &unmarshalIntTest, }, { "-0", negativeZero, }, { "\"\\t\\n\"\n", "\t\n", }, // Cross-architecture numeric tests { "bin: -0b1000000000000000000000000000000000000000000000000000000000000000", map[string]any{"bin": archSafeInt(math.MinInt64)}, }, { // When unmarshaling into map[string]int64, values that overflow int64 // cannot be decoded and result in an empty map. "int_overflow: 9223372036854775808", // math.MaxInt64 + 1 map[string]int64{}, }, // Structs and type conversions. { "a: 'null'", &struct{ A *unmarshalerType }{&unmarshalerType{"null"}}, }, // Anchors and aliases. { "a: &x 1\nb: &y 2\nc: *x\nd: *y\n", &struct{ A, B, C, D int }{1, 2, 1, 2}, }, { "a: &a {c: 1}\nb: *a", &struct { A, B struct { C int } }{struct{ C int }{1}, struct{ C int }{1}}, }, { "a: &a [1, 2]\nb: *a", &struct{ B []int }{[]int{1, 2}}, }, { "a: &a.b1.c [1, 2]\nb: *a.b1.c", &struct{ B []int }{[]int{1, 2}}, }, // Bug https://github.com/yaml/go-yaml/issues/109 // Bug #1133337 { "foo: ''", map[string]*string{"foo": new(string)}, }, { "foo: null", map[string]*string{"foo": nil}, }, // Support for ~ { "foo: ~", map[string]*string{"foo": nil}, }, // Ignored field { "a: 1\nb: 2\n", &struct { A int B int `yaml:"-"` }{1, 0}, }, // Bug #1191981 { "" + "%YAML 1.1\n" + "--- !!str\n" + `"Generic line break (no glyph)\n\` + "\n" + ` Generic line break (glyphed)\n\` + "\n" + ` Line separator\u2028\` + "\n" + ` Paragraph separator\u2029"` + "\n", "" + "Generic line break (no glyph)\n" + "Generic line break (glyphed)\n" + "Line separator\u2028Paragraph separator\u2029", }, // Duration { "a: 3s", map[string]time.Duration{"a": 3 * time.Second}, }, // Zero duration as a string. { "a: '0'", map[string]time.Duration{"a": 0}, }, // Zero duration as an int. { "a: 0", map[string]time.Duration{"a": 0}, }, // Binary data. { "a: !!binary gIGC\n", map[string]string{"a": "\x80\x81\x82"}, }, { "a: !!binary |\n " + strings.Repeat("kJCQ", 17) + "kJ\n CQ\n", map[string]string{"a": strings.Repeat("\x90", 54)}, }, { "a: !!binary |\n " + strings.Repeat("A", 70) + "\n ==\n", map[string]string{"a": strings.Repeat("\x00", 52)}, }, // Scalar to string conversions. // Float to string (regression test for constructFloat missing reflect.String case). { "a: 55.7351", map[string]string{"a": "55.7351"}, }, { "a: -3.14159", map[string]string{"a": "-3.14159"}, }, { "a: 1.23e10", map[string]string{"a": "1.23e10"}, }, { "a: .inf", map[string]string{"a": ".inf"}, }, { "a: -.inf", map[string]string{"a": "-.inf"}, }, { "a: .nan", map[string]string{"a": ".nan"}, }, // Int to string (verify existing constructInt string case). { "a: 42", map[string]string{"a": "42"}, }, { "a: -100", map[string]string{"a": "-100"}, }, { "a: 0x1A", map[string]string{"a": "0x1A"}, }, { "a: 0o755", map[string]string{"a": "0o755"}, }, // Bool to string (verify existing constructBool string case). { "a: true", map[string]string{"a": "true"}, }, { "a: false", map[string]string{"a": "false"}, }, // Issue #39. { "a:\n b:\n c: d\n", map[string]struct{ B any }{"a": {map[string]any{"c": "d"}}}, }, // Custom map type. { "a: {b: c}", M{"a": M{"b": "c"}}, }, // Support encoding.TextUnmarshaler. { "a: 1.2.3.4\n", map[string]textUnmarshaler{"a": {S: "1.2.3.4"}}, }, { "a: 2015-02-24T18:19:39Z\n", map[string]textUnmarshaler{"a": {"2015-02-24T18:19:39Z"}}, }, // Timestamps { // Date only. "a: 2015-01-01\n", map[string]time.Time{"a": time.Date(2015, 1, 1, 0, 0, 0, 0, time.UTC)}, }, { // RFC3339 "a: 2015-02-24T18:19:39.12Z\n", map[string]time.Time{"a": time.Date(2015, 2, 24, 18, 19, 39, .12e9, time.UTC)}, }, { // RFC3339 with short dates. "a: 2015-2-3T3:4:5Z", map[string]time.Time{"a": time.Date(2015, 2, 3, 3, 4, 5, 0, time.UTC)}, }, { // ISO8601 lower case t "a: 2015-02-24t18:19:39Z\n", map[string]time.Time{"a": time.Date(2015, 2, 24, 18, 19, 39, 0, time.UTC)}, }, { // space separate, no time zone "a: 2015-02-24 18:19:39\n", map[string]time.Time{"a": time.Date(2015, 2, 24, 18, 19, 39, 0, time.UTC)}, }, // Some cases not currently handled. Uncomment these when // the code is fixed. // { // // space separated with time zone // "a: 2001-12-14 21:59:43.10 -5", // map[string]any{"a": time.Date(2001, 12, 14, 21, 59, 43, .1e9, time.UTC)}, // }, // { // // arbitrary whitespace between fields // "a: 2001-12-14 \t\t \t21:59:43.10 \t Z", // map[string]any{"a": time.Date(2001, 12, 14, 21, 59, 43, .1e9, time.UTC)}, // }, { // explicit string tag "a: !!str 2015-01-01", map[string]any{"a": "2015-01-01"}, }, { // explicit timestamp tag on quoted string "a: !!timestamp \"2015-01-01\"", map[string]time.Time{"a": time.Date(2015, 1, 1, 0, 0, 0, 0, time.UTC)}, }, { // explicit timestamp tag on unquoted string "a: !!timestamp 2015-01-01", map[string]time.Time{"a": time.Date(2015, 1, 1, 0, 0, 0, 0, time.UTC)}, }, { // explicit timestamp tag into interface. "a: !!timestamp \"2015-01-01\"", map[string]any{"a": time.Date(2015, 1, 1, 0, 0, 0, 0, time.UTC)}, }, { // implicit timestamp tag into interface. "a: 2015-01-01", map[string]any{"a": time.Date(2015, 1, 1, 0, 0, 0, 0, time.UTC)}, }, // UTF-16-LE { "\xff\xfe\xf1\x00o\x00\xf1\x00o\x00:\x00 \x00v\x00e\x00r\x00y\x00 \x00y\x00e\x00s\x00\n\x00", M{"ñoño": "very yes"}, }, // UTF-16-LE with surrogate. { "\xff\xfe\xf1\x00o\x00\xf1\x00o\x00:\x00 \x00v\x00e\x00r\x00y\x00 \x00y\x00e\x00s\x00 \x00=\xd8\xd4\xdf\n\x00", M{"ñoño": "very yes 🟔"}, }, // UTF-16-BE { "\xfe\xff\x00\xf1\x00o\x00\xf1\x00o\x00:\x00 \x00v\x00e\x00r\x00y\x00 \x00y\x00e\x00s\x00\n", M{"ñoño": "very yes"}, }, // UTF-16-BE with surrogate. { "\xfe\xff\x00\xf1\x00o\x00\xf1\x00o\x00:\x00 \x00v\x00e\x00r\x00y\x00 \x00y\x00e\x00s\x00 \xd8=\xdf\xd4\x00\n", M{"ñoño": "very yes 🟔"}, }, // Comment scan exhausting the input buffer (issue #469). { "true\n#" + strings.Repeat(" ", 512*3), "true", }, { "true #" + strings.Repeat(" ", 512*3), "true", }, { `--- foo: ? complex key : complex value ba?r: a?bc `, map[string]any{ "foo": map[string]any{"complex key": "complex value"}, "ba?r": "a?bc", }, }, // issue https://github.com/yaml/go-yaml/issues/157 { `foo: abc bar: def`, struct { F string `yaml:"foo"` // the correct tag, because it has `yaml` prefix B string `bar` //nolint:govet // the incorrect tag, but supported }{ F: "abc", B: "def", // value should be set using whole tag as a name, see issue: }, }, } type M map[string]any type inlineB struct { B int inlineC `yaml:",inline"` } type inlineC struct { C int } type inlineD struct { C *inlineC `yaml:",inline"` D int } func TestUnmarshal(t *testing.T) { for i, item := range unmarshalTests { t.Run(fmt.Sprintf("test %d: %q", i, item.data), func(t *testing.T) { typ := reflect.ValueOf(item.value).Type() value := reflect.New(typ) err := yaml.Unmarshal([]byte(item.data), value.Interface()) if _, ok := err.(*yaml.LoadErrors); !ok { assert.NoError(t, err) } assert.DeepEqualf(t, item.value, value.Elem().Interface(), "error: %v", err) }) } } func TestDecodeFromYAML(t *testing.T) { datatest.RunTestCases(t, func() ([]map[string]any, error) { return datatest.LoadTestCasesFromFile("testdata/decode.yaml", libyaml.LoadAny) }, map[string]datatest.TestHandler{ "decode": runDecodeTest, }) } func runDecodeTest(t *testing.T, tc map[string]any) { t.Helper() // Get test inputs yamlData, ok := tc["yaml"].(string) if !ok { t.Fatal("yaml field must be string") } // Note: "type" field in YAML is renamed to "output_type" during normalization // to avoid conflict with the test type ("decode") typeName, ok := tc["output_type"].(string) if !ok { t.Fatal("output_type field must be string") } want := tc["want"] // Create target from type registry target, err := decodeTypes.NewPointerInstance(typeName) if err != nil { t.Fatalf("failed to create instance of type %s: %v", typeName, err) } // Unmarshal err = yaml.Unmarshal([]byte(yamlData), target) // Get the actual value (dereference pointer) actual := reflect.ValueOf(target).Elem().Interface() // Resolve constants in want wantResolved := decodeValues.Resolve(want) // Handle construct errors - if error occurred, check if want is empty/nil if err != nil { // For LoadErrors, values that can't be converted are skipped if _, ok := err.(*yaml.LoadErrors); !ok { t.Fatalf("unmarshal failed with unexpected error: %v", err) } // LoadErrors is expected for invalid conversions - compare with want } // Compare by marshaling both to YAML and comparing the output // This handles type differences (e.g., int vs float with same value) actualYAML, err := yaml.Marshal(actual) if err != nil { t.Fatalf("failed to marshal actual: %v", err) } wantYAML, err := yaml.Marshal(wantResolved) if err != nil { t.Fatalf("failed to marshal want: %v", err) } // Compare the YAML representations if string(actualYAML) != string(wantYAML) { t.Fatalf("YAML mismatch:\nGot:\n%s\nWant:\n%s", actualYAML, wantYAML) } } func TestUnmarshalFullTimestamp(t *testing.T) { // Full timestamp in same format as encoded. This is confirmed to be // properly decoded by Python as a timestamp as well. str := "2015-02-24T18:19:39.123456789-03:00" var tm any err := yaml.Unmarshal([]byte(str), &tm) assert.NoError(t, err) expectedTime := time.Date(2015, 2, 24, 18, 19, 39, 123456789, tm.(time.Time).Location()) assert.DeepEqual(t, expectedTime, tm) assert.DeepEqual(t, time.Date(2015, 2, 24, 21, 19, 39, 123456789, time.UTC), tm.(time.Time).In(time.UTC)) } func TestDecoderSingleDocument(t *testing.T) { // Test that Decoder.Decode works as expected on // all the unmarshal tests. for i, item := range unmarshalTests { t.Run(fmt.Sprintf("test %d: %q", i, item.data), func(t *testing.T) { if item.data == "" { // Behavior differs when there's no YAML. return } typ := reflect.ValueOf(item.value).Type() value := reflect.New(typ) err := yaml.NewDecoder(strings.NewReader(item.data)).Decode(value.Interface()) if _, ok := err.(*yaml.LoadErrors); !ok { assert.NoError(t, err) } assert.DeepEqual(t, item.value, value.Elem().Interface()) }) } } var decoderTests = []struct { data string values []any }{{ "", nil, }, { "a: b", []any{ map[string]any{"a": "b"}, }, }, { "---\na: b\n...\n", []any{ map[string]any{"a": "b"}, }, }, { "---\n'hello'\n...\n---\ngoodbye\n...\n", []any{ "hello", "goodbye", }, }} func TestDecoder(t *testing.T) { for i, item := range decoderTests { t.Run(fmt.Sprintf("test %d: %q", i, item.data), func(t *testing.T) { var values []any dec := yaml.NewDecoder(strings.NewReader(item.data)) for { var value any err := dec.Decode(&value) if err == io.EOF { break } assert.NoError(t, err) values = append(values, value) } assert.DeepEqual(t, item.values, values) }) } } type errReader struct{} func (errReader) Read([]byte) (int, error) { return 0, errors.New("some read error") } func TestDecoderReadError(t *testing.T) { err := yaml.NewDecoder(errReader{}).Decode(&struct{}{}) assert.ErrorMatches(t, `go-yaml load error in reader at : input error: some read error`, err) } func TestUnmarshalNaN(t *testing.T) { value := map[string]any{} err := yaml.Unmarshal([]byte("notanum: .NaN"), &value) assert.NoError(t, err) assert.True(t, math.IsNaN(value["notanum"].(float64))) } func TestUnmarshalDurationInt(t *testing.T) { // Don't accept plain ints as durations as it's unclear (issue #200). var d time.Duration err := yaml.Unmarshal([]byte("123"), &d) assert.ErrorMatches(t, "yaml: construct errors: line 1: cannot construct !!int `123` into time.Duration", err) } func TestUnmarshalErrorsFromYAML(t *testing.T) { datatest.RunTestCases(t, func() ([]map[string]any, error) { return datatest.LoadTestCasesFromFile("testdata/unmarshal_errors.yaml", libyaml.LoadAny) }, map[string]datatest.TestHandler{ "unmarshal-error": runUnmarshalErrorTest, }) } func runUnmarshalErrorTest(t *testing.T, tc map[string]any) { t.Helper() yamlInput := datatest.RequireString(t, tc, "yaml") expectedError := datatest.RequireString(t, tc, "want") var target any // If a type is specified, use it; otherwise default to any if typeName, ok := tc["output_type"].(string); ok { var err error target, err = decodeTypes.NewPointerInstance(typeName) if err != nil { t.Fatalf("failed to create instance of type %s: %v", typeName, err) } } else { var value any target = &value } err := yaml.Unmarshal([]byte(yamlInput), target) if err == nil { t.Fatalf("got nil; want error %q - Partial unmarshal: %#v", expectedError, target) } assert.Equalf(t, expectedError, err.Error(), "Partial unmarshal: %#v", target) } func TestDecoderErrors(t *testing.T) { datatest.RunTestCases(t, func() ([]map[string]any, error) { return datatest.LoadTestCasesFromFile("testdata/unmarshal_errors.yaml", libyaml.LoadAny) }, map[string]datatest.TestHandler{ "unmarshal-error": func(t *testing.T, tc map[string]any) { t.Helper() yamlInput := datatest.RequireString(t, tc, "yaml") expectedError := datatest.RequireString(t, tc, "want") var target any // If a type is specified, use it; otherwise default to any if typeName, ok := tc["output_type"].(string); ok { var err error target, err = decodeTypes.NewPointerInstance(typeName) if err != nil { t.Fatalf("failed to create instance of type %s: %v", typeName, err) } } else { var value any target = &value } err := yaml.NewDecoder(strings.NewReader(yamlInput)).Decode(target) if err == nil { t.Fatalf("got nil; want error %q - Partial decode: %#v", expectedError, target) } assert.Equalf(t, expectedError, err.Error(), "Partial decode: %#v", target) }, }) } func TestParserErrorUnmarshal(t *testing.T) { var v struct { A, B int } data := "a: 1\n=\nb: 2" err := yaml.Unmarshal([]byte(data), &v) var asErr *libyaml.LoadError assert.ErrorAs(t, err, &asErr) expectedErr := &libyaml.LoadError{ Stage: libyaml.ScannerStage, ContextMark: libyaml.Mark{ Index: 5, Line: 2, Column: 1, }, ContextMsg: "while scanning a simple key", Mark: libyaml.Mark{ Index: 7, Line: 3, Column: 1, }, Message: "could not find expected ':'", } assert.DeepEqual(t, expectedErr, asErr) } func TestParserErrorDecoder(t *testing.T) { var v any data := "value: -" err := yaml.NewDecoder(strings.NewReader(data)).Decode(&v) var asErr *libyaml.LoadError assert.ErrorAs(t, err, &asErr) expectedErr := &libyaml.LoadError{ Stage: libyaml.ScannerStage, Mark: libyaml.Mark{ Index: 7, Line: 1, Column: 8, }, Message: "block sequence entries are not allowed in this context", } assert.DeepEqual(t, expectedErr, asErr) } var unmarshalerTests = []struct { data, tag string value any }{ {"_: {hi: there}", "!!map", map[string]any{"hi": "there"}}, {"_: [1,A]", "!!seq", []any{1, "A"}}, {"_: 10", "!!int", 10}, {"_: null", "!!null", nil}, {`_: BAR!`, "!!str", "BAR!"}, {`_: "BAR!"`, "!!str", "BAR!"}, {"_: !!foo 'BAR!'", "!!foo", "BAR!"}, {`_: ""`, "!!str", ""}, } var unmarshalerResult = map[int]error{} type unmarshalerType struct { value any } func (o *unmarshalerType) UnmarshalYAML(value *yaml.Node) error { if err := value.Load(&o.value); err != nil { return err } if i, ok := o.value.(int); ok { if result, ok := unmarshalerResult[i]; ok { return result } } return nil } type unmarshalerPointer struct { Field *unmarshalerType `yaml:"_"` } type unmarshalerInlined struct { Field *unmarshalerType `yaml:"_"` Inlined unmarshalerType `yaml:",inline"` } type unmarshalerInlinedTwice struct { InlinedTwice unmarshalerInlined `yaml:",inline"` } type legacyUnmarshalerType struct { value any } func (o *legacyUnmarshalerType) UnmarshalYAML(unmarshal func(v any) error) error { if err := unmarshal(&o.value); err != nil { return err } if i, ok := o.value.(int); ok { if result, ok := unmarshalerResult[i]; ok { return result } } return nil } type legacyUnmarshalerPointer struct { Field *legacyUnmarshalerType `yaml:"_"` } type legacyUnmarshalerValue struct { Field legacyUnmarshalerType `yaml:"_"` } func TestUnmarshalerPointerField(t *testing.T) { for _, item := range unmarshalerTests { obj := &unmarshalerPointer{} err := yaml.Unmarshal([]byte(item.data), obj) assert.NoError(t, err) if item.value == nil { assert.IsNil(t, obj.Field) } else { assert.NotNilf(t, obj.Field, "Pointer not initialized (%#v)", item.value) assert.DeepEqual(t, item.value, obj.Field.value) } } for _, item := range unmarshalerTests { obj := &legacyUnmarshalerPointer{} err := yaml.Unmarshal([]byte(item.data), obj) assert.NoError(t, err) if item.value == nil { assert.IsNil(t, obj.Field) } else { assert.NotNilf(t, obj.Field, "Pointer not initialized (%#v)", item.value) assert.DeepEqual(t, item.value, obj.Field.value) } } } func TestUnmarshalerValueField(t *testing.T) { for _, item := range unmarshalerTests { obj := &legacyUnmarshalerValue{} err := yaml.Unmarshal([]byte(item.data), obj) assert.NoError(t, err) assert.NotNilf(t, obj.Field, "Pointer not initialized (%#v)", item.value) assert.DeepEqual(t, item.value, obj.Field.value) } } func TestUnmarshalerInlinedField(t *testing.T) { obj := &unmarshalerInlined{} err := yaml.Unmarshal([]byte("_: a\ninlined: b\n"), obj) assert.NoError(t, err) assert.DeepEqual(t, &unmarshalerType{"a"}, obj.Field) assert.DeepEqual(t, unmarshalerType{map[string]any{"_": "a", "inlined": "b"}}, obj.Inlined) twc := &unmarshalerInlinedTwice{} err = yaml.Unmarshal([]byte("_: a\ninlined: b\n"), twc) assert.NoError(t, err) assert.DeepEqual(t, &unmarshalerType{"a"}, twc.InlinedTwice.Field) assert.DeepEqual(t, unmarshalerType{map[string]any{"_": "a", "inlined": "b"}}, twc.InlinedTwice.Inlined) } func TestUnmarshalerWholeDocument(t *testing.T) { obj := &legacyUnmarshalerType{} err := yaml.Unmarshal([]byte(unmarshalerTests[0].data), obj) assert.NoError(t, err) value, ok := obj.value.(map[string]any) assert.Truef(t, ok, "value: %#v", obj.value) assert.DeepEqual(t, unmarshalerTests[0].value, value["_"]) } func TestUnmarshalerWholeDocumentModern(t *testing.T) { obj := &unmarshalerType{} err := yaml.Unmarshal([]byte(unmarshalerTests[0].data), obj) assert.NoError(t, err) value, ok := obj.value.(map[string]any) assert.Truef(t, ok, "value: %#v", obj.value) assert.DeepEqual(t, unmarshalerTests[0].value, value["_"]) } func TestUnmarshalerWholeDocumentNull(t *testing.T) { obj := &unmarshalerType{} err := yaml.Unmarshal([]byte("null"), obj) assert.NoError(t, err) assert.DeepEqual(t, unmarshalerType{}, *obj) } func TestUnmarshalerLoadErrors(t *testing.T) { unmarshalerResult[2] = &yaml.LoadErrors{Errors: []*yaml.LoadError{{Stage: yaml.ConstructorStage, Message: "foo", Mark: yaml.Mark{Line: 1, Column: 1}}}} unmarshalerResult[4] = &yaml.LoadErrors{Errors: []*yaml.LoadError{{Stage: yaml.ConstructorStage, Message: "bar", Mark: yaml.Mark{Line: 1, Column: 1}}}} defer func() { delete(unmarshalerResult, 2) delete(unmarshalerResult, 4) }() type T struct { Before int After int M map[string]*unmarshalerType } var v T data := `{before: A, m: {abc: 1, def: 2, ghi: 3, jkl: 4}, after: B}` err := yaml.Unmarshal([]byte(data), &v) expectedError := "" + "yaml: construct errors: " + "line 1: cannot construct !!str `A` into int; " + "line 1: foo; " + "line 1: bar; " + "line 1: cannot construct !!str `B` into int" assert.ErrorMatches(t, expectedError, err) assert.NotNil(t, v.M["abc"]) assert.IsNil(t, v.M["def"]) assert.NotNil(t, v.M["ghi"]) assert.IsNil(t, v.M["jkl"]) assert.Equal(t, 1, v.M["abc"].value) assert.Equal(t, 3, v.M["ghi"].value) } func TestLegacyUnmarshalerLoadErrors(t *testing.T) { unmarshalerResult[2] = &yaml.LoadErrors{Errors: []*yaml.LoadError{{Stage: yaml.ConstructorStage, Message: "foo", Mark: yaml.Mark{Line: 1, Column: 1}}}} unmarshalerResult[4] = &yaml.LoadErrors{Errors: []*yaml.LoadError{{Stage: yaml.ConstructorStage, Message: "bar", Mark: yaml.Mark{Line: 1, Column: 1}}}} defer func() { delete(unmarshalerResult, 2) delete(unmarshalerResult, 4) }() type T struct { Before int After int M map[string]*legacyUnmarshalerType } var v T data := `{before: A, m: {abc: 1, def: 2, ghi: 3, jkl: 4}, after: B}` err := yaml.Unmarshal([]byte(data), &v) expectedError := "" + "yaml: construct errors: " + "line 1: cannot construct !!str `A` into int; " + "line 1: foo; " + "line 1: bar; " + "line 1: cannot construct !!str `B` into int" assert.ErrorMatches(t, expectedError, err) assert.NotNil(t, v.M["abc"]) assert.IsNil(t, v.M["def"]) assert.NotNil(t, v.M["ghi"]) assert.IsNil(t, v.M["jkl"]) assert.Equal(t, 1, v.M["abc"].value) assert.Equal(t, 3, v.M["ghi"].value) } func TestLoadErrors_Unwrapping(t *testing.T) { errSentinel := errors.New("foo") errSentinel2 := errors.New("bar") errUnmarshal := yaml.NewLoadError(yaml.ConstructorStage, "foo", yaml.Mark{Line: 1, Column: 2}, errSentinel) errUnmarshal2 := yaml.NewLoadError(yaml.ConstructorStage, "bar", yaml.Mark{Line: 2, Column: 2}, errSentinel2) // Simulate a LoadErrors err := &yaml.LoadErrors{ Errors: []*yaml.LoadError{ errUnmarshal, errUnmarshal2, }, } var errTarget *yaml.LoadError // check we can unwrap an error assert.ErrorAs(t, err, &errTarget) // check we got the first error assert.ErrorIs(t, errTarget, errUnmarshal) // check we can unwrap any sentinel error wrapped in any UnmarshalError assert.ErrorIs(t, err, errSentinel) assert.ErrorIs(t, err, errSentinel2) var errTargetLegacy *yaml.TypeError // check we can unwrap to legacy TypeError assert.ErrorAs(t, err, &errTargetLegacy) } func TestLoadErrors_Unwrapping_Failures(t *testing.T) { errSentinel := errors.New("foo") errUnmarshal := yaml.NewLoadError(yaml.ConstructorStage, "foo", yaml.Mark{Line: 1, Column: 2}, errSentinel) errUnmarshal2 := yaml.NewLoadError(yaml.ConstructorStage, "bar", yaml.Mark{Line: 2, Column: 2}, errors.New("bar")) // Simulate a LoadErrors err := &yaml.LoadErrors{ Errors: []*yaml.LoadError{ errUnmarshal, errUnmarshal2, }, } var errTarget *yaml.LoadError // check we can unwrap an error assert.ErrorAs(t, err, &errTarget) // check we got the first error assert.ErrorIs(t, errTarget, errUnmarshal) // check we can still unwrap the error wrapped in UnmarshalError assert.ErrorIs(t, errTarget, errSentinel) } type proxyTypeError struct{} func (v *proxyTypeError) UnmarshalYAML(node *yaml.Node) error { var s string var a int32 var b int64 if err := node.Load(&s); err != nil { panic(err) } if s == "a" { if err := node.Load(&b); err == nil { panic("should have failed") } return node.Load(&a) } if err := node.Load(&a); err == nil { panic("should have failed") } return node.Load(&b) } func TestUnmarshalerLoadErrorsProxying(t *testing.T) { type T struct { Before int After int M map[string]*proxyTypeError } var v T data := `{before: A, m: {abc: a, def: b}, after: B}` err := yaml.Unmarshal([]byte(data), &v) expectedError := "" + "yaml: construct errors: " + "line 1: cannot construct !!str `A` into int; " + "line 1: cannot construct !!str `a` into int32; " + "line 1: cannot construct !!str `b` into int64; " + "line 1: cannot construct !!str `B` into int" assert.ErrorMatches(t, expectedError, err) } type legacyProxyTypeError struct{} func (v *legacyProxyTypeError) UnmarshalYAML(unmarshal func(any) error) error { var s string var a int32 var b int64 if err := unmarshal(&s); err != nil { panic(err) } if s == "a" { if err := unmarshal(&b); err == nil { panic("should have failed") } return unmarshal(&a) } if err := unmarshal(&a); err == nil { panic("should have failed") } return unmarshal(&b) } func TestLegacyUnmarshalerLoadErrorsProxying(t *testing.T) { type T struct { Before int After int M map[string]*legacyProxyTypeError } var v T data := `{before: A, m: {abc: a, def: b}, after: B}` err := yaml.Unmarshal([]byte(data), &v) expectedError := "" + "yaml: construct errors: " + "line 1: cannot construct !!str `A` into int; " + "line 1: cannot construct !!str `a` into int32; " + "line 1: cannot construct !!str `b` into int64; " + "line 1: cannot construct !!str `B` into int" assert.ErrorMatches(t, expectedError, err) } var errFailing = errors.New("failingErr") type failingUnmarshaler struct{} func (ft *failingUnmarshaler) UnmarshalYAML(node *yaml.Node) error { return errFailing } func TestUnmarshalerError(t *testing.T) { data := `{foo: 123, bar: {}, spam: "test"}` dst := struct { Foo int Bar *failingUnmarshaler Spam string }{} err := yaml.Unmarshal([]byte(data), &dst) expectedErr := &yaml.LoadErrors{ Errors: []*yaml.LoadError{ yaml.NewLoadError(yaml.ConstructorStage, errFailing.Error(), yaml.Mark{Line: 1, Column: 17}, errFailing), }, } assert.DeepEqual(t, expectedErr, err) // whatever could be unmarshaled must be unmarshaled assert.Equal(t, 123, dst.Foo) assert.DeepEqual(t, &failingUnmarshaler{}, dst.Bar) assert.Equal(t, "test", dst.Spam) } type legacyFailingUnmarshaler struct{} func (ft *legacyFailingUnmarshaler) UnmarshalYAML(unmarshal func(any) error) error { return errFailing } func TestLegacyUnmarshalerError(t *testing.T) { data := `{foo: 123, bar: {}, spam: "test"}` dst := struct { Foo int Bar *legacyFailingUnmarshaler Spam string }{} err := yaml.Unmarshal([]byte(data), &dst) expectedErr := &yaml.LoadErrors{ Errors: []*yaml.LoadError{ yaml.NewLoadError(yaml.ConstructorStage, errFailing.Error(), yaml.Mark{Line: 1, Column: 17}, errFailing), }, } assert.DeepEqual(t, expectedErr, err) // whatever could be unmarshaled must be unmarshaled assert.Equal(t, 123, dst.Foo) assert.DeepEqual(t, &legacyFailingUnmarshaler{}, dst.Bar) assert.Equal(t, "test", dst.Spam) } type failingTextUnmarshaler struct{} var _ encoding.TextUnmarshaler = &failingTextUnmarshaler{} func (ft *failingTextUnmarshaler) UnmarshalText(b []byte) error { return errFailing } func TestTextUnmarshalerError(t *testing.T) { data := `{foo: 123, bar: "456", spam: "test"}` dst := struct { Foo int Bar *failingTextUnmarshaler Spam string }{} err := yaml.Unmarshal([]byte(data), &dst) expectedErr := &yaml.LoadErrors{ Errors: []*yaml.LoadError{ yaml.NewLoadError(yaml.ConstructorStage, errFailing.Error(), yaml.Mark{Line: 1, Column: 17}, errFailing), }, } assert.DeepEqual(t, expectedErr, err) // whatever could be unmarshaled must be unmarshaled assert.Equal(t, 123, dst.Foo) assert.DeepEqual(t, &failingTextUnmarshaler{}, dst.Bar) assert.Equal(t, "test", dst.Spam) } func TestUnmarshalError_Unwrapping(t *testing.T) { errSentinel := errors.New("foo") errUnmarshal := yaml.NewLoadError(yaml.ConstructorStage, "foo", yaml.Mark{Line: 1, Column: 2}, errSentinel) assert.ErrorIs(t, errUnmarshal, errSentinel) } func TestTextUnmarshalerNonScalar(t *testing.T) { dst := struct { A textUnmarshaler }{} inputs := []string{ `a: {}`, `a: []`, } for _, input := range inputs { err := yaml.Unmarshal([]byte(input), &dst) t.Logf("%s -> err=%v", input, err) var target *yaml.LoadErrors if !errors.As(err, &target) { t.Errorf("expected yaml.TypeError, got %v", err) } } } type sliceUnmarshaler []int func (su *sliceUnmarshaler) UnmarshalYAML(node *yaml.Node) error { var slice []int err := node.Load(&slice) if err == nil { *su = slice return nil } var intVal int err = node.Load(&intVal) if err == nil { *su = []int{intVal} return nil } return err } func TestUnmarshalerRetry(t *testing.T) { var su sliceUnmarshaler err := yaml.Unmarshal([]byte("[1, 2, 3]"), &su) assert.NoError(t, err) assert.DeepEqual(t, sliceUnmarshaler([]int{1, 2, 3}), su) err = yaml.Unmarshal([]byte("1"), &su) assert.NoError(t, err) assert.DeepEqual(t, sliceUnmarshaler([]int{1}), su) } type legacySliceUnmarshaler []int func (su *legacySliceUnmarshaler) UnmarshalYAML(unmarshal func(any) error) error { var slice []int err := unmarshal(&slice) if err == nil { *su = slice return nil } var intVal int err = unmarshal(&intVal) if err == nil { *su = []int{intVal} return nil } return err } func TestLegacyUnmarshalerRetry(t *testing.T) { var su legacySliceUnmarshaler err := yaml.Unmarshal([]byte("[1, 2, 3]"), &su) assert.NoError(t, err) assert.DeepEqual(t, legacySliceUnmarshaler([]int{1, 2, 3}), su) err = yaml.Unmarshal([]byte("1"), &su) assert.NoError(t, err) assert.DeepEqual(t, legacySliceUnmarshaler([]int{1}), su) } // nodeKindRecorder records the Kind of the node passed to UnmarshalYAML type nodeKindRecorder struct { Kind libyaml.Kind Data any } func (n *nodeKindRecorder) UnmarshalYAML(node *yaml.Node) error { n.Kind = node.Kind return node.Decode(&n.Data) } // TestUnmarshalerNodeKind verifies that custom unmarshalers receive the // correct node kind (SequenceNode, ScalarNode, MappingNode) rather than // DocumentNode. This is a regression test for issue #274. func TestUnmarshalerNodeKind(t *testing.T) { tests := []struct { name string input string wantKind libyaml.Kind wantData any }{ { name: "sequence", input: "[1, 2, 3]", wantKind: libyaml.SequenceNode, wantData: []any{1, 2, 3}, }, { name: "mapping", input: "foo: bar", wantKind: libyaml.MappingNode, wantData: map[string]any{"foo": "bar"}, }, { name: "scalar", input: "hello", wantKind: libyaml.ScalarNode, wantData: "hello", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var recorder nodeKindRecorder err := yaml.Unmarshal([]byte(tt.input), &recorder) assert.NoError(t, err) assert.Equal(t, tt.wantKind, recorder.Kind) assert.DeepEqual(t, tt.wantData, recorder.Data) }) } } // From http://yaml.org/type/merge.html var mergeTests = ` anchors: list: - &CENTER { "x": 1, "y": 2 } - &LEFT { "x": 0, "y": 2 } - &BIG { "r": 10 } - &SMALL { "r": 1 } # All the following maps are equal: plain: # Explicit keys "x": 1 "y": 2 "r": 10 label: center/big mergeOne: # Merge one map << : *CENTER "r": 10 label: center/big mergeMultiple: # Merge multiple maps << : [ *CENTER, *BIG ] label: center/big override: # Override << : [ *BIG, *LEFT, *SMALL ] "x": 1 label: center/big shortTag: # Explicit short merge tag !!merge "<<" : [ *CENTER, *BIG ] label: center/big longTag: # Explicit merge long tag ! "<<" : [ *CENTER, *BIG ] label: center/big inlineMap: # Inlined map << : {"x": 1, "y": 2, "r": 10} label: center/big inlineSequenceMap: # Inlined map in sequence << : [ *CENTER, {"r": 10} ] label: center/big ` func TestMerge(t *testing.T) { want := map[string]any{ "x": 1, "y": 2, "r": 10, "label": "center/big", } wantStringMap := make(map[string]any) for k, v := range want { wantStringMap[fmt.Sprintf("%v", k)] = v } var m map[any]any err := yaml.Unmarshal([]byte(mergeTests), &m) assert.NoError(t, err) for name, test := range m { if name == "anchors" { continue } if name == "plain" { assert.DeepEqualf(t, wantStringMap, test, "test %q failed", name) continue } assert.DeepEqualf(t, want, test, "test %q failed", name) } } func TestMergeStruct(t *testing.T) { type Data struct { X, Y, R int Label string } want := Data{1, 2, 10, "center/big"} var m map[string]Data err := yaml.Unmarshal([]byte(mergeTests), &m) assert.NoError(t, err) for name, test := range m { if name == "anchors" { continue } assert.DeepEqualf(t, want, test, "test %q failed", name) } } var mergeTestsNested = ` mergeouter1: &mergeouter1 d: 40 e: 50 mergeouter2: &mergeouter2 e: 5 f: 6 g: 70 mergeinner1: &mergeinner1 <<: *mergeouter1 inner: a: 1 b: 2 mergeinner2: &mergeinner2 <<: *mergeouter2 inner: a: -1 b: -2 outer: <<: [*mergeinner1, *mergeinner2] f: 60 inner: a: 10 ` func TestMergeNestedStruct(t *testing.T) { // Issue #818: Merging used to just unmarshal twice on the target // value, which worked for maps as these were replaced by the new map, // but not on struct values as these are preserved. This resulted in // the nested data from the merged map to be mixed up with the data // from the map being merged into. // // This test also prevents two potential bugs from showing up: // // 1) A simple implementation might just zero out the nested value // before unmarshaling the second time, but this would clobber previous // data that is usually respected ({C: 30} below). // // 2) A simple implementation might attempt to handle the key skipping // directly by iterating over the merging map without recursion, but // there are more complex cases that require recursion. // // Quick summary of the fields: // // - A must come from outer and not overridden // - B must not be set as its in the ignored merge // - C should still be set as it's preset in the value // - D should be set from the recursive merge // - E should be set from the first recursive merge, ignored on the second // - F should be set in the inlined map from outer, ignored later // - G should be set in the inlined map from the second recursive merge // type Inner struct { A, B, C int } type Outer struct { D, E int Inner Inner Inline map[string]int `yaml:",inline"` } type Data struct { Outer Outer } test := Data{Outer{0, 0, Inner{C: 30}, nil}} want := Data{Outer{40, 50, Inner{A: 10, C: 30}, map[string]int{"f": 60, "g": 70}}} err := yaml.Unmarshal([]byte(mergeTestsNested), &test) assert.NoError(t, err) assert.DeepEqual(t, want, test) // Repeat test with a map. var testm map[string]any wantm := map[string]any{ "f": 60, "inner": map[string]any{ "a": 10, }, "d": 40, "e": 50, "g": 70, } err = yaml.Unmarshal([]byte(mergeTestsNested), &testm) assert.NoError(t, err) assert.DeepEqual(t, wantm, testm["outer"]) } var unmarshalNullTests = []struct { input string pristine, expected func() any }{{ "null", func() any { var v any = "v"; return &v }, func() any { var v any = nil; return &v }, }, { "null", func() any { s := "s"; return &s }, func() any { s := "s"; return &s }, }, { "null", func() any { s := "s"; sptr := &s; return &sptr }, func() any { var sptr *string; return &sptr }, }, { "null", func() any { i := 1; return &i }, func() any { i := 1; return &i }, }, { "null", func() any { i := 1; iptr := &i; return &iptr }, func() any { var iptr *int; return &iptr }, }, { "null", func() any { m := map[string]int{"s": 1}; return &m }, func() any { var m map[string]int; return &m }, }, { "null", func() any { m := map[string]int{"s": 1}; return m }, func() any { m := map[string]int{"s": 1}; return m }, }, { "s2: null\ns3: null", func() any { m := map[string]int{"s1": 1, "s2": 2}; return m }, func() any { m := map[string]int{"s1": 1, "s2": 2, "s3": 0}; return m }, }, { "s2: null\ns3: null", func() any { m := map[string]any{"s1": 1, "s2": 2}; return m }, func() any { m := map[string]any{"s1": 1, "s2": nil, "s3": nil}; return m }, }} func TestUnmarshalNull(t *testing.T) { for _, test := range unmarshalNullTests { pristine := test.pristine() expected := test.expected() err := yaml.Unmarshal([]byte(test.input), pristine) assert.NoError(t, err) assert.DeepEqual(t, expected, pristine) } } func TestUnmarshalPreservesData(t *testing.T) { var v struct { A, B int C int `yaml:"-"` } v.A = 42 v.C = 88 err := yaml.Unmarshal([]byte("---"), &v) assert.NoError(t, err) assert.Equal(t, 42, v.A) assert.Equal(t, 0, v.B) assert.Equal(t, 88, v.C) err = yaml.Unmarshal([]byte("b: 21\nc: 99"), &v) assert.NoError(t, err) assert.Equal(t, 42, v.A) assert.Equal(t, 21, v.B) assert.Equal(t, 88, v.C) } func TestUnmarshalSliceOnPreset(t *testing.T) { // Issue #48. v := struct{ A []int }{[]int{1}} err := yaml.Unmarshal([]byte("a: [2]"), &v) assert.NoError(t, err) assert.DeepEqual(t, []int{2}, v.A) } var unmarshalStrictTests = []struct { known bool unique bool data string value any error string }{{ known: true, data: "a: 1\nc: 2\n", value: struct{ A, B int }{A: 1}, error: `yaml: construct errors: line 2: field c not found in type struct { A int; B int }`, }, { unique: true, data: "a: 1\nb: 2\na: 3\n", value: struct{ A, B int }{A: 3, B: 2}, error: `yaml: construct errors: line 3: mapping key "a" already defined at line 1`, }, { unique: true, data: "c: 3\na: 1\nb: 2\nc: 4\n", value: struct { A int inlineB `yaml:",inline"` }{ A: 1, inlineB: inlineB{ B: 2, inlineC: inlineC{ C: 4, }, }, }, error: `yaml: construct errors: line 4: mapping key "c" already defined at line 1`, }, { unique: true, data: "c: 0\na: 1\nb: 2\nc: 1\n", value: struct { A int inlineB `yaml:",inline"` }{ A: 1, inlineB: inlineB{ B: 2, inlineC: inlineC{ C: 1, }, }, }, error: `yaml: construct errors: line 4: mapping key "c" already defined at line 1`, }, { unique: true, data: "c: 1\na: 1\nb: 2\nc: 3\n", value: struct { A int M map[string]any `yaml:",inline"` }{ A: 1, M: map[string]any{ "b": 2, "c": 3, }, }, error: `yaml: construct errors: line 4: mapping key "c" already defined at line 1`, }, { unique: true, data: "a: 1\n9: 2\nnull: 3\n9: 4", value: map[any]any{ "a": 1, nil: 3, 9: 4, }, error: `yaml: construct errors: line 4: mapping key "9" already defined at line 2`, }} func TestUnmarshalKnownFields(t *testing.T) { for i, item := range unmarshalStrictTests { t.Logf("test %d: %q", i, item.data) // First test that normal Unmarshal unmarshals to the expected value. if !item.unique { typ := reflect.ValueOf(item.value).Type() value := reflect.New(typ) err := yaml.Unmarshal([]byte(item.data), value.Interface()) assert.NoError(t, err) assert.DeepEqual(t, item.value, value.Elem().Interface()) } // Then test that it fails on the same thing with KnownFields on. typ := reflect.ValueOf(item.value).Type() value := reflect.New(typ) dec := yaml.NewDecoder(bytes.NewBufferString(item.data)) dec.KnownFields(item.known) err := dec.Decode(value.Interface()) assert.ErrorMatches(t, item.error, err) } } type textUnmarshaler struct { S string } func (t *textUnmarshaler) UnmarshalText(s []byte) error { t.S = string(s) return nil } func TestFuzzCrashersFromYAML(t *testing.T) { datatest.RunTestCases(t, func() ([]map[string]any, error) { return datatest.LoadTestCasesFromFile("testdata/fuzz_crashers.yaml", libyaml.LoadAny) }, map[string]datatest.TestHandler{ "fuzz-crasher": runFuzzCrasherTest, }) } func runFuzzCrasherTest(t *testing.T, tc map[string]any) { t.Helper() yamlInput := datatest.RequireString(t, tc, "yaml") // Just unmarshal and ensure it doesn't crash var v any _ = yaml.Unmarshal([]byte(yamlInput), &v) } func TestIssue117(t *testing.T) { data := []byte(` a: <<: - ? - `) x := map[string]any{} err := yaml.Unmarshal(data, &x) if err == nil { t.Errorf("expected error, got none") } } func TestParserErrorUnknownAnchorPosition(t *testing.T) { tests := []struct { data string line int column int }{ {"*x", 1, 1}, {"a: *x", 1, 4}, {"a:\n b: *x", 2, 6}, } for _, test := range tests { var n yaml.Node err := yaml.Unmarshal([]byte(test.data), &n) asErr := new(libyaml.LoadError) assert.ErrorAs(t, err, &asErr) expected := &libyaml.LoadError{ Stage: libyaml.ComposerStage, Message: "unknown anchor 'x' referenced", Mark: libyaml.Mark{ Line: test.line, Column: test.column, }, } assert.DeepEqual(t, expected, asErr) } } var marshalIntTest = 123 var ( encodeTypeRegistry = datatest.NewTypeRegistry() encodeValueRegistry = datatest.NewValueRegistry() ) func init() { // Register basic types encodeTypeRegistry.Register("string", "") // Register map types encodeTypeRegistry.Register("map[string]string", map[string]string{}) encodeTypeRegistry.Register("map[string]any", map[string]any{}) encodeTypeRegistry.Register("map[string]uint", map[string]uint{}) encodeTypeRegistry.Register("map[string]int64", map[string]int64{}) encodeTypeRegistry.Register("map[string]uint64", map[string]uint64{}) encodeTypeRegistry.Register("map[string][]string", map[string][]string{}) encodeTypeRegistry.Register("map[string][]any", map[string][]any{}) encodeTypeRegistry.Register("map[string][]map[string]any", map[string][]map[string]any{}) // Register struct types encodeTypeRegistry.Register("testStructHello", testStructHello{}) encodeTypeRegistry.Register("testStructA_Int", testStructA_Int{}) encodeTypeRegistry.Register("testStructA_Float64", testStructA_Float64{}) encodeTypeRegistry.Register("testStructA_Bool", testStructA_Bool{}) encodeTypeRegistry.Register("testStructA_String", testStructA_String{}) encodeTypeRegistry.Register("testStructA_IntSlice", testStructA_IntSlice{}) encodeTypeRegistry.Register("testStructA_IntArray2", testStructA_IntArray2{}) encodeTypeRegistry.Register("testStructA_NestedB", testStructA_NestedB{}) encodeTypeRegistry.Register("testStructA_NestedBPtr", testStructA_NestedBPtr{}) encodeTypeRegistry.Register("testStructEmpty", testStructEmpty{}) // Register struct types with yaml tags encodeTypeRegistry.Register("testStructB_Int_TagA", testStructB_Int_TagA{}) // Register value constants encodeValueRegistry.Register("+Inf", math.Inf(+1)) encodeValueRegistry.Register("-Inf", math.Inf(-1)) encodeValueRegistry.Register("NaN", math.NaN()) encodeValueRegistry.Register("-0", negativeZero) } var marshalTests = []struct { value any data string }{ { (*marshalerType)(nil), "null\n", }, // Simple values. { &marshalIntTest, "123\n", }, { negativeZero, "-0\n", }, { "\t\n", "\"\\t\\n\"\n", }, // Conditional flag { &struct { A int `yaml:"a,omitempty"` B int `yaml:"b,omitempty"` }{1, 0}, "a: 1\n", }, { &struct { A int `yaml:"a,omitempty"` B int `yaml:"b,omitempty"` }{0, 0}, "{}\n", }, { &struct { A *struct{ X, y int } `yaml:"a,omitempty,flow"` }{&struct{ X, y int }{1, 2}}, "a: {x: 1}\n", }, { &struct { A *struct{ X, y int } `yaml:"a,omitempty,flow"` }{nil}, "{}\n", }, { &struct { A *struct{ X, y int } `yaml:"a,omitempty,flow"` }{&struct{ X, y int }{}}, "a: {x: 0}\n", }, { &struct { A struct{ X, y int } `yaml:"a,omitempty,flow"` }{struct{ X, y int }{1, 2}}, "a: {x: 1}\n", }, { &struct { A struct{ X, y int } `yaml:"a,omitempty,flow"` }{struct{ X, y int }{0, 1}}, "{}\n", }, { &struct { A float64 `yaml:"a,omitempty"` B float64 `yaml:"b,omitempty"` }{1, 0}, "a: 1\n", }, { &struct { T1 time.Time `yaml:"t1,omitempty"` T2 time.Time `yaml:"t2,omitempty"` T3 *time.Time `yaml:"t3,omitempty"` T4 *time.Time `yaml:"t4,omitempty"` }{ T2: time.Date(2018, 1, 9, 10, 40, 47, 0, time.UTC), T4: newTime(time.Date(2098, 1, 9, 10, 40, 47, 0, time.UTC)), }, "t2: 2018-01-09T10:40:47Z\nt4: 2098-01-09T10:40:47Z\n", }, // Nil interface that implements Marshaler. { map[string]yaml.Marshaler{ "a": nil, }, "a: null\n", }, // Flow flag { &struct { A []int `yaml:"a,flow"` }{[]int{1, 2}}, "a: [1, 2]\n", }, { &struct { A map[string]string `yaml:"a,flow"` }{map[string]string{"b": "c", "d": "e"}}, "a: {b: c, d: e}\n", }, { &struct { A struct { B, D string } `yaml:"a,flow"` }{struct{ B, D string }{"c", "e"}}, "a: {b: c, d: e}\n", }, { &struct { A string `yaml:"a,flow"` }{"b\nc"}, "a: \"b\\nc\"\n", }, // Unexported field { &struct { u int A int }{0, 1}, "a: 1\n", }, // Ignored field { &struct { A int B int `yaml:"-"` }{1, 2}, "a: 1\n", }, // Struct inlining { &struct { A int C inlineB `yaml:",inline"` }{1, inlineB{2, inlineC{3}}}, "a: 1\nb: 2\nc: 3\n", }, // Struct inlining as a pointer { &struct { A int C *inlineB `yaml:",inline"` }{1, &inlineB{2, inlineC{3}}}, "a: 1\nb: 2\nc: 3\n", }, { &struct { A int C *inlineB `yaml:",inline"` }{1, nil}, "a: 1\n", }, { &struct { A int D *inlineD `yaml:",inline"` }{1, &inlineD{&inlineC{3}, 4}}, "a: 1\nc: 3\nd: 4\n", }, // Map inlining { &struct { A int C map[string]int `yaml:",inline"` }{1, map[string]int{"b": 2, "c": 3}}, "a: 1\nb: 2\nc: 3\n", }, // Duration { map[string]time.Duration{"a": 3 * time.Second}, "a: 3s\n", }, // Binary data. { map[string]string{"a": "\x80\x81\x82"}, "a: !!binary gIGC\n", }, { map[string]string{"a": strings.Repeat("\x90", 54)}, "a: !!binary |\n " + strings.Repeat("kJCQ", 17) + "kJ\n CQ\n", }, // Support encoding.TextMarshaler. { map[string]net.IP{"a": net.IPv4(1, 2, 3, 4)}, "a: 1.2.3.4\n", }, // time.Time gets a timestamp tag. { map[string]time.Time{"a": time.Date(2015, 2, 24, 18, 19, 39, 0, time.UTC)}, "a: 2015-02-24T18:19:39Z\n", }, { map[string]*time.Time{"a": newTime(time.Date(2015, 2, 24, 18, 19, 39, 0, time.UTC))}, "a: 2015-02-24T18:19:39Z\n", }, { // This is confirmed to be properly decoded in Python (libyaml) without a timestamp tag. map[string]time.Time{"a": time.Date(2015, 2, 24, 18, 19, 39, 123456789, time.FixedZone("FOO", -3*60*60))}, "a: 2015-02-24T18:19:39.123456789-03:00\n", }, // Ensure MarshalYAML also gets called on the result of MarshalYAML itself. { &marshalerType{marshalerType{true}}, "true\n", }, { &marshalerType{&marshalerType{true}}, "true\n", }, // yaml.Node { &struct { Value yaml.Node }{ yaml.Node{ Kind: yaml.ScalarNode, Tag: "!!str", Value: "foo", Style: yaml.SingleQuotedStyle, }, }, "value: 'foo'\n", }, { yaml.Node{ Kind: yaml.ScalarNode, Tag: "!!str", Value: "foo", Style: yaml.SingleQuotedStyle, }, "'foo'\n", }, // Enforced tagging with shorthand notation (issue #616). { &struct { Value yaml.Node }{ yaml.Node{ Kind: yaml.ScalarNode, Style: yaml.TaggedStyle, Value: "foo", Tag: "!!str", }, }, "value: !!str foo\n", }, { &struct { Value yaml.Node }{ yaml.Node{ Kind: yaml.MappingNode, Style: yaml.TaggedStyle, Tag: "!!map", }, }, "value: !!map {}\n", }, { &struct { Value yaml.Node }{ yaml.Node{ Kind: yaml.SequenceNode, Style: yaml.TaggedStyle, Tag: "!!seq", }, }, "value: !!seq []\n", }, // bug: question mark in value { map[string]any{ "foo": map[string]any{"bar": "a?bc"}, }, "foo:\n bar: a?bc\n", }, // issue https://github.com/yaml/go-yaml/issues/157 { struct { F string `yaml:"foo"` // the correct tag, because it has `yaml` prefix B string `bar` //nolint:govet // the incorrect tag, but supported }{ F: "abc", B: "def", // value should be set using whole tag as a name, see issue: }, "foo: abc\nbar: def\n", }, } func TestMarshal(t *testing.T) { defer os.Setenv("TZ", os.Getenv("TZ")) os.Setenv("TZ", "UTC") for i, item := range marshalTests { t.Run(fmt.Sprintf("test %d: %q", i, item.data), func(t *testing.T) { data, err := yaml.Marshal(item.value) assert.NoError(t, err) assert.Equal(t, item.data, string(data)) }) } } func TestEncodeToYAML(t *testing.T) { datatest.RunTestCases(t, func() ([]map[string]any, error) { return datatest.LoadTestCasesFromFile("testdata/encode.yaml", libyaml.LoadAny) }, map[string]datatest.TestHandler{ "encode": runEncodeTest, "encode-opts": runEncodeOptsTest, }) } func runEncodeTest(t *testing.T, tc map[string]any) { t.Helper() // Get type and create instance // Note: "type" field in YAML is renamed to "output_type" during normalization // to avoid conflict with the test type ("encode") typeName, ok := tc["output_type"].(string) if !ok { t.Fatalf("output_type must be a string") } data := tc["data"] // Create pointer target of the specified type (for addressability) targetPtr, err := encodeTypeRegistry.NewPointerInstance(typeName) if err != nil { t.Fatalf("Failed to create instance of type %s: %v", typeName, err) } // Resolve value constants (like +Inf, -Inf, NaN, -0) resolvedData := encodeValueRegistry.Resolve(data) // Unmarshal the data into the target to populate it dataBytes, err := yaml.Marshal(resolvedData) if err != nil { t.Fatalf("Failed to marshal data: %v", err) } err = yaml.Unmarshal(dataBytes, targetPtr) if err != nil { t.Fatalf("Failed to unmarshal data into target: %v", err) } // Marshal the target back to YAML output, err := yaml.Marshal(targetPtr) if err != nil { t.Fatalf("Marshal failed: %v", err) } // Compare with expected output want, ok := tc["want"].(string) if !ok { t.Fatalf("want must be a string") } assert.Equal(t, want, string(output)) } func runEncodeOptsTest(t *testing.T, tc map[string]any) { t.Helper() // Get type and create instance // Note: "type" field in YAML is renamed to "output_type" during normalization // to avoid conflict with the test type ("encode-opts") typeName, ok := tc["output_type"].(string) if !ok { t.Fatalf("output_type must be a string") } data := tc["data"] // Parse options var opts []yaml.Option if optsRaw, exists := tc["opts"]; exists { optsMap, ok := optsRaw.(map[string]any) if !ok { t.Fatalf("opts must be a map, got %T", optsRaw) } if rqRaw, exists := optsMap["required-quotes"]; exists { rq, ok := rqRaw.(string) if !ok { t.Fatalf("required-quotes must be a string, got %T", rqRaw) } switch rq { case "single": opts = append(opts, yaml.WithQuotePreference(yaml.QuoteSingle)) case "double": opts = append(opts, yaml.WithQuotePreference(yaml.QuoteDouble)) case "legacy": opts = append(opts, yaml.WithQuotePreference(yaml.QuoteLegacy)) default: t.Fatalf("Unknown required-quotes value: %s", rq) } } } // Create pointer target of the specified type (for addressability) targetPtr, err := encodeTypeRegistry.NewPointerInstance(typeName) if err != nil { t.Fatalf("Failed to create instance of type %s: %v", typeName, err) } // Resolve value constants (like +Inf, -Inf, NaN, -0) resolvedData := encodeValueRegistry.Resolve(data) // Unmarshal the data into the target to populate it dataBytes, err := yaml.Marshal(resolvedData) if err != nil { t.Fatalf("Failed to marshal data: %v", err) } err = yaml.Unmarshal(dataBytes, targetPtr) if err != nil { t.Fatalf("Failed to unmarshal data into target: %v", err) } // Dump the target to YAML with options output, err := yaml.Dump(targetPtr, opts...) if err != nil { t.Fatalf("Dump failed: %v", err) } // Compare with expected output want, ok := tc["want"].(string) if !ok { t.Fatalf("want must be a string") } assert.Equal(t, want, string(output)) } func TestEncoderSingleDocument(t *testing.T) { for i, item := range marshalTests { t.Run(fmt.Sprintf("test %d. %q", i, item.data), func(t *testing.T) { var buf bytes.Buffer enc := yaml.NewEncoder(&buf) err := enc.Encode(item.value) assert.NoError(t, err) err = enc.Close() assert.NoError(t, err) assert.Equal(t, item.data, buf.String()) }) } } func TestEncoderMultipleDocuments(t *testing.T) { var buf bytes.Buffer enc := yaml.NewEncoder(&buf) err := enc.Encode(map[string]string{"a": "b"}) assert.NoError(t, err) err = enc.Encode(map[string]string{"c": "d"}) assert.NoError(t, err) err = enc.Close() assert.NoError(t, err) assert.Equal(t, "a: b\n---\nc: d\n", buf.String()) } func TestEncoderWriteError(t *testing.T) { enc := yaml.NewEncoder(errorWriter{}) err := enc.Encode(map[string]string{"a": "b"}) assert.ErrorMatches(t, `go-yaml dump error in writer: some write error`, err) var dumpErr *yaml.DumpError assert.True(t, errors.As(err, &dumpErr)) assert.Equal(t, yaml.WriterStage, dumpErr.Stage) } type errorWriter struct{} func (errorWriter) Write([]byte) (int, error) { return 0, fmt.Errorf("some write error") } var marshalErrorTests = []struct { value any error string stage yaml.Stage }{{ value: &struct { B int inlineB `yaml:",inline"` }{1, inlineB{2, inlineC{3}}}, //nolint:dupword // struct is duplicated here as the first one is the struct and the second is the name of the inline struct error: `go-yaml dump error in representer: duplicated key 'b' in struct struct \{ B int; .*`, stage: yaml.RepresenterStage, }, { value: &struct { A int B map[string]int `yaml:",inline"` }{1, map[string]int{"a": 2}}, error: `go-yaml dump error in representer: cannot have key "a" in inlined map: conflicts with struct field`, stage: yaml.RepresenterStage, }} func TestMarshalErrors(t *testing.T) { for _, item := range marshalErrorTests { t.Run(item.error, func(t *testing.T) { _, err := yaml.Marshal(item.value) assert.ErrorMatches(t, item.error, err) var dumpErr *yaml.DumpError assert.True(t, errors.As(err, &dumpErr)) assert.Equal(t, item.stage, dumpErr.Stage) }) } } func TestMarshalTypeCache(t *testing.T) { var data []byte var err error func() { type T struct{ A int } data, err = yaml.Marshal(&T{}) assert.NoError(t, err) }() func() { type T struct{ B int } data, err = yaml.Marshal(&T{}) assert.NoError(t, err) }() assert.Equal(t, "b: 0\n", string(data)) } var marshalerTests = []struct { data string value any }{ {"_:\n hi: there\n", map[any]any{"hi": "there"}}, {"_:\n - 1\n - A\n", []any{1, "A"}}, {"_: 10\n", 10}, {"_: null\n", nil}, {"_: BAR!\n", "BAR!"}, } type marshalerType struct { value any } func (o marshalerType) MarshalText() ([]byte, error) { panic("MarshalText called on type with MarshalYAML") } func (o marshalerType) MarshalYAML() (any, error) { return o.value, nil } type marshalerValue struct { Field marshalerType `yaml:"_"` } func TestMarshaler(t *testing.T) { for _, item := range marshalerTests { t.Run(item.data, func(t *testing.T) { obj := &marshalerValue{} obj.Field.value = item.value data, err := yaml.Marshal(obj) assert.NoError(t, err) assert.Equal(t, item.data, string(data)) }) } } func TestMarshalerWholeDocument(t *testing.T) { obj := &marshalerType{} obj.value = map[string]string{"hello": "world!"} data, err := yaml.Marshal(obj) assert.NoError(t, err) assert.Equal(t, "hello: world!\n", string(data)) } type failingMarshaler struct{} func (ft *failingMarshaler) MarshalYAML() (any, error) { return nil, errFailing } func TestMarshalerError(t *testing.T) { _, err := yaml.Marshal(&failingMarshaler{}) assert.ErrorIs(t, err, errFailing) } func TestSetIndent(t *testing.T) { var buf bytes.Buffer enc := yaml.NewEncoder(&buf) enc.SetIndent(8) err := enc.Encode(map[string]any{"a": map[string]any{"b": map[string]string{"c": "d"}}}) assert.NoError(t, err) err = enc.Close() assert.NoError(t, err) assert.Equal(t, "a:\n b:\n c: d\n", buf.String()) } func TestSortedOutput(t *testing.T) { order := []any{ false, true, 1, uint(1), 1.0, 1.1, 1.2, 2, uint(2), 2.0, 2.1, "", ".1", ".2", ".a", "1", "2", "a!10", "a/0001", "a/002", "a/3", "a/10", "a/11", "a/0012", "a/100", "a~10", "ab/1", "b/1", "b/01", "b/2", "b/02", "b/3", "b/03", "b1", "b01", "b3", "c2.10", "c10.2", "d1", "d7", "d7abc", "d12", "d12a", "e2b", "e4b", "e21a", } m := make(map[any]int) for _, k := range order { m[k] = 1 } data, err := yaml.Marshal(m) assert.NoError(t, err) out := "\n" + string(data) last := 0 for i, k := range order { repr := fmt.Sprint(k) if s, ok := k.(string); ok { if _, err = strconv.ParseFloat(repr, 32); s == "" || err == nil { repr = `"` + repr + `"` } } index := strings.Index(out, "\n"+repr+":") if index == -1 { t.Fatalf("%#v is not in the output: %#v", k, out) } if index < last { t.Fatalf("%#v was generated before %#v: %q", k, order[i-1], out) } last = index } } func newTime(t time.Time) *time.Time { return &t } func TestCompactSeqIndentDefault(t *testing.T) { var buf bytes.Buffer enc := yaml.NewEncoder(&buf) enc.CompactSeqIndent() err := enc.Encode(map[string]any{"a": []string{"b", "c"}}) assert.NoError(t, err) err = enc.Close() assert.NoError(t, err) // The default indent is 4, so these sequence elements get 2 indents as before assert.Equal(t, `a: - b - c `, buf.String()) } func TestCompactSequenceWithSetIndent(t *testing.T) { var buf bytes.Buffer enc := yaml.NewEncoder(&buf) enc.CompactSeqIndent() enc.SetIndent(2) err := enc.Encode(map[string]any{"a": []string{"b", "c"}}) assert.NoError(t, err) err = enc.Close() assert.NoError(t, err) // The sequence indent is 2, so these sequence elements don't get indented at all assert.Equal(t, `a: - b - c `, buf.String()) } type ( normal string compact string ) // newlinePlusNormalToNewlinePlusCompact maps the normal encoding (prefixed with a newline) // to the compact encoding (prefixed with a newline), for test cases in marshalTests var newlinePlusNormalToNewlinePlusCompact = map[normal]compact{ normal(` v: - A - B `): compact(` v: - A - B `), normal(` v: - A - |- B C `): compact(` v: - A - |- B C `), normal(` v: - A - 1 - B: - 2 - 3 `): compact(` v: - A - 1 - B: - 2 - 3 `), normal(` a: - 1 - 2 `): compact(` a: - 1 - 2 `), normal(` a: b: - c: 1 d: 2 `): compact(` a: b: - c: 1 d: 2 `), } func TestEncoderCompactIndents(t *testing.T) { for i, item := range marshalTests { t.Run(fmt.Sprintf("test %d. %q", i, item.data), func(t *testing.T) { var buf bytes.Buffer enc := yaml.NewEncoder(&buf) enc.CompactSeqIndent() err := enc.Encode(item.value) assert.NoError(t, err) err = enc.Close() assert.NoError(t, err) // Default to expecting the item data expected := item.data // If there's a different compact representation, use that if c, ok := newlinePlusNormalToNewlinePlusCompact[normal("\n"+item.data)]; ok { expected = string(c[1:]) } assert.Equal(t, expected, buf.String()) }) } } func TestNewLinePreserved(t *testing.T) { obj := &marshalerValue{} obj.Field.value = "a:\n b:\n c: d\n" data, err := yaml.Marshal(obj) assert.NoError(t, err) assert.Equal(t, "_: |\n a:\n b:\n c: d\n", string(data)) obj.Field.value = "\na:\n b:\n c: d\n" data, err = yaml.Marshal(obj) assert.NoError(t, err) // the newline at the start of the file should be preserved assert.Equal(t, "_: |\n\n a:\n b:\n c: d\n", string(data)) } // Scalar style tests for complex whitespace (tabs and Unicode) // These tests are kept in Go because they involve whitespace characters // that are difficult to represent accurately in YAML test data files. func TestScalarStyleWithTabs(t *testing.T) { testCases := []struct { input string expected string desc string }{ { "\t\n", "\"\\t\\n\"\n", "Tab + newline", }, { "\t", "\"\\t\"\n", "Just tab", }, { "hello\tworld", "\"hello\\tworld\"\n", "Text with tab", }, { "\tThis starts with tab\nand is long enough\nfor literal style", "|-\n \tThis starts with tab\n and is long enough\n for literal style\n", "Multiline starting with tab", }, { "\tB\n\tC\n", "|\n \tB\n \tC\n", "Tab B newline tab C newline", }, { "\ta\n", "|\n \ta\n", "Tab + char + newline", }, { "\thello\n", "|\n \thello\n", "Tab + text + newline", }, { "\t\nhello", "|-\n \t\n hello\n", "Tab + newline + text", }, } for i, testCase := range testCases { t.Run(fmt.Sprintf("test_%d_%s", i, testCase.desc), func(t *testing.T) { data, err := yaml.Marshal(testCase.input) assert.NoError(t, err) assert.Equal(t, testCase.expected, string(data)) }) } } func TestUnicodeWhitespaceHandling(t *testing.T) { // Test cases for Unicode whitespace characters that should be properly handled // by the shouldUseLiteralStyle function using unicode.IsSpace() testCases := []struct { input string expected string desc string }{ // Unicode whitespace characters { "hello\u00A0\n", // non-breaking space "|\n hello\u00A0\n", "Non-breaking space with content", }, { "\u00A0\n", // non-breaking space "\"\u00A0\\n\"\n", "Non-breaking space only", }, { "hello\u2000\n", // en quad "|\n hello\u2000\n", "En quad with content", }, { "\u2000\n", // en quad "\"\u2000\\n\"\n", "En quad only", }, { "hello\u2001\n", // em quad "|\n hello\u2001\n", "Em quad with content", }, { "\u2001\n", // em quad "\"\u2001\\n\"\n", "Em quad only", }, { "hello\u2002\n", // en space "|\n hello\u2002\n", "En space with content", }, { "\u2002\n", // en space "\"\u2002\\n\"\n", "En space only", }, { "hello\u2003\n", // em space "|\n hello\u2003\n", "Em space with content", }, { "\u2003\n", // em space "\"\u2003\\n\"\n", "Em space only", }, { "hello\u2004\n", // three-per-em space "|\n hello\u2004\n", "Three-per-em space with content", }, { "\u2004\n", // three-per-em space "\"\u2004\\n\"\n", "Three-per-em space only", }, { "hello\u2005\n", // four-per-em space "|\n hello\u2005\n", "Four-per-em space with content", }, { "\u2005\n", // four-per-em space "\"\u2005\\n\"\n", "Four-per-em space only", }, { "hello\u2006\n", // six-per-em space "|\n hello\u2006\n", "Six-per-em space with content", }, { "\u2006\n", // six-per-em space "\"\u2006\\n\"\n", "Six-per-em space only", }, { "hello\u2007\n", // figure space "|\n hello\u2007\n", "Figure space with content", }, { "\u2007\n", // figure space "\"\u2007\\n\"\n", "Figure space only", }, { "hello\u2008\n", // punctuation space "|\n hello\u2008\n", "Punctuation space with content", }, { "\u2008\n", // punctuation space "\"\u2008\\n\"\n", "Punctuation space only", }, { "hello\u2009\n", // thin space "|\n hello\u2009\n", "Thin space with content", }, { "\u2009\n", // thin space "\"\u2009\\n\"\n", "Thin space only", }, { "hello\u200A\n", // hair space "|\n hello\u200A\n", "Hair space with content", }, { "\u200A\n", // hair space "\"\u200A\\n\"\n", "Hair space only", }, // Other Unicode whitespace { "hello\u2028\n", // line separator "|+\n hello\u2028\n", "Line separator with content", }, { "\u2028\n", // line separator "\"\\L\\n\"\n", "Line separator only", }, { "hello\u2029\n", // paragraph separator "|+\n hello\u2029\n", "Paragraph separator with content", }, { "\u2029\n", // paragraph separator "\"\\P\\n\"\n", "Paragraph separator only", }, { "hello\u205F\n", // medium mathematical space "|\n hello\u205F\n", "Medium mathematical space with content", }, { "\u205F\n", // medium mathematical space "\"\u205F\\n\"\n", "Medium mathematical space only", }, { "hello\u3000\n", // ideographic space "|\n hello\u3000\n", "Ideographic space with content", }, { "\u3000\n", // ideographic space "\"\u3000\\n\"\n", "Ideographic space only", }, // Mixed Unicode whitespace { "hello\u00A0\u2000\u2001\n", // mixed Unicode spaces "|\n hello\u00A0\u2000\u2001\n", "Mixed Unicode spaces with content", }, { "\u00A0\u2000\u2001\n", // mixed Unicode spaces "\"\u00A0\u2000\u2001\\n\"\n", "Mixed Unicode spaces only", }, // Unicode whitespace with ASCII whitespace { "hello \u00A0\t\n", // ASCII + Unicode spaces "|\n hello \u00A0\t\n", "ASCII + Unicode spaces with content", }, { " \u00A0\t\n", // ASCII + Unicode spaces "\" \u00A0\\t\\n\"\n", "ASCII + Unicode spaces only", }, } for i, testCase := range testCases { t.Run(fmt.Sprintf("test_%d_%s", i, testCase.desc), func(t *testing.T) { data, err := yaml.Marshal(testCase.input) assert.NoError(t, err) assert.Equal(t, testCase.expected, string(data)) }) } } func TestOptsYAML(t *testing.T) { tests := []struct { name string yamlStr string expectErr bool errMatch string }{ { name: "valid options", yamlStr: "indent: 4\nknown-fields: true", expectErr: false, }, { name: "typo in field name", yamlStr: "knnown-fields: true", expectErr: true, errMatch: "knnown-fields not found", }, { name: "another typo", yamlStr: "indnt: 2", expectErr: true, errMatch: "indnt not found", }, { name: "multiple options with one typo", yamlStr: "indent: 2\nunicoode: true", expectErr: true, errMatch: "unicoode not found", }, { name: "all valid options", yamlStr: ` indent: 2 compact-seq-indent: true line-width: 80 unicode: true canonical: false line-break: ln explicit-start: true explicit-end: false flow-simple-coll: true known-fields: true single-document: true unique-keys: true `, expectErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { opt, err := yaml.OptsYAML(tt.yamlStr) if tt.expectErr { if err == nil { t.Fatalf("expected error but got none") } if tt.errMatch != "" && !strings.Contains(err.Error(), tt.errMatch) { t.Errorf("expected error to contain %q, got: %v", tt.errMatch, err) } } else { if err != nil { t.Fatalf("unexpected error: %v", err) } if opt == nil { t.Fatal("expected non-nil option") } } }) } } // FuzzEncodeFromJSON checks that any JSON encoded value can also be encoded as YAML... and decoded. func FuzzEncodeFromJSON(f *testing.F) { // Load seed corpus from testdata YAML file cases, err := datatest.LoadTestCasesFromFile("testdata/fuzz_json_roundtrip.yaml", libyaml.LoadAny) if err != nil { f.Fatalf("Failed to load seed corpus: %v", err) } // Add each seed to the fuzz corpus for _, tc := range cases { if jsonInput, ok := datatest.GetString(tc, "json"); ok { f.Add(jsonInput) } } f.Fuzz(func(t *testing.T, s string) { var v any if err := json.Unmarshal([]byte(s), &v); err != nil { t.Skipf("not valid JSON %q", s) } t.Logf("JSON %q", s) t.Logf("Go %q <%[1]x>", v) // Encode as YAML b, err := yaml.Marshal(v) if err != nil { t.Error(err) } t.Logf("YAML %q <%[1]x>", b) // Decode as YAML var v2 any if err := yaml.Unmarshal(b, &v2); err != nil { t.Error(err) } t.Logf("Go %q <%[1]x>", v2) b2, err := yaml.Marshal(v2) if err != nil { t.Error(err) } t.Logf("YAML %q <%[1]x>", b2) if !bytes.Equal(b, b2) { t.Errorf("Marshal->Unmarshal->Marshal mismatch:\n- expected: %q\n- got: %q", b, b2) } }) } func TestPlugins(t *testing.T) { datatest.RunTestCases(t, func() ([]map[string]any, error) { return datatest.LoadTestCasesFromFile("testdata/plugin.yaml", libyaml.LoadAny) }, map[string]datatest.TestHandler{ "plugin-pass": runPluginTest, "plugin-error": runPluginTest, }) } func runPluginTest(t *testing.T, tc map[string]any) { t.Helper() // Build YAML string from plugin config for OptsYAML pluginCfg := tc["plugin"] pluginYAML, err := yaml.Dump(map[string]any{"plugin": pluginCfg}) if err != nil { t.Fatalf("Failed to marshal plugin config: %v", err) } opts, err := yaml.OptsYAML(string(pluginYAML)) if err != nil { t.Fatalf("OptsYAML failed: %v", err) } // Generate data from spec data, err := datatest.GenerateData(tc["data"]) if err != nil { t.Fatalf("Failed to generate data: %v", err) } // Load with plugin options var result any err = yaml.Load(data, &result, opts) // Check result expectedError := "" if wantVal, hasWant := tc["want"]; hasWant { if s, ok := wantVal.(string); ok { expectedError = s } } if expectedError != "" { if err == nil { t.Fatalf("expected error %q, got nil", expectedError) } assert.Equal(t, expectedError, err.Error()) return } assert.NoError(t, err) } func TestLimit(t *testing.T) { datatest.RunTestCases(t, func() ([]map[string]any, error) { return datatest.LoadTestCasesFromFile("testdata/limit.yaml", libyaml.LoadAny) }, map[string]datatest.TestHandler{ "limit": runLimitTest, "limit-error": runLimitTest, "limit-pass": runLimitTest, }) } func runLimitTest(t *testing.T, tc map[string]any) { t.Helper() // Generate data from spec dataSpec := tc["data"] data, err := datatest.GenerateData(dataSpec) if err != nil { t.Fatalf("Failed to generate data: %v", err) } // Get expected error if any (for limit-error tests) // For limit-pass tests, want might be a map describing expected structure expectedError := "" if wantVal, hasWant := tc["want"]; hasWant { switch v := wantVal.(type) { case string: expectedError = v case map[string]any: // Future: could validate structure here // For now, just ignore (treated as success case) default: t.Fatalf("want field must be a string or map, got %T", wantVal) } } // Run unmarshal var v any err = yaml.Unmarshal(data, &v) if expectedError != "" { if err == nil { t.Fatalf("expected error %q, got nil", expectedError) } assert.ErrorMatches(t, expectedError, err) return } assert.NoError(t, err) } // Keep benchmark using hardcoded data for performance consistency var limitTests = []struct { name string data []byte error string }{ { name: "1000kb of maps with 100 aliases", data: []byte(`{a: &a [{a}` + strings.Repeat(`,{a}`, 1000*1024/4-100) + `], b: &b [*a` + strings.Repeat(`,*a`, 99) + `]}`), error: "yaml: document contains excessive aliasing", }, { name: "1000kb of deeply nested slices", data: []byte(strings.Repeat(`[`, 1000*1024)), error: "yaml: while increasing flow level at line 1, column 10001: exceeded max depth of 10000", }, { name: "1000kb of deeply nested maps", data: []byte("x: " + strings.Repeat(`{`, 1000*1024)), error: "yaml: while increasing flow level at line 1, column 10004: exceeded max depth of 10000", }, { name: "1000kb of deeply nested indents", data: []byte(strings.Repeat(`- `, 1000*1024)), error: "yaml: while increasing indent level at line 1: line 1, column 20001: exceeded max depth of 10000", }, { name: "1000kb of 1000-indent lines", data: []byte(strings.Repeat(strings.Repeat(`- `, 1000)+"\n", 1024/2)), }, {name: "1kb of maps", data: []byte(`a: &a [{a}` + strings.Repeat(`,{a}`, 1*1024/4-1) + `]`)}, {name: "10kb of maps", data: []byte(`a: &a [{a}` + strings.Repeat(`,{a}`, 10*1024/4-1) + `]`)}, {name: "100kb of maps", data: []byte(`a: &a [{a}` + strings.Repeat(`,{a}`, 100*1024/4-1) + `]`)}, {name: "1000kb of maps", data: []byte(`a: &a [{a}` + strings.Repeat(`,{a}`, 1000*1024/4-1) + `]`)}, {name: "1000kb slice nested at max-depth", data: []byte(strings.Repeat(`[`, 10000) + `1` + strings.Repeat(`,1`, 1000*1024/2-20000-1) + strings.Repeat(`]`, 10000))}, {name: "1000kb slice nested in maps at max-depth", data: []byte("{a,b:\n" + strings.Repeat(" {a,b:", 10000-2) + ` [1` + strings.Repeat(",1", 1000*1024/2-6*10000-1) + `]` + strings.Repeat(`}`, 10000-1))}, {name: "1000kb of 10000-nested lines", data: []byte(strings.Repeat(`- `+strings.Repeat(`[`, 10000)+strings.Repeat(`]`, 10000)+"\n", 1000*1024/20000))}, } func BenchmarkLimit(b *testing.B) { for _, tc := range limitTests { tc := tc b.Run(tc.name, func(b *testing.B) { for i := 0; i < b.N; i++ { var v any err := yaml.Unmarshal(tc.data, &v) if tc.error != "" { assert.ErrorMatches(b, tc.error, err) continue } assert.NoError(b, err) } }) } } func TestParserGetEvents(t *testing.T) { datatest.RunTestCases(t, func() ([]map[string]any, error) { return datatest.LoadTestCasesFromFile("testdata/parser_events.yaml", libyaml.LoadAny) }, map[string]datatest.TestHandler{ "parser-events": runParserEventsTest, }) } func runParserEventsTest(t *testing.T, tc map[string]any) { t.Helper() // Extract test data yamlInput := datatest.RequireString(t, tc, "yaml") want := datatest.RequireString(t, tc, "want") // Run test events, err := libyaml.ParserGetEvents([]byte(yamlInput)) if err != nil { t.Fatalf("ParserGetEvents error: %v", err) } // Trim trailing newline from want (YAML literal blocks add one) want = datatest.TrimTrailingNewline(want) assert.Equal(t, want, events) } golang-go.yaml-yaml-v4-4.0.0~rc5/yts/000077500000000000000000000000001521353103300172405ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc5/yts/known-failing-tests000066400000000000000000000173451521353103300231000ustar00rootroot00000000000000TestYAMLSuite/229Q/JSONComparisonTest TestYAMLSuite/27NA/EventComparisonTest TestYAMLSuite/27NA/LoadTest TestYAMLSuite/2AUY/JSONComparisonTest TestYAMLSuite/2JQS/EventComparisonTest TestYAMLSuite/2JQS/LoadTest TestYAMLSuite/2LFX/EventComparisonTest TestYAMLSuite/2LFX/LoadTest TestYAMLSuite/2SXE/EventComparisonTest TestYAMLSuite/2SXE/LoadTest TestYAMLSuite/33X3/JSONComparisonTest TestYAMLSuite/35KP/JSONComparisonTest TestYAMLSuite/3HFZ/LoadTest TestYAMLSuite/3UYS/EventComparisonTest TestYAMLSuite/3UYS/LoadTest TestYAMLSuite/4FJ6/LoadTest TestYAMLSuite/4H7K/LoadTest TestYAMLSuite/4MUZ-00/EventComparisonTest TestYAMLSuite/4MUZ-00/LoadTest TestYAMLSuite/4MUZ-01/EventComparisonTest TestYAMLSuite/4MUZ-01/LoadTest TestYAMLSuite/4MUZ-02/EventComparisonTest TestYAMLSuite/4MUZ-02/LoadTest TestYAMLSuite/4RWC/JSONComparisonTest TestYAMLSuite/565N/JSONComparisonTest TestYAMLSuite/58MP/EventComparisonTest TestYAMLSuite/58MP/LoadTest TestYAMLSuite/5MUD/EventComparisonTest TestYAMLSuite/5MUD/LoadTest TestYAMLSuite/5T43/EventComparisonTest TestYAMLSuite/5T43/LoadTest TestYAMLSuite/5TYM/JSONComparisonTest TestYAMLSuite/652Z/JSONComparisonTest TestYAMLSuite/6BCT/EventComparisonTest TestYAMLSuite/6BCT/LoadTest TestYAMLSuite/6BFJ/EventComparisonTest TestYAMLSuite/6BFJ/LoadTest TestYAMLSuite/6CA3/EventComparisonTest TestYAMLSuite/6CA3/LoadTest TestYAMLSuite/6LVF/EventComparisonTest TestYAMLSuite/6LVF/LoadTest TestYAMLSuite/6M2F/EventComparisonTest TestYAMLSuite/6M2F/LoadTest TestYAMLSuite/6PBE/LoadTest TestYAMLSuite/6SLA/JSONComparisonTest TestYAMLSuite/6WLZ/JSONComparisonTest TestYAMLSuite/6XDY/JSONComparisonTest TestYAMLSuite/6ZKB/EventComparisonTest TestYAMLSuite/6ZKB/JSONComparisonTest TestYAMLSuite/74H7/JSONComparisonTest TestYAMLSuite/7Z25/EventComparisonTest TestYAMLSuite/7Z25/JSONComparisonTest TestYAMLSuite/8G76/JSONComparisonTest TestYAMLSuite/8XYN/EventComparisonTest TestYAMLSuite/8XYN/LoadTest TestYAMLSuite/96NN-00/EventComparisonTest TestYAMLSuite/96NN-00/LoadTest TestYAMLSuite/96NN-01/EventComparisonTest TestYAMLSuite/96NN-01/LoadTest TestYAMLSuite/98YD/JSONComparisonTest TestYAMLSuite/9C9N/EventComparisonTest TestYAMLSuite/9C9N/LoadTest TestYAMLSuite/9DXL/EventComparisonTest TestYAMLSuite/9DXL/JSONComparisonTest TestYAMLSuite/9HCY/EventComparisonTest TestYAMLSuite/9HCY/LoadTest TestYAMLSuite/9JBA/EventComparisonTest TestYAMLSuite/9JBA/LoadTest TestYAMLSuite/9KAX/JSONComparisonTest TestYAMLSuite/9MMW/LoadTest TestYAMLSuite/9MQT-01/JSONComparisonTest TestYAMLSuite/9SA2/EventComparisonTest TestYAMLSuite/9SA2/LoadTest TestYAMLSuite/9U5K/JSONComparisonTest TestYAMLSuite/9WXW/JSONComparisonTest TestYAMLSuite/A2M4/EventComparisonTest TestYAMLSuite/A2M4/LoadTest TestYAMLSuite/AVM7/JSONComparisonTest TestYAMLSuite/AZ63/JSONComparisonTest TestYAMLSuite/BEC7/EventComparisonTest TestYAMLSuite/BEC7/LoadTest TestYAMLSuite/BS4K/LoadTest TestYAMLSuite/C4HZ/EventComparisonTest TestYAMLSuite/C4HZ/JSONComparisonTest TestYAMLSuite/CFD4/EventComparisonTest TestYAMLSuite/CFD4/LoadTest TestYAMLSuite/CN3R/EventComparisonTest TestYAMLSuite/CVW2/EventComparisonTest TestYAMLSuite/CVW2/LoadTest TestYAMLSuite/DBG4/JSONComparisonTest TestYAMLSuite/DFF7/MarshalTest TestYAMLSuite/DHP8/JSONComparisonTest TestYAMLSuite/DK95-00/EventComparisonTest TestYAMLSuite/DK95-00/LoadTest TestYAMLSuite/DK95-01/EventComparisonTest TestYAMLSuite/DK95-01/JSONComparisonTest TestYAMLSuite/DK95-01/LoadTest TestYAMLSuite/DK95-03/EventComparisonTest TestYAMLSuite/DK95-03/LoadTest TestYAMLSuite/DK95-04/EventComparisonTest TestYAMLSuite/DK95-04/LoadTest TestYAMLSuite/DK95-05/JSONComparisonTest TestYAMLSuite/DK95-06/JSONComparisonTest TestYAMLSuite/DK95-07/EventComparisonTest TestYAMLSuite/DK95-07/LoadTest TestYAMLSuite/EB22/LoadTest TestYAMLSuite/EHF6/EventComparisonTest TestYAMLSuite/F2C7/JSONComparisonTest TestYAMLSuite/FRK4/EventComparisonTest TestYAMLSuite/FRK4/LoadTest TestYAMLSuite/G4RS/EventComparisonTest TestYAMLSuite/G5U8/EventComparisonTest TestYAMLSuite/G5U8/LoadTest TestYAMLSuite/H2RW/JSONComparisonTest TestYAMLSuite/HRE5/EventComparisonTest TestYAMLSuite/HRE5/LoadTest TestYAMLSuite/HWV9/JSONComparisonTest TestYAMLSuite/J7PZ/JSONComparisonTest TestYAMLSuite/J7VC/JSONComparisonTest TestYAMLSuite/JEF9-02/EventComparisonTest TestYAMLSuite/JEF9-02/JSONComparisonTest TestYAMLSuite/JEF9-02/MarshalTest TestYAMLSuite/JHB9/JSONComparisonTest TestYAMLSuite/JR7V/EventComparisonTest TestYAMLSuite/JR7V/LoadTest TestYAMLSuite/K3WX/EventComparisonTest TestYAMLSuite/K3WX/LoadTest TestYAMLSuite/K4SU/JSONComparisonTest TestYAMLSuite/KK5P/LoadTest TestYAMLSuite/KMK3/JSONComparisonTest TestYAMLSuite/KS4U/LoadTest TestYAMLSuite/KSS4/JSONComparisonTest TestYAMLSuite/L24T-01/EventComparisonTest TestYAMLSuite/L24T-01/JSONComparisonTest TestYAMLSuite/L383/JSONComparisonTest TestYAMLSuite/L94M/JSONComparisonTest TestYAMLSuite/LX3P/LoadTest TestYAMLSuite/M2N8-00/EventComparisonTest TestYAMLSuite/M2N8-00/LoadTest TestYAMLSuite/M2N8-01/LoadTest TestYAMLSuite/M5DY/LoadTest TestYAMLSuite/M6YH/JSONComparisonTest TestYAMLSuite/M7A3/EventComparisonTest TestYAMLSuite/M7A3/JSONComparisonTest TestYAMLSuite/MUS6-00/EventComparisonTest TestYAMLSuite/MUS6-00/LoadTest TestYAMLSuite/MUS6-05/EventComparisonTest TestYAMLSuite/MUS6-05/LoadTest TestYAMLSuite/MUS6-06/EventComparisonTest TestYAMLSuite/MUS6-06/LoadTest TestYAMLSuite/NHX8/EventComparisonTest TestYAMLSuite/NHX8/LoadTest TestYAMLSuite/NJ66/EventComparisonTest TestYAMLSuite/NJ66/LoadTest TestYAMLSuite/NKF9/EventComparisonTest TestYAMLSuite/NKF9/LoadTest TestYAMLSuite/PUW8/JSONComparisonTest TestYAMLSuite/Q5MG/EventComparisonTest TestYAMLSuite/Q5MG/LoadTest TestYAMLSuite/Q9WF/LoadTest TestYAMLSuite/QB6E/EventComparisonTest TestYAMLSuite/QB6E/LoadTest TestYAMLSuite/QLJ7/LoadTest TestYAMLSuite/QT73/JSONComparisonTest TestYAMLSuite/R4YG/EventComparisonTest TestYAMLSuite/R4YG/LoadTest TestYAMLSuite/RHX7/LoadTest TestYAMLSuite/RLU9/JSONComparisonTest TestYAMLSuite/RR7F/JSONComparisonTest TestYAMLSuite/RTP8/EventComparisonTest TestYAMLSuite/RTP8/LoadTest TestYAMLSuite/RZP5/LoadTest TestYAMLSuite/RZT7/JSONComparisonTest TestYAMLSuite/S3PD/EventComparisonTest TestYAMLSuite/S3PD/LoadTest TestYAMLSuite/S4JQ/JSONComparisonTest TestYAMLSuite/S98Z/EventComparisonTest TestYAMLSuite/S98Z/LoadTest TestYAMLSuite/SBG9/LoadTest TestYAMLSuite/SM9W-01/EventComparisonTest TestYAMLSuite/SM9W-01/LoadTest TestYAMLSuite/SU5Z/EventComparisonTest TestYAMLSuite/SU5Z/LoadTest TestYAMLSuite/SYW4/JSONComparisonTest TestYAMLSuite/U9NS/JSONComparisonTest TestYAMLSuite/UGM3/JSONComparisonTest TestYAMLSuite/UGM3/MarshalTest TestYAMLSuite/UKK6-00/EventComparisonTest TestYAMLSuite/UKK6-00/LoadTest TestYAMLSuite/UT92/EventComparisonTest TestYAMLSuite/UT92/LoadTest TestYAMLSuite/V9D5/LoadTest TestYAMLSuite/VJP3-01/EventComparisonTest TestYAMLSuite/VJP3-01/LoadTest TestYAMLSuite/W4TN/EventComparisonTest TestYAMLSuite/W4TN/LoadTest TestYAMLSuite/W5VH/EventComparisonTest TestYAMLSuite/W5VH/LoadTest TestYAMLSuite/WZ62/EventComparisonTest TestYAMLSuite/WZ62/LoadTest TestYAMLSuite/X38W/EventComparisonTest TestYAMLSuite/X38W/LoadTest TestYAMLSuite/X4QW/EventComparisonTest TestYAMLSuite/X4QW/LoadTest TestYAMLSuite/XW4D/LoadTest TestYAMLSuite/Y2GN/EventComparisonTest TestYAMLSuite/Y2GN/JSONComparisonTest TestYAMLSuite/Y79Y-001/EventComparisonTest TestYAMLSuite/Y79Y-001/LoadTest TestYAMLSuite/Y79Y-003/EventComparisonTest TestYAMLSuite/Y79Y-003/LoadTest TestYAMLSuite/Y79Y-003/MarshalTest TestYAMLSuite/Y79Y-004/MarshalTest TestYAMLSuite/Y79Y-005/MarshalTest TestYAMLSuite/Y79Y-006/MarshalTest TestYAMLSuite/Y79Y-007/MarshalTest TestYAMLSuite/Y79Y-008/MarshalTest TestYAMLSuite/Y79Y-009/MarshalTest TestYAMLSuite/Y79Y-010/EventComparisonTest TestYAMLSuite/Y79Y-010/LoadTest TestYAMLSuite/YD5X/JSONComparisonTest TestYAMLSuite/YJV2/EventComparisonTest TestYAMLSuite/YJV2/LoadTest TestYAMLSuite/ZF4X/JSONComparisonTest TestYAMLSuite/ZWK4/JSONComparisonTest golang-go.yaml-yaml-v4-4.0.0~rc5/yts/test_suite_test.go000066400000000000000000000175661521353103300230350ustar00rootroot00000000000000// Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // YAML Test Suite compliance tests. // Runs official YAML test suite cases and tracks known failures. package yts import ( "bytes" "encoding/json" "io" "os" "path/filepath" "reflect" "strings" "testing" "go.yaml.in/yaml/v4" "go.yaml.in/yaml/v4/internal/libyaml" ) var knownFailingTests = loadKnownFailingTests() func loadKnownFailingTests() map[string]bool { fileContent, err := os.ReadFile("known-failing-tests") if err != nil { return make(map[string]bool) } lines := strings.Split(string(fileContent), "\n") knownTests := make(map[string]bool) for _, line := range lines { trimmedLine := strings.TrimSpace(line) if trimmedLine != "" { knownTests[trimmedLine] = true } } return knownTests } func shouldSkipTest(t *testing.T) { t.Helper() if os.Getenv("RUNALL") == "1" { return } name := t.Name() runFailing := os.Getenv("RUNFAILING") == "1" isKnownFailing := knownFailingTests[name] t.Logf("NAME::: %v, %v, %v", name, runFailing, isKnownFailing) switch { case runFailing && !isKnownFailing: t.Skipf("Skipping non-failing test: %s", name) case !runFailing && isKnownFailing: t.Skipf("Skipping known failing test: %s", name) } } func TestYAMLSuite(t *testing.T) { testDir := "./testdata/data-2022-01-17" if _, err := os.Stat(testDir + "/229Q"); os.IsNotExist(err) { t.Fatalf(`YTS tests require data files to be present at '%s'. Run 'make get-test-data' to download them first, or just run the tests with 'make test'.`, testDir) } runTestsInDir(t, testDir, "") } func runTestsInDir(t *testing.T, rootDir string, relPath string) { t.Helper() fullPath := filepath.Join(rootDir, relPath) entries, err := os.ReadDir(fullPath) if err != nil { t.Fatalf("Failed to read directory %s: %v", fullPath, err) } for _, entry := range entries { entryRelPath := filepath.Join(relPath, entry.Name()) entryFullPath := filepath.Join(rootDir, entryRelPath) if entry.IsDir() { // Check if it's a test case directory (contains in.yaml) if _, err := os.Stat(filepath.Join(entryFullPath, "in.yaml")); err == nil { t.Run(strings.ReplaceAll(entryRelPath, string(filepath.Separator), "-"), func(t *testing.T) { runTest(t, entryFullPath) }) } else { // Otherwise, recurse into the subdirectory runTestsInDir(t, rootDir, entryRelPath) } } } } func normalizeLineEndings(s string) string { return strings.NewReplacer( "\r", "", ).Replace(s) } func getEvents(in []byte) (string, error) { return libyaml.ParserGetEvents(in) } // loadFirstDocument loads only the first document from YAML input. // Returns nil value and nil error for empty input (0 documents). // Ignores additional documents if present. func loadFirstDocument(in []byte, out any) error { loader, err := yaml.NewLoader(bytes.NewReader(in)) if err != nil { return err } err = loader.Load(out) if err == io.EOF { return nil // Empty input is OK for YTS } return err } func runTest(t *testing.T, testPath string) { t.Helper() // Read test description descPath := filepath.Join(testPath, "===") desc, err := os.ReadFile(descPath) var testDescription string if err == nil { testDescription = string(desc) } else { testDescription = "No description available." } t.Logf("Running test: %s\nDescription: %s", testPath, testDescription) inYAMLPath := filepath.Join(testPath, "in.yaml") inYAML, err := os.ReadFile(inYAMLPath) if err != nil { t.Fatalf("Test: %s\nDescription: %s\nError: Failed to read in.yaml: %v", testPath, testDescription, err) } errorPath := filepath.Join(testPath, "error") _, err = os.Stat(errorPath) expectError := err == nil var unmarshaledValue any var unmarshalErr error t.Run("LoadTest", func(t *testing.T) { shouldSkipTest(t) unmarshalErr = loadFirstDocument(inYAML, &unmarshaledValue) if expectError { if unmarshalErr == nil { t.Errorf("Test: %s\nDescription: %s\nError: Expected unmarshal error but got none", testPath, testDescription) } return } if unmarshalErr != nil { t.Errorf("Test: %s\nDescription: %s\nError: Unexpected unmarshal error: %v", testPath, testDescription, unmarshalErr) } }) t.Run("EventComparisonTest", func(t *testing.T) { shouldSkipTest(t) expectedEventsPath := filepath.Join(testPath, "test.event") if _, err := os.Stat(expectedEventsPath); err != nil { return } expectedEventsBytes, err := os.ReadFile(expectedEventsPath) if err != nil { t.Errorf("Test: %s\nDescription: %s\nError: Failed to read test.event: %v", testPath, testDescription, err) return } expectedEvents := normalizeLineEndings(string(expectedEventsBytes)) expectedEvents = strings.TrimSuffix(expectedEvents, "\n") actualEvents, eventErr := getEvents(inYAML) if expectError { if eventErr == nil { t.Errorf("Test: %s\nDescription: %s\nError: Expected error on event parsing but got none", testPath, testDescription) } return } if eventErr != nil { t.Errorf("Test: %s\nDescription: %s\nError: Unexpected error on event parsing: %v", testPath, testDescription, eventErr) return } actualEventsStr := normalizeLineEndings(actualEvents) if actualEventsStr != expectedEvents { t.Errorf("Test: %s\nDescription: %s\nError: Event mismatch\nExpected:\n%q\nGot:\n%q", testPath, testDescription, expectedEvents, actualEventsStr) } }) // Only proceed with marshal and JSON tests if unmarshal was successful and no expected error t.Run("MarshalTest", func(t *testing.T) { shouldSkipTest(t) var currentUnmarshaledValue any currentUnmarshalErr := loadFirstDocument(inYAML, ¤tUnmarshaledValue) if !(currentUnmarshalErr == nil || expectError) { return } marshaledYAML, marshalErr := yaml.Dump(currentUnmarshaledValue) if marshalErr != nil { t.Errorf("Test: %s\nDescription: %s\nError: Failed to marshal value: %v", testPath, testDescription, marshalErr) return } outYAMLPath := filepath.Join(testPath, "out.yaml") if _, err := os.Stat(outYAMLPath); err != nil { return } expectedOutYAML, err := os.ReadFile(outYAMLPath) if err != nil { t.Errorf("Test: %s\nDescription: %s\nError: Failed to read out.yaml: %v", testPath, testDescription, err) return } var expectedUnmarshaledValue any err = loadFirstDocument(expectedOutYAML, &expectedUnmarshaledValue) if err != nil { t.Errorf("Test: %s\nDescription: %s\nError: Failed to unmarshal out.yaml: %v", testPath, testDescription, err) return } var reUnmarshaledValue any err = loadFirstDocument(marshaledYAML, &reUnmarshaledValue) if err != nil { t.Errorf("Test: %s\nDescription: %s\nError: Failed to re-unmarshal marshaled YAML: %v", testPath, testDescription, err) } else if !reflect.DeepEqual(reUnmarshaledValue, expectedUnmarshaledValue) { t.Errorf("Test: %s\nDescription: %s\nError: Marshal output mismatch\nExpected: %+v\nGot : %+v", testPath, testDescription, expectedUnmarshaledValue, reUnmarshaledValue) } }) t.Run("JSONComparisonTest", func(t *testing.T) { shouldSkipTest(t) var currentUnmarshaledValue any currentUnmarshalErr := loadFirstDocument(inYAML, ¤tUnmarshaledValue) if !(currentUnmarshalErr == nil || expectError) { return } inJSONPath := filepath.Join(testPath, "in.json") if _, err := os.Stat(inJSONPath); err != nil { return } inJSON, err := os.ReadFile(inJSONPath) if err != nil { t.Errorf("Test: %s\nDescription: %s\nError: Failed to read in.json: %v", testPath, testDescription, err) return } var jsonValue any jsonErr := json.Unmarshal(inJSON, &jsonValue) if jsonErr != nil { t.Errorf("Test: %s\nDescription: %s\nError: Failed to unmarshal in.json: %v", testPath, testDescription, jsonErr) } else if !reflect.DeepEqual(currentUnmarshaledValue, jsonValue) { t.Errorf("Test: %s\nDescription: %s\nError: YAML unmarshal vs JSON unmarshal mismatch\nExpected: %+v\nGot : %+v", testPath, testDescription, jsonValue, currentUnmarshaledValue) } }) }