pax_global_header00006660000000000000000000000064151650133250014513gustar00rootroot0000000000000052 comment=2bebec13c9cdc510762e48128852bf268d134c79 golang-go.yaml-yaml-v4-4.0.0~rc4/000077500000000000000000000000001516501332500164255ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc4/.github/000077500000000000000000000000001516501332500177655ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc4/.github/dependabot.yaml000066400000000000000000000005341516501332500227600ustar00rootroot00000000000000# Copyright 2025 The go-yaml Project Contributors # SPDX-License-Identifier: Apache-2.0 version: 2 updates: - package-ecosystem: gomod directory: / # Location of package manifests schedule: interval: weekly - package-ecosystem: github-actions directory: / groups: actions: patterns: ['*'] schedule: interval: weekly golang-go.yaml-yaml-v4-4.0.0~rc4/.github/workflows/000077500000000000000000000000001516501332500220225ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc4/.github/workflows/check-commit-message.yaml000066400000000000000000000014251516501332500266750ustar00rootroot00000000000000# 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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 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~rc4/.github/workflows/codeql.yaml000066400000000000000000000024761516501332500241660ustar00rootroot00000000000000# 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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: persist-credentials: false # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL # yamllint disable-line rule:line-length uses: github/codeql-action/init@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} - name: Perform CodeQL Analysis # yamllint disable-line rule:line-length uses: github/codeql-action/analyze@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10 with: category: /language:${{matrix.language}} golang-go.yaml-yaml-v4-4.0.0~rc4/.github/workflows/files.yaml000066400000000000000000000030721516501332500240120ustar00rootroot00000000000000# 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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: persist-credentials: false - name: actionlint # yamllint disable-line rule:line-length uses: raven-actions/actionlint@963d4779ef039e217e5d0e6fd73ce9ab7764e493 # v2.1.0 yamllint: runs-on: ubuntu-latest steps: - name: checkout-action uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: persist-credentials: false - name: typos-action uses: crate-ci/typos@bb4666ad77b539a6b4ce4eda7ebb6de553704021 # v1.42.0 golang-go.yaml-yaml-v4-4.0.0~rc4/.github/workflows/forbid-merge-commits.yaml000066400000000000000000000010201516501332500267120ustar00rootroot00000000000000# 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@1b1f54566e417ffc7694b722f894560a179bdd12 # v1.0.6 with: fail-on-merge-commits: true golang-go.yaml-yaml-v4-4.0.0~rc4/.github/workflows/go.yaml000066400000000000000000000047321516501332500233210ustar00rootroot00000000000000# 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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Get latest patch for each minor version # yamllint disable-line rule:line-length uses: arnested/go-version-action@2065c152882e3e76ea0c02c02565486af0591000 # v2.1.0 id: latest-patch-versions with: latest-patches-only: true patch-level: true golang-go.yaml-yaml-v4-4.0.0~rc4/.gitignore000066400000000000000000000002361516501332500204160ustar00rootroot00000000000000# Copyright 2025 The go-yaml Project Contributors # SPDX-License-Identifier: Apache-2.0 /.cache/ /.claude/ /CLAUDE.md /yts/testdata/ /go-yaml /note/ /*.yaml golang-go.yaml-yaml-v4-4.0.0~rc4/.golangci.yaml000066400000000000000000000017111516501332500211520ustar00rootroot00000000000000# Copyright 2025 The go-yaml Project Contributors # SPDX-License-Identifier: Apache-2.0 version: '2' linters: enable: - dupword - govet - mirror - misspell - nolintlint - staticcheck - modernize - nilnesserr - thelper - unconvert disable: - errcheck - ineffassign - unused settings: dupword: ignore: - 'NULL' - DOCUMENT-START - BLOCK-END misspell: locale: US nolintlint: allow-unused: false require-specific: true require-explanation: true govet: enable-all: true disable: - fieldalignment - shadow 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~rc4/.ls-lint.yaml000066400000000000000000000010171516501332500207500ustar00rootroot00000000000000# 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~rc4/.typos.toml000066400000000000000000000017301516501332500205570ustar00rootroot00000000000000# 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 golang-go.yaml-yaml-v4-4.0.0~rc4/.yamllint.yaml000066400000000000000000000012261516501332500212210ustar00rootroot00000000000000# 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~rc4/CONTRIBUTING.md000066400000000000000000000141621516501332500206620ustar00rootroot00000000000000Contributing 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~rc4/GNUmakefile000066400000000000000000000106021516501332500204760ustar00rootroot00000000000000# 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~rc4/LICENSE000066400000000000000000000261451516501332500174420ustar00rootroot00000000000000 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~rc4/NOTICE000066400000000000000000000012761516501332500173370ustar00rootroot00000000000000The 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~rc4/README.md000066400000000000000000000140761516501332500177140ustar00rootroot00000000000000go.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~rc4/cmd/000077500000000000000000000000001516501332500171705ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc4/cmd/go-yaml/000077500000000000000000000000001516501332500205355ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc4/cmd/go-yaml/README.md000066400000000000000000000027731516501332500220250ustar00rootroot00000000000000# 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~rc4/cmd/go-yaml/cli_test.go000066400000000000000000000234041516501332500226750ustar00rootroot00000000000000package 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~rc4/cmd/go-yaml/event.go000066400000000000000000000334551516501332500222170ustar00rootroot00000000000000// 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 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 + 1, // libyaml uses 0-based lines StartColumn: ev.StartMark.Column, EndLine: ev.EndMark.Line + 1, 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~rc4/cmd/go-yaml/json.go000066400000000000000000000056151516501332500220440ustar00rootroot00000000000000// 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~rc4/cmd/go-yaml/main.go000066400000000000000000000547651516501332500220310ustar00rootroot00000000000000// 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" ) const version = "4.0.0.1" // stringSlice is a custom flag type for collecting multiple -o flags type stringSlice []string func (s *stringSlice) String() string { if s == nil { return "" } return fmt.Sprint(*s) } 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.V2}, nil }}, "v3": {typ: "preset", handler: func(string) ([]yaml.Option, error) { return []yaml.Option{yaml.V3}, nil }}, "v4": {typ: "preset", handler: func(string) ([]yaml.Option, error) { return []yaml.Option{yaml.V4}, 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.V4) // 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~rc4/cmd/go-yaml/node.go000066400000000000000000000263411516501332500220170ustar00rootroot00000000000000// 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" { if n.Encoding != 0 { info.Encoding = formatEncoding(n.Encoding) } if n.Version != nil { info.Version = formatVersion(n.Version) } if len(n.TagDirectives) > 0 { info.TagDirectives = make([]TagDirectiveInfo, len(n.TagDirectives)) for i, td := range n.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{} // Add encoding if present if n.Encoding != 0 { content = append(content, MapItem{Key: "encoding", Value: formatEncoding(n.Encoding)}) } // Add version if present if n.Version != nil { content = append(content, MapItem{Key: "version", Value: formatVersion(n.Version)}) } // Add tag directives if present if len(n.TagDirectives) > 0 { var directives []TagDirectiveInfo for _, td := range n.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~rc4/cmd/go-yaml/parser.go000066400000000000000000000116341516501332500223650ustar00rootroot00000000000000// 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 + 1, StartColumn: yamlToken.StartMark.Column, EndLine: yamlToken.EndMark.Line + 1, 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 + 1, StartColumn: comment.StartMark.Column + 1, EndLine: comment.EndMark.Line + 1, EndColumn: comment.EndMark.Column + 1, } 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~rc4/cmd/go-yaml/testdata/000077500000000000000000000000001516501332500223465ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc4/cmd/go-yaml/testdata/basic.yaml000066400000000000000000000043211516501332500243130ustar00rootroot00000000000000# 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~rc4/cmd/go-yaml/testdata/cmd/000077500000000000000000000000001516501332500231115ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc4/cmd/go-yaml/testdata/cmd/stream.yaml000066400000000000000000000024641516501332500252760ustar00rootroot00000000000000# 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~rc4/cmd/go-yaml/testdata/comments.yaml000066400000000000000000000040431516501332500250600ustar00rootroot00000000000000# 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:0'} - {token: BLOCK-MAPPING-START, pos: '1:0'} - {token: KEY, pos: '1:0'} - {token: SCALAR, value: a, pos: '1:0-1'} - {token: VALUE, pos: '1:1-2'} - {token: COMMENT, line: '#c', pos: '1:6-8'} - {token: SCALAR, value: b, pos: '1:3-4'} - {token: BLOCK-END, pos: '2:0'} - {token: STREAM-END, pos: '2:0'} 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~rc4/cmd/go-yaml/testdata/document-implicit.yaml000066400000000000000000000176251516501332500266730ustar00rootroot00000000000000# 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:0'} - {event: MAPPING-START, pos: '1:0'} - {event: SCALAR, value: key, style: Plain, pos: '1:0-3'} - {event: SCALAR, value: value, style: Plain, pos: '1:5-10'} - {event: MAPPING-END, pos: '2:0'} - {event: DOCUMENT-END, implicit: true, pos: '2:0'} - 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:0-3'} - {event: MAPPING-START, pos: '2:0'} - {event: SCALAR, value: key, style: Plain, pos: '2:0-3'} - {event: SCALAR, value: value, style: Plain, pos: '2:5-10'} - {event: MAPPING-END, pos: '3:0'} - {event: DOCUMENT-END, implicit: true, pos: '3:0'} - 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:0-5'} - {event: SCALAR, value: first, style: Plain, pos: '1:0-5'} - {event: DOCUMENT-END, implicit: true, pos: '2:0'} - {event: DOCUMENT-START, pos: '2:0-3'} - {event: SCALAR, value: second, style: Plain, pos: '3:0-6'} - {event: DOCUMENT-END, implicit: true, pos: '4:0'} - 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:0-3'} - {event: SCALAR, value: first, style: Plain, pos: '2:0-5'} - {event: DOCUMENT-END, implicit: true, pos: '3:0'} - {event: DOCUMENT-START, pos: '3:0-3'} - {event: SCALAR, value: second, style: Plain, pos: '4:0-6'} - {event: DOCUMENT-END, implicit: true, pos: '5:0'} - 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:0-3'} - {event: MAPPING-START, pos: '2:0'} - {event: SCALAR, value: key, style: Plain, pos: '2:0-3'} - {event: SCALAR, value: value, style: Plain, pos: '2:5-10'} - {event: MAPPING-END, pos: '3:0'} - {event: DOCUMENT-END, pos: '3:0-3'} - 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:0-5'} - {event: SCALAR, value: first, style: Plain, pos: '1:0-5'} - {event: DOCUMENT-END, pos: '2:0-3'} - {event: DOCUMENT-START, pos: '3:0-3'} - {event: SCALAR, value: second, style: Plain, pos: '4:0-6'} - {event: DOCUMENT-END, implicit: true, pos: '5:0'} - 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:0-5'} - {event: SCALAR, value: first, style: Plain, pos: '1:0-5'} - {event: DOCUMENT-END, implicit: true, pos: '2:0'} - {event: DOCUMENT-START, pos: '2:0-3'} - {event: SCALAR, value: second, style: Plain, pos: '3:0-6'} - {event: DOCUMENT-END, implicit: true, pos: '4:0'} - {event: DOCUMENT-START, pos: '4:0-3'} - {event: SCALAR, value: third, style: Plain, pos: '5:0-5'} - {event: DOCUMENT-END, implicit: true, pos: '6:0'} - 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:0-3'} - {event: MAPPING-START, pos: '2:0'} - {event: SCALAR, value: doc1, style: Plain, pos: '2:0-4'} - {event: SCALAR, value: value1, style: Plain, pos: '2:6-12'} - {event: SCALAR, value: doc2, style: Plain, pos: '3:0-4'} - {event: SCALAR, value: value2, style: Plain, pos: '3:6-12'} - {event: MAPPING-END, pos: '4:0'} - {event: DOCUMENT-END, pos: '4:0-3'} - 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:0-3'} - {event: SCALAR, style: Plain, pos: '2:0'} - {event: DOCUMENT-END, implicit: true, pos: '2:0'} - {event: DOCUMENT-START, pos: '2:0-3'} - {event: SCALAR, style: Plain, pos: '3:0'} - {event: DOCUMENT-END, implicit: true, pos: '3:0'} - 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:0'} - {event: SEQUENCE-START, pos: '1:0'} - {event: SCALAR, value: item1, style: Plain, pos: '1:2-7'} - {event: SCALAR, value: item2, style: Plain, pos: '2:2-7'} - {event: SEQUENCE-END, pos: '3:0'} - {event: DOCUMENT-END, implicit: true, pos: '3:0'} - 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:0-3'} - {event: SEQUENCE-START, pos: '2:0'} - {event: SCALAR, value: item1, style: Plain, pos: '2:2-7'} - {event: SCALAR, value: item2, style: Plain, pos: '3:2-7'} - {event: SEQUENCE-END, pos: '4:0'} - {event: DOCUMENT-END, implicit: true, pos: '4:0'} - 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:0-3'} - {event: SCALAR, value: first, style: Plain, pos: '2:0-5'} - {event: DOCUMENT-END, pos: '3:0-3'} - {event: DOCUMENT-START, pos: '4:0-3'} - {event: SCALAR, value: second, style: Plain, pos: '5:0-6'} - {event: DOCUMENT-END, pos: '6:0-3'} golang-go.yaml-yaml-v4-4.0.0~rc4/cmd/go-yaml/testdata/key-ordering.yaml000066400000000000000000000113521516501332500256330ustar00rootroot00000000000000# 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~rc4/cmd/go-yaml/testdata/multi-document.yaml000066400000000000000000000263221516501332500262050ustar00rootroot00000000000000# 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~rc4/cmd/go-yaml/testdata/node-profuse.yaml000066400000000000000000000022671516501332500256470ustar00rootroot00000000000000# 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~rc4/cmd/go-yaml/testdata/options.yaml000066400000000000000000000066201516501332500247310ustar00rootroot00000000000000- 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~rc4/cmd/go-yaml/testdata/output-modes.yaml000066400000000000000000000073531516501332500257070ustar00rootroot00000000000000# 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~rc4/cmd/go-yaml/testdata/positions.yaml000066400000000000000000000027641516501332500252720ustar00rootroot00000000000000# Position format tests for go-yaml CLI - name: Single character position text: | a TOKEN: | - {token: STREAM-START, pos: '1:0'} - {token: SCALAR, value: a, pos: '1:0-1'} - {token: STREAM-END, pos: '2:0'} - name: | Same line range text: | key: value TOKEN: | - {token: STREAM-START, pos: '1:0'} - {token: BLOCK-MAPPING-START, pos: '1:0'} - {token: KEY, pos: '1:0'} - {token: SCALAR, value: key, pos: '1:0-3'} - {token: VALUE, pos: '1:3-4'} - {token: SCALAR, value: value, pos: '1:5-10'} - {token: BLOCK-END, pos: '2:0'} - {token: STREAM-END, pos: '2:0'} - name: | Multi-line literal scalar text: | key: | line1 line2 TOKEN: | - {token: STREAM-START, pos: '1:0'} - {token: BLOCK-MAPPING-START, pos: '1:0'} - {token: KEY, pos: '1:0'} - {token: SCALAR, value: key, pos: '1:0-3'} - {token: VALUE, pos: '1:3-4'} - {token: SCALAR, value: "line1\nline2\n", style: Literal, pos: '1:5-4:0'} - {token: BLOCK-END, pos: '4:0'} - {token: STREAM-END, pos: '4:0'} - name: | Comment position with line comment text: | a: b #c TOKEN: | - {token: STREAM-START, pos: '1:0'} - {token: BLOCK-MAPPING-START, pos: '1:0'} - {token: KEY, pos: '1:0'} - {token: SCALAR, value: a, pos: '1:0-1'} - {token: VALUE, pos: '1:1-2'} - {token: COMMENT, line: '#c', pos: '1:6-8'} - {token: SCALAR, value: b, pos: '1:3-4'} - {token: BLOCK-END, pos: '2:0'} - {token: STREAM-END, pos: '2:0'} golang-go.yaml-yaml-v4-4.0.0~rc4/cmd/go-yaml/token.go000066400000000000000000000433531516501332500222140ustar00rootroot00000000000000// 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~rc4/cmd/go-yaml/yaml.go000066400000000000000000000224141516501332500220310ustar00rootroot00000000000000// 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~rc4/doc.go000066400000000000000000000067611516501332500175330ustar00rootroot00000000000000// // Copyright (c) 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.V3) // // Options can be combined and later options override earlier ones: // // // Start with v3 defaults, then override indent // yaml.NewDumper(w, // yaml.V3, // yaml.WithIndent(4), // ) // // 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~rc4/docs/000077500000000000000000000000001516501332500173555ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc4/docs/README.md000066400000000000000000000021101516501332500206260ustar00rootroot00000000000000# 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~rc4/docs/dev/000077500000000000000000000000001516501332500201335ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc4/docs/dev/README.md000066400000000000000000000025011516501332500214100ustar00rootroot00000000000000# 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 - **[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 - **[resolver-problem.mmd](resolver-problem.mmd)** - Diagram illustrating the architectural issue of `resolve()` being called from 4 different places ## 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~rc4/docs/dev/call-hierarchy.mmd000066400000000000000000000023261516501332500235240ustar00rootroot00000000000000--- title: go-yaml Call Hierarchy --- flowchart TB subgraph LOAD ["LOAD: Pull-Based"] direction TB L_entry["Entry Point
Decoder.Decode / Node.Decode"] L_constructor["Constructor
constructor.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_constructor L_constructor -->|calls| L_composer 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 Point
Encoder.Encode / Node.Encode"] D_representer["Representer.MarshalDoc
representer.go"] D_serializer["Serializer.node
serializer.go"] D_emitter["Emitter.Emit
emitter.go"] D_writer["Writer.flush
writer.go"] D_entry -->|calls| D_representer D_representer -->|for Go values| D_emitter D_representer -->|for Node| D_serializer D_serializer -->|calls emit| D_emitter D_emitter -->|calls| D_writer end golang-go.yaml-yaml-v4-4.0.0~rc4/docs/dev/comment-flow.mmd000066400000000000000000000015761516501332500232520ustar00rootroot00000000000000--- 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~rc4/docs/dev/dump-load-api.md000066400000000000000000000331341516501332500231120ustar00rootroot00000000000000# 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.V3)` (matches Marshal: 4-space indent, non-compact) - **v2 semantics**: `yaml.Dump(&data, yaml.V2)` (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.V3) // 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.V3) 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.V3) 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.V3) 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.V2) // v3: 4-space indent, non-compact sequences yaml.Dump(&config, yaml.V3) // v4: 2-space indent, compact sequences (default) yaml.Dump(&config, yaml.V4) // 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.V3, 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~rc4/docs/dev/go-yaml-internals.md000066400000000000000000001242471516501332500240310ustar00rootroot00000000000000go-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 ↑ ↓ (Repr) ↓ ↑ ↓ Resolver ↓ ↑ ↓ (Nodes) ←→ (Nodes) ↑ ↓ Composer Serializer ↑ ↓ (Events) ←→ (Events) ↑ ↓ Parser ↓ ↑ ↓ (Tokens) ↓ ↑ ↓ Scanner Emitter ↑ ↓ (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 `Unmarshal()`, `Decoder.Decode()`, or `Node.Decode()`) orchestrate the process by creating and coordinating the stages: - **Entry points** create a **Composer** (which owns a **Parser**) - **Entry points** call **Composer**.Parse() to get Node trees - **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 - **Entry points** create a **Constructor** and call Construct() to convert Nodes to Go values - **Resolver** is called by Composer (for tag inference) and Constructor (for value conversion) ### 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` * `resolver.go / resolve - Infers tag for untagged scalars` Transforms: * **Event sequence → tree structure** * **Tag short-form normalization** (`tag:yaml.org,2002:str` → `!!str`) * **DEFAULT TYPE INFERENCE for untagged scalars** via `resolve("", value)` ⚠️ * **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 * In the current implementation, Composer calls `resolve()` for untagged scalars - this may be the wrong place (see Problems section) * 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. Info: - File: internal/libyaml/resolver.go (170 lines) - Main Function: `func resolve(tag string, in string) (rtag string, out any)` - Input: Tag string + scalar value - Output: Resolved tag + typed Go value - Called From: * Composer ([`composer.go`](../../internal/libyaml/composer.go) / `scalar()`) * Constructor ([`constructor.go`](../../internal/libyaml/constructor.go) / `scalar()`) * Representer ([`representer.go`](../../internal/libyaml/representer.go) / `stringv()`) * Serializer ([`serializer.go`](../../internal/libyaml/serializer.go) / `node()`) - Important Processes: * `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`) Notes: * In go-yaml, this is not a separate stage but a function called from multiple places * Called from Composer (to set Node.Tag) and Constructor (to get typed value) ⚠️ * The YAML spec treats Resolver as a distinct stage producing the "Representation Graph" ### 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` * `resolver.go / resolve - Re-resolves tag to get typed value` 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 * **Re-resolution of tag/value** via `resolve(n.Tag, n.Value)` ⚠️ * **`indicatedString()` check** for quoted scalars (quoted/literal scalars skip `resolve()`) * **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: * `resolve()` is called again here - duplicates work done in Composer * 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) skip tag resolution via `indicatedString()` check * 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 (or Node trees) into YAML text. **Control Flow:** The dump stack uses a push-based call hierarchy. Entry points (such as `Marshal()`, `Encoder.Encode()`, or `Node.Encode()`) orchestrate the process by creating and coordinating the stages: - **Entry points** create a **Representer** (which owns an **Emitter**) - **Entry points** call **Representer**.MarshalDoc() or similar methods - **Representer** walks Go values and calls emit() to push Events to owned **Emitter** - If input is a `*Node`, **Representer** delegates to **Serializer** (part of Representer) - **Serializer** walks the Node tree and pushes Events to **Emitter** via emit() - **Emitter** accumulates Events, formats output, and calls **Writer** to flush bytes - **Resolver** is called by Representer and Serializer (to check if quoting/tags needed) ### Representer Converts Go values directly to events (bypasses Node tree). Info: - File: internal/libyaml/representer.go (564 lines) - Main Function: `func (r *Representer) MarshalDoc(tag string, in reflect.Value)` - Input: `reflect.Value` + `Options` - Output: Events pushed to owned Emitter - Called From: * Entry point [`yaml.go / Marshal()`](../../yaml.go) * Entry point [`yaml.go / Encoder.Encode()`](../../yaml.go) * Entry point [`node.go / Node.Encode()`](../../internal/libyaml/node.go) - Important Processes: * `representer.go / marshal - Dispatches by Go type` * `representer.go / emit - Sends event to owned Emitter` * `serializer.go / node - Delegates Node to Serializer (same file scope)` * `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` * `emitter.go / Emitter.Emit - Emits events (owned Emitter)` * `resolver.go / resolve - Checks if quoting needed` 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 * **Resolve-check for quoting decisions** via `resolve()` ⚠️ * **YAML 1.1 compatibility checks** (`isBase60Float()`, `isOldBool()`) * **Style selection** (literal for multiline strings) * **Binary data base64 encoding** - non-UTF-8 strings automatically tagged `!!binary` and base64 encoded * **Flow style from struct tags** Notes: * Representer calls `resolve()` to determine if quoting is needed - this couples dump to load logic * When input is `*Node`, delegates to Serializer instead * Map key sorting ensures deterministic output by using natural sort with numeric awareness ### Serializer Converts Node tree to events (used when marshaling from Node). Info: - File: internal/libyaml/serializer.go (192 lines) - Main Function: `func (r *Representer) node(node *Node, tail string)` - Input: `*Node` tree - Output: Events pushed to Emitter (via Representer) - Called From: * Representer ([`representer.go / Representer.nodev()`](../../internal/libyaml/representer.go)) * Representer ([`representer.go / marshal()`](../../internal/libyaml/representer.go)) - Important Processes: * `representer.go / emit - Sends event to Emitter (via Representer)` * `representer.go / nilv - Emits null scalar` * `serializer.go / node (recursive) - Walks child nodes` * `serializer.go / isSimpleCollection - Checks if flow style appropriate` * `emitter.go / Emitter.Emit - Emits events (via Representer)` * `resolver.go / resolve - Checks if tag can be elided` Transforms: * **Node tree → event stream** * **Tag elision check** via `resolve()` ⚠️ * **Force quoting** when tag would be misresolved * **Flow style detection for simple collections** (`WithFlowSimpleCollections` option) - automatically uses flow style for eligible collections * **Comment placement/shifting** (foot → tail) * **Style flag interpretation** * **Invalid UTF-8 → base64** Notes: * `resolve()` is called to check if tag can be elided - fourth place it's called * Simple collection = all scalar children, fits within line width ### 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 See also: - [Resolver Problem Diagram](resolver-problem.mmd) ### 1. resolve() Called in Four Places | Location | Stage | Purpose | |----------|-------|---------| | [`composer.go`](../../internal/libyaml/composer.go) / `scalar()` | Load | Set Node.Tag for untagged scalars | | [`constructor.go`](../../internal/libyaml/constructor.go) / `scalar()` | Load | Get actual Go value | | [`representer.go`](../../internal/libyaml/representer.go) / `stringv()` | Dump | Check if quoting needed | | [`serializer.go`](../../internal/libyaml/serializer.go) / `node()` | Dump | Check if tag can be elided | **Problem:** Same expensive resolution logic runs multiple times. Tags are resolved but values aren't stored. **Better:** Store `(tag, resolvedValue)` at first resolution, or defer ALL resolution to Constructor. ### 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 The representer calls `resolve()` to check if strings would be misresolved - this is parser-era logic living in the dump stack. **Problem:** Changes to resolution affect both stacks. Round-trip safety depends on this coupling. ### 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~rc4/docs/dev/how-go-yaml-works.md000066400000000000000000000251421516501332500237640ustar00rootroot00000000000000# 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 ↑ ↓ (Repr) ↓ ↑ ↓ Resolver ↓ ↑ ↓ (Nodes) ←→ (Nodes) ↑ ↓ Composer Serializer ↑ ↓ (Events) ←→ (Events) ↑ ↓ Parser ↓ ↑ ↓ (Tokens) ↓ ↑ ↓ Scanner Emitter ↑ ↓ (Code Points) ←→ (Code Points) ↑ ↓ Reader Writer ↑ ↓ (Raw Bytes) ←→ (Raw Bytes) ``` **Stack Asymmetry** 1. **Load vs Dump Asymmetry**: Load has more stages than Dump 2. **Scanner+Parser on Load** break tokenization and parsing into separate steps, while **Emitter on Dump** combines these 3. **Resolver on Load** handles tag resolution as a separate stage, while **Representer on Dump** produces nodes directly 4. **Representations align** across the pipelines, showing the paired nature of the transforms ## 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 → Nodes) **File**: `internal/libyaml/representer.go` The Representer converts Go values to YAML representation: - 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"`) - Produces nodes directly (skips the Repr stage) ### 2. 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 ### 3. 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. ### 4. 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 → Serializer → Emitter → Writer - Each stage transforms data from one representation to another - The stages are asymmetric: Load has more steps than Dump - "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~rc4/docs/dev/pipeline-overview.mmd000066400000000000000000000035431516501332500243100ustar00rootroot00000000000000--- 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[/Nodes/] 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 L_nodes --> 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[/Nodes - optional/] 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_events D_nodes --> D_serializer D_serializer --> D_events D_events --> D_emitter D_emitter --> D_codepoints D_codepoints --> D_writer D_writer --> D_bytes end RESOLVER{{Resolver
resolve function
NOT a stage}} L_composer -.->|"calls for
untagged scalars"| RESOLVER L_constructor -.->|"calls for
typed values"| RESOLVER D_representer -.->|"calls to check
quoting needed"| RESOLVER D_serializer -.->|"calls to check
tag elision"| RESOLVER L_native <--> D_native L_nodes <-.-> D_nodes L_events <-.-> D_events L_codepoints <-.-> D_codepoints L_bytes <-.-> D_bytes golang-go.yaml-yaml-v4-4.0.0~rc4/docs/dev/resolver-problem.mmd000066400000000000000000000016541516501332500241370ustar00rootroot00000000000000--- title: resolve Called From 4 Places --- flowchart TB subgraph LOAD ["LOAD STACK calls resolve()"] direction LR L_composer[Composer
composer.go line 182
Sets Node.Tag] --> L_resolve1[resolve - 1st call] L_constructor[Constructor
constructor.go line 702
Gets typed value] --> L_resolve2[resolve - 2nd call
Duplicate work] end subgraph DUMP ["DUMP STACK calls resolve()"] direction LR D_representer[Representer
representer.go line 477
Checks if quoting needed] --> D_resolve3[resolve - 3rd call] D_serializer[Serializer
serializer.go line 34
Checks if tag can be elided] --> D_resolve4[resolve - 4th call] end subgraph PROBLEMS ["PROBLEMS"] direction LR P1[Duplicate resolution on Load] P2[Dump depends on Load logic] P3[No caching of resolved values] end LOAD --> DUMP DUMP --> PROBLEMS golang-go.yaml-yaml-v4-4.0.0~rc4/docs/options.md000066400000000000000000000401701516501332500213740ustar00rootroot00000000000000# 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.V2) ``` **When to use:** Matching go-yaml v2 output format (2-space indent). ### Using v3 Options ```go dumper, _ := yaml.NewDumper(writer, yaml.V3) ``` **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.V4) ``` **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.V4` 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.V3, 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.V3, // 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.V4, 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.V2, ) 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.V3) // 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.V3` gives you 4 spaces (V3 wins), while `yaml.V3, 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~rc4/docs/v3-to-v4-migration.md000066400000000000000000000137761516501332500232030ustar00rootroot00000000000000# 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.V3 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 ### Functional Options v4 introduces a functional options pattern for configuration: ```go // Version presets yaml.Dump(&data, yaml.V2) // Use v2 defaults yaml.Dump(&data, yaml.V3) // Use v3 defaults yaml.Dump(&data, yaml.V4) // 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.V3, 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.V3` preset: ```go // Get v3-style formatting in v4 data, err := yaml.Dump(&config, yaml.V3) ``` 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.V3` preset to maintain v3 formatting 4. Done! ```go // Only change needed for basic migration data, err := yaml.Dump(&config, yaml.V3) ``` ### 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.V3` preset to maintain v3 formatting: ```go yaml.Dump(&data, yaml.V3) ``` ### 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~rc4/dumper.go000066400000000000000000000057451516501332500202630ustar00rootroot00000000000000// 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 yaml import ( "bytes" "errors" "io" "reflect" "go.yaml.in/yaml/v4/internal/libyaml" ) // 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 := libyaml.ApplyOptions(opts...) if err != nil { return nil, err } var buf bytes.Buffer d, err := NewDumper(&buf, func(opts *libyaml.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, &LoadErrors{Errors: []*libyaml.ConstructError{{ Err: errors.New("yaml: 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 } // A Dumper writes YAML values to an output stream with configurable options. type Dumper struct { encoder *libyaml.Representer opts *libyaml.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 := libyaml.ApplyOptions(opts...) if err != nil { return nil, err } return &Dumper{ encoder: libyaml.NewRepresenter(w, o), opts: o, }, 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) d.encoder.MarshalDoc("", reflect.ValueOf(v)) 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.encoder.Finish() return nil } golang-go.yaml-yaml-v4-4.0.0~rc4/dumper_test.go000066400000000000000000000051251516501332500213120ustar00rootroot00000000000000// Tests for the Dump API, including WithAllDocuments functionality. package yaml_test import ( "strings" "testing" "go.yaml.in/yaml/v4" "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 := yaml.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 := yaml.Dump(configs, yaml.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 := yaml.Dump(docs, yaml.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 := yaml.Dump(docs, yaml.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 := yaml.Dump(single, yaml.WithAllDocuments()) assert.NotNil(t, err) assert.ErrorMatches(t, ".*WithAllDocuments requires a slice input.*", err) } golang-go.yaml-yaml-v4-4.0.0~rc4/example/000077500000000000000000000000001516501332500200605ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc4/example/README.md000066400000000000000000000074571516501332500213540ustar00rootroot00000000000000# 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.V4 - 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.V4, ) ``` ## 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~rc4/example/basic_dumper/000077500000000000000000000000001516501332500225155ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc4/example/basic_dumper/main.go000066400000000000000000000014141516501332500237700ustar00rootroot00000000000000// 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~rc4/example/basic_loader/000077500000000000000000000000001516501332500224675ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc4/example/basic_loader/main.go000066400000000000000000000013051516501332500237410ustar00rootroot00000000000000// 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~rc4/example/dumper_indent_comparison/000077500000000000000000000000001516501332500251475ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc4/example/dumper_indent_comparison/main.go000066400000000000000000000053331516501332500264260ustar00rootroot00000000000000// 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~rc4/example/dumper_with_indent/000077500000000000000000000000001516501332500237505ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc4/example/dumper_with_indent/main.go000066400000000000000000000014671516501332500252330ustar00rootroot00000000000000// 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~rc4/example/load_into_node/000077500000000000000000000000001516501332500230355ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc4/example/load_into_node/main.go000066400000000000000000000054131516501332500243130ustar00rootroot00000000000000// 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~rc4/example/loader_dumper_demo/000077500000000000000000000000001516501332500237065ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc4/example/loader_dumper_demo/main.go000066400000000000000000000065271516501332500251730ustar00rootroot00000000000000// 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~rc4/example/multi_document_dumper/000077500000000000000000000000001516501332500244645ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc4/example/multi_document_dumper/main.go000066400000000000000000000016031516501332500257370ustar00rootroot00000000000000// 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~rc4/example/multi_document_loader/000077500000000000000000000000001516501332500244365ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc4/example/multi_document_loader/main.go000066400000000000000000000015401516501332500257110ustar00rootroot00000000000000// 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~rc4/example/multiple_options_loader/000077500000000000000000000000001516501332500250145ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc4/example/multiple_options_loader/main.go000066400000000000000000000033341516501332500262720ustar00rootroot00000000000000// 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~rc4/example/node_dump_with_options/000077500000000000000000000000001516501332500246405ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc4/example/node_dump_with_options/main.go000066400000000000000000000026511516501332500261170ustar00rootroot00000000000000// 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.V3); 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~rc4/example/node_load_decode_comparison/000077500000000000000000000000001516501332500255415ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc4/example/node_load_decode_comparison/main.go000066400000000000000000000032701516501332500270160ustar00rootroot00000000000000// 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~rc4/example/node_load_strict_unmarshaler/000077500000000000000000000000001516501332500257755ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc4/example/node_load_strict_unmarshaler/main.go000066400000000000000000000026271516501332500272570ustar00rootroot00000000000000// 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~rc4/example/node_programmatic_build/000077500000000000000000000000001516501332500247315ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc4/example/node_programmatic_build/main.go000066400000000000000000000021631516501332500262060ustar00rootroot00000000000000// 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~rc4/example/single_document_loader/000077500000000000000000000000001516501332500245655ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc4/example/single_document_loader/main.go000066400000000000000000000021631516501332500260420ustar00rootroot00000000000000// 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~rc4/example/version_options/000077500000000000000000000000001516501332500233205ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc4/example/version_options/main.go000066400000000000000000000027171516501332500246020ustar00rootroot00000000000000// 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.V2 - 2-space indent, non-compact sequences ===") out, _ := yaml.Dump(&cfg, yaml.V2) fmt.Print(string(out)) // v3 options - 4-space indent (default), non-compact sequences fmt.Println("=== yaml.V3 - 4-space indent, non-compact sequences ===") out, _ = yaml.Dump(&cfg, yaml.V3) fmt.Print(string(out)) // v4 options - 2-space indent, compact sequences fmt.Println("=== yaml.V4 - 2-space indent, compact sequences ===") out, _ = yaml.Dump(&cfg, yaml.V4) fmt.Print(string(out)) // Override v4 options fmt.Println("=== yaml.V4 with WithIndent(3) override ===") out, _ = yaml.Dump(&cfg, yaml.V4, 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~rc4/example/with_v4_option/000077500000000000000000000000001516501332500230345ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc4/example/with_v4_option/main.go000066400000000000000000000042461516501332500243150ustar00rootroot00000000000000// 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.V3) 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~rc4/example/with_v4_override/000077500000000000000000000000001516501332500233435ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc4/example/with_v4_override/main.go000066400000000000000000000034731516501332500246250ustar00rootroot00000000000000// 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.V4 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.V4 - 2-space indent:") var buf bytes.Buffer dumper, err := yaml.NewDumper(&buf, yaml.V4) 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. v4 options, then WithIndent(3) - should use 3-space indent:") buf.Reset() dumper2, err := yaml.NewDumper(&buf, yaml.V4, 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 v4 options - should use 2-space indent (v4 wins):") buf.Reset() dumper3, err := yaml.NewDumper(&buf, yaml.WithIndent(5), yaml.V4) 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~rc4/go.mod000066400000000000000000000000431516501332500175300ustar00rootroot00000000000000module go.yaml.in/yaml/v4 go 1.18 golang-go.yaml-yaml-v4-4.0.0~rc4/internal/000077500000000000000000000000001516501332500202415ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc4/internal/libyaml/000077500000000000000000000000001516501332500216725ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc4/internal/libyaml/README.md000066400000000000000000000345151516501332500231610ustar00rootroot00000000000000# 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 - **yamlprivate.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 - **yamlprivate_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 - **yamlprivate.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 - **yamlprivate.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 `yamldatatest_loader.go` and `yamldatatest_test.go`: **Core functions**: - `LoadYAML(data []byte) (interface{}, error)` - Parses YAML using libyaml parser with scalar type resolution (exported) - `UnmarshalStruct(target interface{}, data map[string]interface{}) error` - Populates structs (exported) - `LoadTestCases(filename string) ([]TestCase, error)` - Loads and parses test YAML files - `coerceScalar(value string) interface{}` - Resolves scalar strings to appropriate Go types (int, float64, bool, nil, string) **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 `LoadYAML` 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~rc4/internal/libyaml/api.go000066400000000000000000000505401516501332500227760ustar00rootroot00000000000000// 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 // High-level API helpers for parser and emitter initialization and // configuration. // Provides convenience functions for token insertion and stream management. package libyaml import ( "io" ) 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 } // 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), } } // 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 } // 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, } } // 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 } ///* // * Destroy a token object. // */ // //YAML_DECLARE(void) //yaml_token_delete(yaml_token_t *token) //{ // assert(token); // Non-NULL token object expected. // // switch (token.type) // { // case YAML_TAG_DIRECTIVE_TOKEN: // yaml_free(token.data.tag_directive.handle); // yaml_free(token.data.tag_directive.prefix); // break; // // case YAML_ALIAS_TOKEN: // yaml_free(token.data.alias.value); // break; // // case YAML_ANCHOR_TOKEN: // yaml_free(token.data.anchor.value); // break; // // case YAML_TAG_TOKEN: // yaml_free(token.data.tag.handle); // yaml_free(token.data.tag.suffix); // break; // // case YAML_SCALAR_TOKEN: // yaml_free(token.data.scalar.value); // break; // // default: // break; // } // // memset(token, 0, sizeof(yaml_token_t)); //} // ///* // * Check if a string is a valid UTF-8 sequence. // * // * Check 'reader.c' for more details on UTF-8 encoding. // */ // //static int //yaml_check_utf8(yaml_char_t *start, size_t length) //{ // yaml_char_t *end = start+length; // yaml_char_t *pointer = start; // // while (pointer < end) { // unsigned char octet; // unsigned int width; // unsigned int value; // size_t k; // // octet = pointer[0]; // width = (octet & 0x80) == 0x00 ? 1 : // (octet & 0xE0) == 0xC0 ? 2 : // (octet & 0xF0) == 0xE0 ? 3 : // (octet & 0xF8) == 0xF0 ? 4 : 0; // value = (octet & 0x80) == 0x00 ? octet & 0x7F : // (octet & 0xE0) == 0xC0 ? octet & 0x1F : // (octet & 0xF0) == 0xE0 ? octet & 0x0F : // (octet & 0xF8) == 0xF0 ? octet & 0x07 : 0; // if (!width) return 0; // if (pointer+width > end) return 0; // for (k = 1; k < width; k ++) { // octet = pointer[k]; // if ((octet & 0xC0) != 0x80) return 0; // value = (value << 6) + (octet & 0x3F); // } // if (!((width == 1) || // (width == 2 && value >= 0x80) || // (width == 3 && value >= 0x800) || // (width == 4 && value >= 0x10000))) return 0; // // pointer += width; // } // // return 1; //} // // 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{} } ///* // * Create a document object. // */ // //YAML_DECLARE(int) //yaml_document_initialize(document *yaml_document_t, // version_directive *yaml_version_directive_t, // tag_directives_start *yaml_tag_directive_t, // tag_directives_end *yaml_tag_directive_t, // start_implicit int, end_implicit int) //{ // struct { // error yaml_error_type_t // } context // struct { // start *yaml_node_t // end *yaml_node_t // top *yaml_node_t // } nodes = { NULL, NULL, NULL } // version_directive_copy *yaml_version_directive_t = NULL // struct { // start *yaml_tag_directive_t // end *yaml_tag_directive_t // top *yaml_tag_directive_t // } tag_directives_copy = { NULL, NULL, NULL } // value yaml_tag_directive_t = { NULL, NULL } // mark yaml_mark_t = { 0, 0, 0 } // // assert(document) // Non-NULL document object is expected. // assert((tag_directives_start && tag_directives_end) || // (tag_directives_start == tag_directives_end)) // // Valid tag directives are expected. // // if (!STACK_INIT(&context, nodes, INITIAL_STACK_SIZE)) goto error // // if (version_directive) { // version_directive_copy = yaml_malloc(sizeof(yaml_version_directive_t)) // if (!version_directive_copy) goto error // version_directive_copy.major = version_directive.major // version_directive_copy.minor = version_directive.minor // } // // if (tag_directives_start != tag_directives_end) { // tag_directive *yaml_tag_directive_t // if (!STACK_INIT(&context, tag_directives_copy, INITIAL_STACK_SIZE)) // goto error // for (tag_directive = tag_directives_start // tag_directive != tag_directives_end; tag_directive ++) { // assert(tag_directive.handle) // assert(tag_directive.prefix) // if (!yaml_check_utf8(tag_directive.handle, // strlen((char *)tag_directive.handle))) // goto error // if (!yaml_check_utf8(tag_directive.prefix, // strlen((char *)tag_directive.prefix))) // goto error // value.handle = yaml_strdup(tag_directive.handle) // value.prefix = yaml_strdup(tag_directive.prefix) // if (!value.handle || !value.prefix) goto error // if (!PUSH(&context, tag_directives_copy, value)) // goto error // value.handle = NULL // value.prefix = NULL // } // } // // DOCUMENT_INIT(*document, nodes.start, nodes.end, version_directive_copy, // tag_directives_copy.start, tag_directives_copy.top, // start_implicit, end_implicit, mark, mark) // // return 1 // //error: // STACK_DEL(&context, nodes) // yaml_free(version_directive_copy) // while (!STACK_EMPTY(&context, tag_directives_copy)) { // value yaml_tag_directive_t = POP(&context, tag_directives_copy) // yaml_free(value.handle) // yaml_free(value.prefix) // } // STACK_DEL(&context, tag_directives_copy) // yaml_free(value.handle) // yaml_free(value.prefix) // // return 0 //} // ///* // * Destroy a document object. // */ // //YAML_DECLARE(void) //yaml_document_delete(document *yaml_document_t) //{ // struct { // error yaml_error_type_t // } context // tag_directive *yaml_tag_directive_t // // context.error = YAML_NO_ERROR // Eliminate a compiler warning. // // assert(document) // Non-NULL document object is expected. // // while (!STACK_EMPTY(&context, document.nodes)) { // node yaml_node_t = POP(&context, document.nodes) // yaml_free(node.tag) // switch (node.type) { // case YAML_SCALAR_NODE: // yaml_free(node.data.scalar.value) // break // case YAML_SEQUENCE_NODE: // STACK_DEL(&context, node.data.sequence.items) // break // case YAML_MAPPING_NODE: // STACK_DEL(&context, node.data.mapping.pairs) // break // default: // assert(0) // Should not happen. // } // } // STACK_DEL(&context, document.nodes) // // yaml_free(document.version_directive) // for (tag_directive = document.tag_directives.start // tag_directive != document.tag_directives.end // tag_directive++) { // yaml_free(tag_directive.handle) // yaml_free(tag_directive.prefix) // } // yaml_free(document.tag_directives.start) // // memset(document, 0, sizeof(yaml_document_t)) //} // ///** // * Get a document node. // */ // //YAML_DECLARE(yaml_node_t *) //yaml_document_get_node(document *yaml_document_t, index int) //{ // assert(document) // Non-NULL document object is expected. // // if (index > 0 && document.nodes.start + index <= document.nodes.top) { // return document.nodes.start + index - 1 // } // return NULL //} // ///** // * Get the root object. // */ // //YAML_DECLARE(yaml_node_t *) //yaml_document_get_root_node(document *yaml_document_t) //{ // assert(document) // Non-NULL document object is expected. // // if (document.nodes.top != document.nodes.start) { // return document.nodes.start // } // return NULL //} // ///* // * Add a scalar node to a document. // */ // //YAML_DECLARE(int) //yaml_document_add_scalar(document *yaml_document_t, // tag *yaml_char_t, value *yaml_char_t, length int, // style yaml_scalar_style_t) //{ // struct { // error yaml_error_type_t // } context // mark yaml_mark_t = { 0, 0, 0 } // tag_copy *yaml_char_t = NULL // value_copy *yaml_char_t = NULL // node yaml_node_t // // assert(document) // Non-NULL document object is expected. // assert(value) // Non-NULL value is expected. // // if (!tag) { // tag = (yaml_char_t *)YAML_DEFAULT_SCALAR_TAG // } // // if (!yaml_check_utf8(tag, strlen((char *)tag))) goto error // tag_copy = yaml_strdup(tag) // if (!tag_copy) goto error // // if (length < 0) { // length = strlen((char *)value) // } // // if (!yaml_check_utf8(value, length)) goto error // value_copy = yaml_malloc(length+1) // if (!value_copy) goto error // memcpy(value_copy, value, length) // value_copy[length] = '\0' // // SCALAR_NODE_INIT(node, tag_copy, value_copy, length, style, mark, mark) // if (!PUSH(&context, document.nodes, node)) goto error // // return document.nodes.top - document.nodes.start // //error: // yaml_free(tag_copy) // yaml_free(value_copy) // // return 0 //} // ///* // * Add a sequence node to a document. // */ // //YAML_DECLARE(int) //yaml_document_add_sequence(document *yaml_document_t, // tag *yaml_char_t, style yaml_sequence_style_t) //{ // struct { // error yaml_error_type_t // } context // mark yaml_mark_t = { 0, 0, 0 } // tag_copy *yaml_char_t = NULL // struct { // start *yaml_node_item_t // end *yaml_node_item_t // top *yaml_node_item_t // } items = { NULL, NULL, NULL } // node yaml_node_t // // assert(document) // Non-NULL document object is expected. // // if (!tag) { // tag = (yaml_char_t *)YAML_DEFAULT_SEQUENCE_TAG // } // // if (!yaml_check_utf8(tag, strlen((char *)tag))) goto error // tag_copy = yaml_strdup(tag) // if (!tag_copy) goto error // // if (!STACK_INIT(&context, items, INITIAL_STACK_SIZE)) goto error // // SEQUENCE_NODE_INIT(node, tag_copy, items.start, items.end, // style, mark, mark) // if (!PUSH(&context, document.nodes, node)) goto error // // return document.nodes.top - document.nodes.start // //error: // STACK_DEL(&context, items) // yaml_free(tag_copy) // // return 0 //} // ///* // * Add a mapping node to a document. // */ // //YAML_DECLARE(int) //yaml_document_add_mapping(document *yaml_document_t, // tag *yaml_char_t, style yaml_mapping_style_t) //{ // struct { // error yaml_error_type_t // } context // mark yaml_mark_t = { 0, 0, 0 } // tag_copy *yaml_char_t = NULL // struct { // start *yaml_node_pair_t // end *yaml_node_pair_t // top *yaml_node_pair_t // } pairs = { NULL, NULL, NULL } // node yaml_node_t // // assert(document) // Non-NULL document object is expected. // // if (!tag) { // tag = (yaml_char_t *)YAML_DEFAULT_MAPPING_TAG // } // // if (!yaml_check_utf8(tag, strlen((char *)tag))) goto error // tag_copy = yaml_strdup(tag) // if (!tag_copy) goto error // // if (!STACK_INIT(&context, pairs, INITIAL_STACK_SIZE)) goto error // // MAPPING_NODE_INIT(node, tag_copy, pairs.start, pairs.end, // style, mark, mark) // if (!PUSH(&context, document.nodes, node)) goto error // // return document.nodes.top - document.nodes.start // //error: // STACK_DEL(&context, pairs) // yaml_free(tag_copy) // // return 0 //} // ///* // * Append an item to a sequence node. // */ // //YAML_DECLARE(int) //yaml_document_append_sequence_item(document *yaml_document_t, // sequence int, item int) //{ // struct { // error yaml_error_type_t // } context // // assert(document) // Non-NULL document is required. // assert(sequence > 0 // && document.nodes.start + sequence <= document.nodes.top) // // Valid sequence id is required. // assert(document.nodes.start[sequence-1].type == YAML_SEQUENCE_NODE) // // A sequence node is required. // assert(item > 0 && document.nodes.start + item <= document.nodes.top) // // Valid item id is required. // // if (!PUSH(&context, // document.nodes.start[sequence-1].data.sequence.items, item)) // return 0 // // return 1 //} // ///* // * Append a pair of a key and a value to a mapping node. // */ // //YAML_DECLARE(int) //yaml_document_append_mapping_pair(document *yaml_document_t, // mapping int, key int, value int) //{ // struct { // error yaml_error_type_t // } context // // pair yaml_node_pair_t // // assert(document) // Non-NULL document is required. // assert(mapping > 0 // && document.nodes.start + mapping <= document.nodes.top) // // Valid mapping id is required. // assert(document.nodes.start[mapping-1].type == YAML_MAPPING_NODE) // // A mapping node is required. // assert(key > 0 && document.nodes.start + key <= document.nodes.top) // // Valid key id is required. // assert(value > 0 && document.nodes.start + value <= document.nodes.top) // // Valid value id is required. // // pair.key = key // pair.value = value // // if (!PUSH(&context, // document.nodes.start[mapping-1].data.mapping.pairs, pair)) // return 0 // // return 1 //} // // golang-go.yaml-yaml-v4-4.0.0~rc4/internal/libyaml/api_test.go000066400000000000000000000401221516501332500240300ustar00rootroot00000000000000// Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Tests for high-level API helpers. // Data-driven tests for parser/emitter configuration and token insertion // functions. package libyaml import ( "fmt" "reflect" "strconv" "strings" "testing" "go.yaml.in/yaml/v4/internal/testutil/assert" ) func TestAPI(t *testing.T) { RunTestCases(t, "api.yaml", map[string]TestHandler{ "api-new": runAPINewTest, "api-method": runAPIMethodTest, "api-panic": runAPIPanicTest, "api-delete": runAPIDeleteTest, "api-new-event": runAPINewEventTest, }) } 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) } // 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~rc4/internal/libyaml/composer.go000066400000000000000000000216161516501332500240560ustar00rootroot00000000000000// 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 } // NewComposer creates a new composer from a byte slice. func NewComposer(b []byte) *Composer { p := Composer{ Parser: NewParser(), } if len(b) == 0 { b = []byte{'\n'} } p.Parser.SetInputString(b) return &p } // NewComposerFromReader creates a new composer from an io.Reader. func NewComposerFromReader(r io.Reader) *Composer { p := Composer{ Parser: NewParser(), } p.Parser.SetInputReader(r) return &p } 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 } } 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 { failf("attempted to go past the end of stream; corrupted value?") } if c.event.Type != e { c.fail(fmt.Errorf("expected %s event but got %s", e, c.event.Type)) } 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 } func (c *Composer) fail(err error) { Fail(err) } func (c *Composer) anchor(n *Node, anchor []byte) { if anchor != nil { n.Anchor = string(anchor) c.anchors[n.Anchor] = n } } // Parse parses the next YAML node from the event stream. func (c *Composer) Parse() *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()) } } func (c *Composer) node(kind Kind, defaultTag, 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 } else if defaultTag != "" { tag = defaultTag } else if kind == ScalarNode { // Delegate to resolver to determine tag from value tag, _ = resolve("", value) } n := &Node{ Kind: kind, Tag: tag, Value: value, Style: style, } if !c.Textless { n.Line = c.event.StartMark.Line + 1 n.Column = c.event.StartMark.Column + 1 n.HeadComment = string(c.event.HeadComment) n.LineComment = string(c.event.LineComment) n.FootComment = string(c.event.FootComment) } return n } func (c *Composer) parseChild(parent *Node) *Node { child := c.Parse() parent.Content = append(parent.Content, child) return child } 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 } func (c *Composer) createStreamNode() *Node { n := &Node{ Kind: StreamNode, Encoding: c.encoding, } if !c.Textless && c.event.Type != NO_EVENT { n.Line = c.event.StartMark.Line + 1 n.Column = c.event.StartMark.Column + 1 } return n } // captureDirectives captures version and tag directives from upcoming DOCUMENT_START. func (c *Composer) captureDirectives(n *Node) { if c.peek() == DOCUMENT_START_EVENT { if vd := c.event.GetVersionDirective(); vd != nil { n.Version = &StreamVersionDirective{ Major: vd.Major(), Minor: vd.Minor(), } } if tds := c.event.GetTagDirectives(); len(tds) > 0 { n.TagDirectives = make([]StreamTagDirective, len(tds)) for i, td := range tds { n.TagDirectives[i] = StreamTagDirective{ Handle: td.GetHandle(), Prefix: td.GetPrefix(), } } } } } 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(&ParserError{ Message: msg, Mark: Mark{ Line: n.Line, Column: n.Column, }, }) } c.expect(ALIAS_EVENT) return n } 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) var defaultTag string if nodeStyle != 0 { defaultTag = strTag } n := c.node(ScalarNode, defaultTag, nodeTag, nodeValue) n.Style |= nodeStyle c.anchor(n, c.event.Anchor) c.expect(SCALAR_EVENT) return n } func (c *Composer) sequence() *Node { n := c.node(SequenceNode, seqTag, 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 } func (c *Composer) mapping() *Node { n := c.node(MappingNode, mapTag, 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 } func Fail(err error) { panic(&YAMLError{err}) } func failf(format string, args ...any) { panic(&YAMLError{fmt.Errorf("yaml: "+format, args...)}) } golang-go.yaml-yaml-v4-4.0.0~rc4/internal/libyaml/constructor.go000066400000000000000000000734151516501332500246200ustar00rootroot00000000000000// 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" "errors" "fmt" "math" "reflect" "strings" "sync" "time" ) // -------------------------------------------------------------------------- // Interfaces and types needed by constructor // constructor interface may be implemented by types to customize their // behavior when being constructed from a YAML document. type constructor interface { UnmarshalYAML(value *Node) error } type obsoleteConstructor interface { UnmarshalYAML(construct func(any) error) error } // Marshaler interface may be implemented by types to customize their // behavior when being marshaled into a YAML document. type Marshaler interface { MarshalYAML() (any, 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 } // handleErr recovers from panics caused by yaml errors func handleErr(err *error) { if v := recover(); v != nil { if e, ok := v.(*YAMLError); ok { *err = e.Err } else { panic(v) } } } // -------------------------------------------------------------------------- // Struct field information 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 } 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 } var ( structMap = make(map[reflect.Type]*structInfo) fieldMapMutex sync.RWMutex constructorType reflect.Type ) func init() { var v constructor constructorType = reflect.ValueOf(&v).Elem().Type() } // hasConstructYAMLMethod checks if a type has an UnmarshalYAML method // that looks like it implements yaml.Unmarshaler (from root package). // This is needed because we can't directly check for the interface type // since it's in a different package that we can't import. 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" { return false } // Return type should be error retType := mtype.Out(0) if retType.Kind() != reflect.Interface || retType.Name() != "error" { return false } return true } 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 } // 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 } type Constructor struct { doc *Node aliases map[*Node]bool TypeErrors []*ConstructError stringMapType reflect.Type generalMapType reflect.Type KnownFields bool UniqueKeys bool constructCount int aliasCount int aliasDepth int mergedFields map[any]bool } 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() ) func NewConstructor(opts *Options) *Constructor { return &Constructor{ stringMapType: stringMapType, generalMapType: generalMapType, KnownFields: opts.KnownFields, UniqueKeys: opts.UniqueKeys, aliases: make(map[*Node]bool), } } // Construct decodes YAML input into the provided output value. // The out parameter must be a pointer to the value to decode into. // Returns a [LoadErrors] if type mismatches occur during decoding. func Construct(in []byte, out any, opts *Options) error { d := NewConstructor(opts) p := NewComposer(in) defer p.Destroy() node := p.Parse() if node != nil { v := reflect.ValueOf(out) if v.Kind() == reflect.Pointer && !v.IsNil() { v = v.Elem() } d.Construct(node, v) } if len(d.TypeErrors) > 0 { return &LoadErrors{Errors: d.TypeErrors} } return nil } 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, &ConstructError{ Err: fmt.Errorf("cannot construct %s%s into %s", shortTag(tag), value, out.Type()), Line: n.Line, Column: n.Column, }) } 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, &ConstructError{ Err: err, Line: n.Line, Column: n.Column, }) return false } } func (c *Constructor) callObsoleteConstructor(n *Node, u obsoleteConstructor) (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, &ConstructError{ Err: err, Line: n.Line, Column: n.Column, }) return false } } 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 } // 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.(obsoleteConstructor); ok { good = c.callObsoleteConstructor(n, u) return out, true, good } } } return out, false, false } 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 } const ( // 400,000 decode operations is ~500kb of dense object declarations, or // ~5kb of dense object declarations with 10000% alias expansion alias_ratio_range_low = 400000 // 4,000,000 decode operations is ~5MB of dense object declarations, or // ~4.5MB of dense object declarations with 10% alias expansion alias_ratio_range_high = 4000000 // alias_ratio_range is the range over which we scale allowed alias ratios alias_ratio_range = float64(alias_ratio_range_high - alias_ratio_range_low) ) func allowedAliasRatio(constructCount int) float64 { switch { case constructCount <= alias_ratio_range_low: // allow 99% to come from alias expansion for small-to-medium documents return 0.99 case constructCount >= alias_ratio_range_high: // allow 10% to come from alias expansion for very large documents return 0.10 default: // scale smoothly from 99% down to 10% over the range. // this maps to 396,000 - 400,000 allowed alias-driven decodes over the range. // 400,000 decode operations is ~100MB of allocations in worst-case scenarios (single-item maps). return 0.99 - 0.89*(float64(constructCount-alias_ratio_range_low)/alias_ratio_range) } } // 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 } // tryCallYAMLConstructor checks if the value has an UnmarshalYAML method that takes // a *yaml.Node (from the root package) and calls it if found. // This handles the case where user types implement yaml.Unmarshaler instead of 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 } // Check if it's the same underlying type as our Node // Both yaml.Node and libyaml.Node have the same structure if elemType.Name() != "Node" { return false, false } // Call the method with a converted node // Since yaml.Node and libyaml.Node have the same structure, // we can convert using unsafe pointer cast 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, &ConstructError{ Err: e.(error), Line: n.Line, Column: n.Column, }) return true, false } } func (c *Constructor) Construct(n *Node, out reflect.Value) (good bool) { c.constructCount++ if c.aliasDepth > 0 { c.aliasCount++ } if c.aliasCount > 100 && c.constructCount > 1000 && float64(c.aliasCount)/float64(c.constructCount) > allowedAliasRatio(c.constructCount) { failf("document contains excessive aliasing") } if out.Type() == nodeType { out.Set(reflect.ValueOf(n).Elem()) return true } // 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, &ConstructError{ Err: err, Line: n.Line, Column: n.Column, }) return false } 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 } 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: failf("cannot construct node with unknown kind %d", n.Kind) } return good } 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 } func (c *Constructor) alias(n *Node, out reflect.Value) (good bool) { if c.aliases[n] { // TODO this could actually be allowed in some circumstances. failf("anchor '%s' value contains itself", n.Value) } c.aliases[n] = true c.aliasDepth++ good = c.Construct(n.Alias, out) c.aliasDepth-- delete(c.aliases, n) return good } 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 } func (c *Constructor) scalar(n *Node, out reflect.Value) bool { 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 { failf("!!binary value contains invalid base64 data") } resolved = string(data) } } if resolved == nil { return c.null(out) } if resolvedv := reflect.ValueOf(resolved); out.Type() == resolvedv.Type() { // We've resolved to exactly the type we want, so use that. out.Set(resolvedv) return true } // Perhaps we can use the value as a TextUnmarshaler to // set its value. if out.CanAddr() { u, ok := out.Addr().Interface().(encoding.TextUnmarshaler) if ok { var text []byte if tag == binaryTag { text = []byte(resolved.(string)) } else { // We let any value be constructed into TextUnmarshaler. // That might be more lax than we'd like, but the // TextUnmarshaler itself should bowl out any dubious values. text = []byte(n.Value) } err := u.UnmarshalText(text) if err != nil { c.TypeErrors = append(c.TypeErrors, &ConstructError{ Err: err, Line: n.Line, Column: n.Column, }) return false } return true } } switch out.Kind() { case reflect.String: if tag == binaryTag { out.SetString(resolved.(string)) return true } out.SetString(n.Value) return true case reflect.Slice: // allow decoding !!binary-tagged value into []byte specifically if out.Type().Elem().Kind() == reflect.Uint8 { if tag == binaryTag { out.SetBytes([]byte(resolved.(string))) return true } } case reflect.Interface: out.Set(reflect.ValueOf(resolved)) return true case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: // This used to work in v2, but it's very unfriendly. 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.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.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.Struct: if resolvedv := reflect.ValueOf(resolved); out.Type() == resolvedv.Type() { out.Set(resolvedv) return true } case reflect.Pointer: panic("yaml internal error: please report the issue") } c.tagError(n, tag, out) return false } func settableValueOf(i any) reflect.Value { v := reflect.ValueOf(i) sv := reflect.New(v.Type()).Elem() sv.Set(v) return sv } 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() { failf("invalid array: want %d elements but got %d", out.Len(), l) } 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 } 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, &ConstructError{ Err: fmt.Errorf("mapping key %#v already defined at line %d", nj.Value, ni.Line), 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) { continue } c.setPossiblyUnhashableKey(mergedFields, ki, true) } kkind := k.Kind() if kkind == reflect.Interface { kkind = k.Elem().Kind() } if kkind == reflect.Map || kkind == reflect.Slice { failf("cannot use '%#v' as a map key; try decoding into yaml.Node", k.Interface()) } 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 } 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 } 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, &ConstructError{ Err: fmt.Errorf("field %s already set in type %s", name.String(), out.Type()), 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, &ConstructError{ Err: fmt.Errorf("field %s not found in type %s", name.String(), out.Type()), Line: ni.Line, Column: ni.Column, }) } } c.mergedFields = mergedFields if mergeNode != nil { c.merge(n, mergeNode, out) } return true } func failWantMap() { failf("map merge requires map or sequence of maps as the value") } func (c *Constructor) setPossiblyUnhashableKey(m map[any]bool, key any, value bool) { defer func() { if err := recover(); err != nil { failf("%v", err) } }() m[key] = value } func (c *Constructor) getPossiblyUnhashableKey(m map[any]bool, key any) bool { defer func() { if err := recover(); err != nil { failf("%v", err) } }() return m[key] } 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) } } } switch merge.Kind { case MappingNode: c.Construct(merge, out) case AliasNode: if merge.Alias != nil && merge.Alias.Kind != MappingNode { failWantMap() } 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() } } else if ni.Kind != MappingNode { failWantMap() } c.Construct(ni, out) } default: failWantMap() } c.mergedFields = mergedFields } func isMerge(n *Node) bool { return n.Kind == ScalarNode && shortTag(n.Tag) == mergeTag } golang-go.yaml-yaml-v4-4.0.0~rc4/internal/libyaml/constructor_test.go000066400000000000000000000014261516501332500256500ustar00rootroot00000000000000// 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 := LoadYAML([]byte(tc.Yaml)) assert.NoErrorf(t, err, "LoadYAML() error: %v", err) // Compare the result with expected value if !reflect.DeepEqual(result, tc.Want) { t.Errorf("LoadYAML() = %v (type: %T), want %v (type: %T)", result, result, tc.Want, tc.Want) } }, }) } golang-go.yaml-yaml-v4-4.0.0~rc4/internal/libyaml/doc.go000066400000000000000000000004521516501332500227670ustar00rootroot00000000000000// 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~rc4/internal/libyaml/emitter.go000066400000000000000000001527501516501332500237040ustar00rootroot00000000000000// 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" ) // 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 } // 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 } // 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 } // 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 } // 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") } // 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", } } // 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) } // processLineComment preserves the original signature and delegates to // processLineCommentLinebreak passing false for linebreak func (emitter *Emitter) processLineComment() error { return emitter.processLineCommentLinebreak(false) } // 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 } func (emitter *Emitter) silentNilEvent(event *Event) bool { return event.Type == SCALAR_EVENT && event.Implicit && !emitter.canonical && len(emitter.scalar_data.value) == 0 } // 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 } // 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 } // 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 } // 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 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 } // Write an 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 } // 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 } 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 } 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 } func (emitter *Emitter) writeAnchor(value []byte) error { if err := emitter.writeAll(value); err != nil { return err } emitter.whitespace = false emitter.indention = false return nil } 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 } 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 } 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 } 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 } 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 } 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 } 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 } 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 } 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 } golang-go.yaml-yaml-v4-4.0.0~rc4/internal/libyaml/emitter_test.go000066400000000000000000000020621516501332500247310ustar00rootroot00000000000000// 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, }) } 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~rc4/internal/libyaml/errors.go000066400000000000000000000072621516501332500235440ustar00rootroot00000000000000// 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" ) type MarkedYAMLError struct { // optional context ContextMark Mark ContextMessage string Mark Mark Message string } func (e MarkedYAMLError) Error() string { var builder strings.Builder builder.WriteString("yaml: ") if len(e.ContextMessage) > 0 { fmt.Fprintf(&builder, "%s at %s: ", e.ContextMessage, e.ContextMark) } if len(e.ContextMessage) == 0 || e.ContextMark != e.Mark { fmt.Fprintf(&builder, "%s: ", e.Mark) } builder.WriteString(e.Message) return builder.String() } type ParserError MarkedYAMLError func (e ParserError) Error() string { return MarkedYAMLError(e).Error() } type ScannerError MarkedYAMLError func (e ScannerError) Error() string { return MarkedYAMLError(e).Error() } type ReaderError struct { Offset int Value int Err error } func (e ReaderError) Error() string { return fmt.Sprintf("yaml: offset %d: %s", e.Offset, e.Err) } func (e ReaderError) Unwrap() error { return e.Err } type EmitterError struct { Message string } func (e EmitterError) Error() string { return fmt.Sprintf("yaml: %s", e.Message) } type WriterError struct { Err error } func (e WriterError) Error() string { return fmt.Sprintf("yaml: %s", e.Err) } func (e WriterError) Unwrap() error { return e.Err } // ConstructError represents a single, non-fatal error that occurred during // the constructing of a YAML document into a Go value. type ConstructError struct { Err error Line int Column int } func (e *ConstructError) Error() string { return fmt.Sprintf("line %d: %s", e.Line, e.Err.Error()) } func (e *ConstructError) Unwrap() error { return e.Err } // LoadErrors is returned when one or more fields cannot be properly decoded. type LoadErrors struct { Errors []*ConstructError } func (e *LoadErrors) Error() string { var b strings.Builder b.WriteString("yaml: construct errors:") for _, err := range e.Errors { b.WriteString("\n ") b.WriteString(err.Error()) } 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 // *ConstructError targets by returning the first error in the list. func (e *LoadErrors) As(target any) bool { switch t := target.(type) { case **ConstructError: 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.Error()) } *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 an obsolete 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 } func (e *TypeError) Error() string { return fmt.Sprintf("yaml: unmarshal errors:\n %s", strings.Join(e.Errors, "\n ")) } // YAMLError is an internal error wrapper type. type YAMLError struct { Err error } func (e *YAMLError) Error() string { return e.Err.Error() } golang-go.yaml-yaml-v4-4.0.0~rc4/internal/libyaml/node.go000066400000000000000000000236441516501332500231570ustar00rootroot00000000000000// 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" ) const longTagPrefix = "tag:yaml.org,2002:" var ( longTags = make(map[string]string) shortTags = make(map[string]string) ) 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 } } func shortTag(tag string) string { if strings.HasPrefix(tag, longTagPrefix) { if stag, ok := shortTags[tag]; ok { return stag } return "!!" + tag[len(longTagPrefix):] } return tag } 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 const ( DocumentNode Kind = 1 << iota SequenceNode MappingNode ScalarNode AliasNode StreamNode ) // Style represents the formatting style of a YAML node type Style uint32 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 } // 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 // StreamNode-specific fields (only valid when Kind == StreamNode) // Encoding holds the stream encoding (UTF-8, UTF-16LE, UTF-16BE). // Only valid for StreamNode. Encoding Encoding // Version holds the YAML version directive (%YAML). // Only valid for StreamNode. Version *StreamVersionDirective // TagDirectives holds the %TAG directives. // Only valid for StreamNode. TagDirectives []StreamTagDirective } // 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.Encoding == 0 && n.Version == nil && n.TagDirectives == 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) } 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) e := NewRepresenter(noWriter, DefaultOptions) defer e.Destroy() e.MarshalDoc("", reflect.ValueOf(v)) e.Finish() p := NewComposer(e.Out) p.Textless = true defer p.Destroy() doc := p.Parse() *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 } e := NewRepresenter(noWriter, o) defer e.Destroy() e.MarshalDoc("", reflect.ValueOf(v)) e.Finish() p := NewComposer(e.Out) p.Textless = true defer p.Destroy() doc := p.Parse() *n = *doc.Content[0] return nil } golang-go.yaml-yaml-v4-4.0.0~rc4/internal/libyaml/options.go000066400000000000000000000272661516501332500237310ustar00rootroot00000000000000// // Copyright (c) 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 } // Option allows configuring YAML loading and dumping operations. type Option func(*Options) error // 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, } 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~rc4/internal/libyaml/parser.go000066400000000000000000001122611516501332500235200ustar00rootroot00000000000000// 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" ) // 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++ } // 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 } func formatParserError(problem string, problem_mark Mark) error { return ParserError{ Mark: problem_mark, Message: problem, } } func formatParserErrorContext(context string, context_mark Mark, problem string, problem_mark Mark) error { return ParserError{ ContextMark: context_mark, ContextMessage: context, Mark: problem_mark, Message: problem, } } // 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. if !implicit { 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 } 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 } // 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) } // 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 } var default_tag_directives = []TagDirective{ {[]byte("!"), []byte("!")}, {[]byte("!!"), []byte("tag:yaml.org,2002:")}, } // 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 } // ParserGetEvents parses the YAML input and returns the generated event stream. func ParserGetEvents(in []byte) (string, error) { p := NewComposer(in) 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 } 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~rc4/internal/libyaml/parser_test.go000066400000000000000000000112451516501332500245570ustar00rootroot00000000000000// 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, }) } // 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~rc4/internal/libyaml/reader.go000066400000000000000000000320721516501332500234670ustar00rootroot00000000000000// 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" ) func formatReaderError(problem string, offset int, value int) error { return ReaderError{ Offset: offset, Value: value, Err: errors.New(problem), } } // Byte order marks. 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 ReaderError{ Offset: parser.offset, Value: -1, Err: fmt.Errorf("input error: %w", err), } } return nil } // 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( "invalid leading UTF-8 octet", parser.offset, int(octet)) } // Check if the raw buffer contains an incomplete character. if width > raw_unread { if parser.eof { return formatReaderError( "incomplete UTF-8 octet sequence", parser.offset, -1) } 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( "invalid trailing UTF-8 octet", parser.offset+k, int(octet)) } // 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", parser.offset, -1) } // Check the range of the value. if value >= 0xD800 && value <= 0xDFFF || value > 0x10FFFF { return formatReaderError( "invalid Unicode character", parser.offset, int(value)) } 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", parser.offset, -1) } 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( "unexpected low surrogate area", parser.offset, int(value)) } // 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", parser.offset, -1) } 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( "expected low surrogate area", parser.offset+2, int(value2)) } // 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( "control characters are not allowed", parser.offset, int(value)) } // 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 } golang-go.yaml-yaml-v4-4.0.0~rc4/internal/libyaml/reader_test.go000066400000000000000000000106501516501332500245240ustar00rootroot00000000000000// 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 ( "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]) err := formatReaderError(problem, offset, value) // 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~rc4/internal/libyaml/representer.go000066400000000000000000000336121516501332500245640ustar00rootroot00000000000000// 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 marshaling from Go types to the intermediate node representation. package libyaml import ( "encoding" "fmt" "io" "reflect" "regexp" "sort" "strconv" "strings" "time" "unicode" "unicode/utf8" ) type keyList []reflect.Value func (l keyList) Len() int { return len(l) } func (l keyList) Swap(i, j int) { l[i], l[j] = l[j], l[i] } 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") } // Sentinel values for newRepresenter parameters. // These provide clarity at call sites, similar to http.NoBody. var ( noWriter io.Writer = nil noVersionDirective *VersionDirective = nil noTagDirective []TagDirective = nil ) type Representer struct { Emitter Emitter Out []byte flow bool Indent int lineWidth int doneInit bool explicitStart bool explicitEnd bool flowSimpleCollections bool quotePreference QuoteStyle } // NewRepresenter creates a new YAML representr with the given options. // // The writer parameter specifies the output destination for the representr. // If writer is nil, the representr will write to an internal buffer. func NewRepresenter(writer io.Writer, opts *Options) *Representer { 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) r := &Representer{ Emitter: emitter, Indent: opts.Indent, lineWidth: opts.LineWidth, explicitStart: opts.ExplicitStart, explicitEnd: opts.ExplicitEnd, flowSimpleCollections: opts.FlowSimpleCollections, quotePreference: opts.QuotePreference, } if writer != nil { r.Emitter.SetOutputWriter(writer) } else { r.Emitter.SetOutputString(&r.Out) } return r } func (r *Representer) init() { if r.doneInit { return } if r.Indent == 0 { r.Indent = 4 } r.Emitter.BestIndent = r.Indent r.emit(NewStreamStartEvent(UTF8_ENCODING)) r.doneInit = true } func (r *Representer) Finish() { r.Emitter.OpenEnded = false r.emit(NewStreamEndEvent()) } func (r *Representer) Destroy() { r.Emitter.Delete() } func (r *Representer) emit(event Event) { // This will internally delete the event value. r.must(r.Emitter.Emit(&event)) } func (r *Representer) must(err error) { if err != nil { msg := err.Error() if msg == "" { msg = "unknown problem generating YAML content" } failf("%s", msg) } } func (r *Representer) MarshalDoc(tag string, in reflect.Value) { r.init() var node *Node if in.IsValid() { node, _ = in.Interface().(*Node) } if node != nil && node.Kind == DocumentNode { r.nodev(in) } else { // Use !explicitStart for implicit flag (true = implicit/no marker) r.emit(NewDocumentStartEvent(noVersionDirective, noTagDirective, !r.explicitStart)) r.marshal(tag, in) // Use !explicitEnd for implicit flag r.emit(NewDocumentEndEvent(!r.explicitEnd)) } } func (r *Representer) marshal(tag string, in reflect.Value) { tag = shortTag(tag) if !in.IsValid() || in.Kind() == reflect.Pointer && in.IsNil() { r.nilv() return } iface := in.Interface() switch value := iface.(type) { case *Node: r.nodev(in) return case Node: if !in.CanAddr() { n := reflect.New(in.Type()).Elem() n.Set(in) in = n } r.nodev(in.Addr()) return case time.Time: r.timev(tag, in) return case *time.Time: r.timev(tag, in.Elem()) return case time.Duration: r.stringv(tag, reflect.ValueOf(value.String())) return case Marshaler: v, err := value.MarshalYAML() if err != nil { Fail(err) } if v == nil { r.nilv() return } r.marshal(tag, reflect.ValueOf(v)) return case encoding.TextMarshaler: text, err := value.MarshalText() if err != nil { Fail(err) } in = reflect.ValueOf(string(text)) case nil: r.nilv() return } switch in.Kind() { case reflect.Interface: r.marshal(tag, in.Elem()) case reflect.Map: r.mapv(tag, in) case reflect.Pointer: r.marshal(tag, in.Elem()) case reflect.Struct: r.structv(tag, in) case reflect.Slice, reflect.Array: r.slicev(tag, in) case reflect.String: r.stringv(tag, in) case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: r.intv(tag, in) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: r.uintv(tag, in) case reflect.Float32, reflect.Float64: r.floatv(tag, in) case reflect.Bool: r.boolv(tag, in) default: panic("cannot marshal type: " + in.Type().String()) } } func (r *Representer) mapv(tag string, in reflect.Value) { r.mappingv(tag, func() { keys := keyList(in.MapKeys()) sort.Sort(keys) for _, k := range keys { r.marshal("", k) r.marshal("", in.MapIndex(k)) } }) } 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 } func (r *Representer) structv(tag string, in reflect.Value) { sinfo, err := getStructInfo(in.Type()) if err != nil { panic(err) } r.mappingv(tag, func() { 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 } r.marshal("", reflect.ValueOf(info.Key)) r.flow = info.Flow r.marshal("", 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 { panic(fmt.Sprintf("cannot have key %q in inlined map: conflicts with struct field", k.String())) } r.marshal("", k) r.flow = false r.marshal("", m.MapIndex(k)) } } } }) } func (r *Representer) mappingv(tag string, f func()) { implicit := tag == "" style := BLOCK_MAPPING_STYLE if r.flow { r.flow = false style = FLOW_MAPPING_STYLE } r.emit(NewMappingStartEvent(nil, []byte(tag), implicit, style)) f() r.emit(NewMappingEndEvent()) } func (r *Representer) slicev(tag string, in reflect.Value) { implicit := tag == "" style := BLOCK_SEQUENCE_STYLE if r.flow { r.flow = false style = FLOW_SEQUENCE_STYLE } r.emit(NewSequenceStartEvent(nil, []byte(tag), implicit, style)) n := in.Len() for i := 0; i < n; i++ { r.marshal("", in.Index(i)) } r.emit(NewSequenceEndEvent()) } // 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 marshaled 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) } // 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_]*)?$`) // 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 marshaled 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 == "<<" } func (r *Representer) stringv(tag string, in reflect.Value) { var style ScalarStyle s := in.String() canUsePlain := true switch { case !utf8.ValidString(s): if tag == binaryTag { failf("explicitly tagged !!binary data must be base64-encoded") } if tag != "" { failf("cannot marshal 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 == "": // Check to see if it would resolve to a specific // tag when represented unquoted. If it doesn't, // there's no need to quote it. rtag, _ := resolve("", s) canUsePlain = rtag == strTag && !(isBase60Float(s) || isOldBool(s) || looksLikeMerge(s)) } // Note: it's possible for user code to emit invalid YAML // if they explicitly specify a tag and a string containing // text that's incompatible with that tag. switch { case strings.Contains(s, "\n"): if r.flow || !shouldUseLiteralStyle(s) { style = DOUBLE_QUOTED_SCALAR_STYLE } else { style = LITERAL_SCALAR_STYLE } case canUsePlain: style = PLAIN_SCALAR_STYLE default: style = r.quotePreference.ScalarStyle() } r.emitScalar(s, "", tag, style, nil, nil, nil, nil) } func (r *Representer) boolv(tag string, in reflect.Value) { var s string if in.Bool() { s = "true" } else { s = "false" } r.emitScalar(s, "", tag, PLAIN_SCALAR_STYLE, nil, nil, nil, nil) } func (r *Representer) intv(tag string, in reflect.Value) { s := strconv.FormatInt(in.Int(), 10) r.emitScalar(s, "", tag, PLAIN_SCALAR_STYLE, nil, nil, nil, nil) } func (r *Representer) uintv(tag string, in reflect.Value) { s := strconv.FormatUint(in.Uint(), 10) r.emitScalar(s, "", tag, PLAIN_SCALAR_STYLE, nil, nil, nil, nil) } func (r *Representer) timev(tag string, in reflect.Value) { t := in.Interface().(time.Time) s := t.Format(time.RFC3339Nano) r.emitScalar(s, "", tag, PLAIN_SCALAR_STYLE, nil, nil, nil, nil) } func (r *Representer) floatv(tag string, in reflect.Value) { // 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" } r.emitScalar(s, "", tag, PLAIN_SCALAR_STYLE, nil, nil, nil, nil) } func (r *Representer) nilv() { r.emitScalar("null", "", "", PLAIN_SCALAR_STYLE, nil, nil, nil, nil) } func (r *Representer) emitScalar( value, anchor, tag string, style ScalarStyle, head, line, foot, tail []byte, ) { // TODO Kill this function. Replace all initialize calls by their underlining Go literals. 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 r.emit(event) } func (r *Representer) nodev(in reflect.Value) { r.node(in.Interface().(*Node), "") } golang-go.yaml-yaml-v4-4.0.0~rc4/internal/libyaml/resolver.go000066400000000000000000000136351516501332500240720ustar00rootroot00000000000000// 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" "math" "regexp" "strconv" "strings" "time" ) type resolveMapItem struct { value any tag string } 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) 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} } } } func resolvableTag(tag string) bool { switch tag { case "", strTag, boolTag, intTag, floatTag, nullTag, timestampTag: return true } return false } var yamlStyleFloat = regexp.MustCompile(`^[-+]?(\.[0-9]+|[0-9]+(\.[0-9]*)?)([eE][-+]?[0-9]+)?$`) 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 } } } failf("cannot construct %s `%s` as a %s", shortTag(rtag), in, shortTag(tag)) }() // 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 } // 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]) } // 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. } // 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 } golang-go.yaml-yaml-v4-4.0.0~rc4/internal/libyaml/scanner.go000066400000000000000000002465011516501332500236620ustar00rootroot00000000000000// 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 // // Advance the buffer pointer. 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]) } func (parser *Parser) skipLine() { if isCRLF(parser.buffer, parser.buffer_pos) { parser.mark.Index += 2 parser.mark.Column = 0 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 = 0 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 = 0 parser.mark.Line++ parser.unread-- parser.newlines++ return s } // Scan gets 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 } func formatScannerError(problem string, problem_mark Mark) error { problem_mark.Line += 1 return ScannerError{ Mark: problem_mark, Message: problem, } } func formatScannerErrorContext(context string, context_mark Mark, problem string, problem_mark Mark) error { context_mark.Line += 1 problem_mark.Line += 1 return ScannerError{ ContextMark: context_mark, ContextMessage: context, Mark: problem_mark, Message: problem, } } func (parser *Parser) setScannerTagError(directive bool, context_mark Mark, problem string) error { context := "while parsing a tag" if directive { context = "while parsing a %TAG directive" } return formatScannerErrorContext(context, context_mark, problem, parser.mark) } 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. 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 == 0 && 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 == 0 && 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 == 0 && 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 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) } 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) 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 } // max_flow_level limits the flow_level const max_flow_level = 10000 // Increase the flow level and resize the simple key list if needed. func (parser *Parser) increaseFlowLevel() error { // Increase the flow level. parser.flow_level++ if parser.flow_level > max_flow_level { return formatScannerErrorContext( "while increasing flow level", parser.simple_key.mark, fmt.Sprintf("exceeded max depth of %d", max_flow_level), 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 } // max_indents limits the indents stack size const max_indents = 10000 // Push the current indentation level to the stack and set the new level // the current column is greater than the indentation level. In this case, // append or insert the specified token into the token queue. 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 len(parser.indents) > max_indents { return formatScannerErrorContext( "while increasing indent level", parser.simple_key.mark, fmt.Sprintf("exceeded max depth of %d", max_indents), 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 } // Initialize the scanner and produce the STREAM-START 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) fetchStreamEnd() error { // Force new line. if parser.mark.Column != 0 { parser.mark.Column = 0 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) 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) 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) 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) 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) 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) 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) 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) 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) 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) 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) 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) 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) 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 == 0 && 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 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) 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) 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 } const max_number_length = 2 // Scan the version number of VERSION-DIRECTIVE. // // Scope: // // %YAML 1.1 # a comment \n // ^ // %YAML 1.1 # a comment \n // ^ 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) 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 } 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 } /* * Scan a TAG 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) 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) 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) 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 { indent = increment } } // 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) 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 == 0 && ((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) 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 == 0 && ((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. if (parser.buffer[parser.buffer_pos] == ':' && isBlankOrZero(parser.buffer, parser.buffer_pos+1)) || (parser.flow_level > 0 && (parser.buffer[parser.buffer_pos] == ',' || (parser.buffer[parser.buffer_pos] == '?' && isBlankOrZero(parser.buffer, parser.buffer_pos+1)) || parser.buffer[parser.buffer_pos] == '[' || parser.buffer[parser.buffer_pos] == ']' || parser.buffer[parser.buffer_pos] == '{' || 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 } 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 } 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 = 0 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 = 0 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 } golang-go.yaml-yaml-v4-4.0.0~rc4/internal/libyaml/scanner_test.go000066400000000000000000000066101516501332500247140ustar00rootroot00000000000000// 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, }) } // 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) } } golang-go.yaml-yaml-v4-4.0.0~rc4/internal/libyaml/serializer.go000066400000000000000000000143531516501332500244000ustar00rootroot00000000000000// 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 ( "strings" "unicode/utf8" ) // node serializes a Node tree into YAML events. // This is the core of the serializer stage - it walks the tree and produces events. func (r *Representer) node(node *Node, tail string) { // Zero nodes behave as nil. if node.Kind == 0 && node.IsZero() { r.nilv() return } // If the tag was not explicitly requested, and dropping it won't change the // implicit tag of the value, don't include it in the presentation. tag := node.Tag stag := shortTag(tag) var forceQuoting bool if tag != "" && node.Style&TaggedStyle == 0 { if node.Kind == ScalarNode { if stag == strTag && node.Style&(SingleQuotedStyle|DoubleQuotedStyle|LiteralStyle|FoldedStyle) != 0 { tag = "" } else { rtag, _ := resolve("", node.Value) if rtag == stag && stag != mergeTag { tag = "" } else if stag == strTag { tag = "" forceQuoting = true } } } else { var rtag string switch node.Kind { case MappingNode: rtag = mapTag case SequenceNode: rtag = seqTag } if rtag == stag { tag = "" } } } switch node.Kind { case DocumentNode: event := NewDocumentStartEvent(noVersionDirective, noTagDirective, !r.explicitStart) event.HeadComment = []byte(node.HeadComment) r.emit(event) for _, node := range node.Content { r.node(node, "") } event = NewDocumentEndEvent(!r.explicitEnd) event.FootComment = []byte(node.FootComment) r.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 || r.isSimpleCollection(node) { style = FLOW_SEQUENCE_STYLE } event := NewSequenceStartEvent([]byte(node.Anchor), []byte(longTag(tag)), tag == "", style) event.HeadComment = []byte(node.HeadComment) r.emit(event) for _, node := range node.Content { r.node(node, "") } event = NewSequenceEndEvent() event.LineComment = []byte(node.LineComment) event.FootComment = []byte(node.FootComment) r.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 || r.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) r.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 } r.node(k, tail) tail = foot v := node.Content[i+1] r.node(v, "") } event = NewMappingEndEvent() event.TailComment = []byte(tail) event.LineComment = []byte(node.LineComment) event.FootComment = []byte(node.FootComment) r.emit(event) case AliasNode: event := NewAliasEvent([]byte(node.Value)) event.HeadComment = []byte(node.HeadComment) event.LineComment = []byte(node.LineComment) event.FootComment = []byte(node.FootComment) r.emit(event) case ScalarNode: value := node.Value if !utf8.ValidString(value) { if stag == binaryTag { failf("explicitly tagged !!binary data must be base64-encoded") } if stag != "" { failf("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 = r.quotePreference.ScalarStyle() } r.emitScalar(value, node.Anchor, tag, style, []byte(node.HeadComment), []byte(node.LineComment), []byte(node.FootComment), []byte(tail)) default: failf("cannot represent node with unknown kind %d", node.Kind) } } // isSimpleCollection checks if a node contains only scalar values and would // fit within the line width when rendered in flow style. func (r *Representer) isSimpleCollection(node *Node) bool { if !r.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 := r.estimateFlowLength(node) width := r.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 (r *Representer) 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 } golang-go.yaml-yaml-v4-4.0.0~rc4/internal/libyaml/testdata/000077500000000000000000000000001516501332500235035ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc4/internal/libyaml/testdata/api.yaml000066400000000000000000000143651516501332500251510ustar00rootroot00000000000000# Copyright 2025 The go-yaml Project Contributors # SPDX-License-Identifier: Apache-2.0 # API tests for constructors and methods # # Purpose: Tests the public API of Parser and Emitter types (constructors, methods, cleanup). # # Test Types: # api-new - Tests constructors (NewParser, NewEmitter) # api-method - Tests methods and resulting field state # api-panic - Tests methods that should panic under certain conditions # api-delete - Tests cleanup via Delete() method # api-new-event - Tests event constructor functions # # Common Keys: # name - Test case name (string) # type - Test type (api-new | api-method | api-panic | api-delete | api-new-event) # with - Constructor name (NewParser | NewEmitter) - which type to instantiate # call - Method call specification [MethodName, arg1, arg2, ...] - method to call with arguments # init - Setup method call [MethodName, arg1, arg2, ...] - called before main method (for api-panic) # byte - Boolean flag, when true converts string arguments to []byte # test - Field validation specifications (for api-new, api-method, api-delete): # List of field checks, each in format: operator: [field, value] # Operators: # * nil - Boolean, check if field is nil (true) or not nil (false) # * cap - Integer, check slice capacity equals this value # * len - Integer, check length (for slices/strings) equals this value # * eq - Value, check field equals this value # * gte - Integer, check value is greater than or equal # * len-gt - Integer, check length is greater than this value # Examples: # * nil: [buffer, false] - buffer is not nil # * cap: [buffer, 512] - buffer has capacity 512 # * eq: [input, 'test'] - input field equals 'test' # want - Expected result (format varies by test type): # * For api-panic: string containing expected panic message substring # * For other types: format varies # Constructor tests - 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-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] # Method tests - 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-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] # Panic tests - 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-panic: name: Emitter set encoding twice with: NewEmitter init: [SetEncoding, UTF8_ENCODING] call: [SetEncoding, UTF16LE_ENCODING] want: must set the output encoding only once # Delete tests - api-delete: name: Parser delete with: NewParser byte: true init: [SetInputString, test] test: - len: [input, 0] - len: [buffer, 0] - 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~rc4/internal/libyaml/testdata/constructor.yaml000066400000000000000000000035451516501332500267630ustar00rootroot00000000000000# 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~rc4/internal/libyaml/testdata/emitter.yaml000066400000000000000000000274121516501332500260460ustar00rootroot00000000000000# 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 golang-go.yaml-yaml-v4-4.0.0~rc4/internal/libyaml/testdata/parser.yaml000066400000000000000000000167341516501332500256760ustar00rootroot00000000000000# 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 # Error tests - parse-error: name: Error state yaml: | key: : invalid golang-go.yaml-yaml-v4-4.0.0~rc4/internal/libyaml/testdata/reader.yaml000066400000000000000000000115221516501332500256320ustar00rootroot00000000000000# 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~rc4/internal/libyaml/testdata/scanner.yaml000066400000000000000000000174171516501332500260320ustar00rootroot00000000000000# 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 # 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 golang-go.yaml-yaml-v4-4.0.0~rc4/internal/libyaml/testdata/writer.yaml000066400000000000000000000037271516501332500257140ustar00rootroot00000000000000# 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~rc4/internal/libyaml/testdata/yaml.yaml000066400000000000000000000063531516501332500253400ustar00rootroot00000000000000# 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~rc4/internal/libyaml/testdata/yamlprivate.yaml000066400000000000000000000354031516501332500267310ustar00rootroot00000000000000# 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~rc4/internal/libyaml/writer.go000066400000000000000000000013131516501332500235330ustar00rootroot00000000000000// 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~rc4/internal/libyaml/writer_test.go000066400000000000000000000047401516501332500246010ustar00rootroot00000000000000// 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~rc4/internal/libyaml/yaml.go000066400000000000000000000660311516501332500231710ustar00rootroot00000000000000// 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" "io" "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) } 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. ) 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). ) 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 } 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). Column int // The position column (0-indexed internally, displayed as 1-indexed). } 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+1) } return builder.String() } // Node Styles type styleInt int8 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 "" } } 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. ) 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 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. ) 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 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 ) 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", } 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 } func (e *Event) ScalarStyle() ScalarStyle { return ScalarStyle(e.Style) } func (e *Event) SequenceStyle() SequenceStyle { return SequenceStyle(e.Style) } 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. ) type NodeType int // Node types. const ( // An empty node. NO_NODE NodeType = iota SCALAR_NODE // A scalar node. SEQUENCE_NODE // A sequence node. MAPPING_NODE // A mapping node. ) // NodeItem represents an element of a sequence node. type NodeItem int // NodePair represents an element of a mapping node. type NodePair struct { key int // The key of the element. value int // The value of the element. } // parserNode represents a single node in the YAML document tree. type parserNode struct { typ NodeType // The node type. tag []byte // The node tag. // The node data. // The scalar parameters (for SCALAR_NODE). scalar struct { value []byte // The scalar value. length int // The length of the scalar value. style ScalarStyle // The scalar style. } // The sequence parameters (for YAML_SEQUENCE_NODE). sequence struct { items_data []NodeItem // The stack of sequence items. style SequenceStyle // The sequence style. } // The mapping parameters (for MAPPING_NODE). mapping struct { pairs_data []NodePair // The stack of mapping pairs (key, value). pairs_start *NodePair // The beginning of the stack. pairs_end *NodePair // The end of the stack. pairs_top *NodePair // The top of the stack. style MappingStyle // The mapping style. } start_mark Mark // The beginning of the node. end_mark Mark // The end of the node. } // Document structure. type Document struct { // The document nodes. nodes []parserNode // The version directive. version_directive *VersionDirective // The list of tag directives. tag_directives_data []TagDirective tag_directives_start int // The beginning of the tag directives list. tag_directives_end int // The end of the tag directives list. start_implicit int // Is the document start indicator implicit? end_implicit int // Is the document end indicator implicit? // The start/end of the document. start_mark, end_mark Mark } // ReadHandler is called when the [Parser] needs to read more bytes from the // source. The handler should write not more than size bytes to the buffer. // The number of written bytes should be set to the size_read variable. // // [in,out] data A pointer to an application data specified by // // yamlParser.setInput(). // // [out] buffer The buffer to write the data from the source. // [in] size The size of the buffer. // [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 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. ) 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. } // 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 // 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. // 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. document *Document // The currently parsed document. } 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 } // Emitter Definitions // 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 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. document *Document // The currently emitted document. } golang-go.yaml-yaml-v4-4.0.0~rc4/internal/libyaml/yaml_test.go000066400000000000000000000061051516501332500242240ustar00rootroot00000000000000// 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~rc4/internal/libyaml/yamldatatest_loader.go000066400000000000000000000117661516501332500262560ustar00rootroot00000000000000// Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // YAML test data loading utilities. // Provides helper functions for loading and processing YAML test data, // including scalar coercion. package libyaml import ( "errors" "fmt" "io" "strings" ) // coerceScalar converts a YAML scalar string to an appropriate Go type func coerceScalar(value string) any { // Try bool and null switch value { case "true": return true case "false": return false case "null": return nil } // Try hex int (0x or 0X prefix) - needed for test data byte arrays var intVal int if _, err := fmt.Sscanf(strings.ToLower(value), "0x%x", &intVal); err == nil { return intVal } // Try float (must check before int because %d will parse "1.5" as "1") if strings.Contains(value, ".") { var floatVal float64 if _, err := fmt.Sscanf(value, "%f", &floatVal); err == nil { return floatVal } } // Try decimal int - use int64 to handle large values on 32-bit systems var int64Val int64 if _, err := fmt.Sscanf(value, "%d", &int64Val); err == nil { // Return as int if it fits, otherwise int64 if int64Val == int64(int(int64Val)) { return int(int64Val) } return int64Val } // Default to string return value } // LoadYAML parses YAML data using the native libyaml Parser. // This function is exported so it can be used by other packages for data-driven testing. // It returns a generic interface{} which is typically: // - map[string]interface{} for YAML mappings // - []interface{} for YAML sequences // - scalar values, resolved according to the following rules: // - Booleans: "true" and "false" are returned as bool (true/false). // - Nulls: "null" is returned as nil. // - Floats: values containing "." are parsed as float64. // - Decimal integers: values matching integer format are parsed as int. // - All other values are returned as string. // // This scalar resolution behavior matches the implementation in coerceScalar. func LoadYAML(data []byte) (any, error) { parser := NewParser() parser.SetInputString(data) defer parser.Delete() type stackEntry struct { container any // map[string]interface{} or []interface{} key string // for maps: current key waiting for value } var stack []stackEntry var root any for { var event Event if err := parser.Parse(&event); err != nil { if errors.Is(err, io.EOF) { break } return nil, err } switch event.Type { case STREAM_END_EVENT: // End of stream, we're done return root, nil case STREAM_START_EVENT, DOCUMENT_START_EVENT: // Structural markers, no action needed case MAPPING_START_EVENT: newMap := make(map[string]any) stack = append(stack, stackEntry{container: newMap}) case MAPPING_END_EVENT: if len(stack) > 0 { popped := stack[len(stack)-1] stack = stack[:len(stack)-1] // Add completed map to parent or set as root if len(stack) == 0 { root = popped.container } else { parent := &stack[len(stack)-1] if m, ok := parent.container.(map[string]any); ok { m[parent.key] = popped.container parent.key = "" // Reset key after use } else if s, ok := parent.container.([]any); ok { parent.container = append(s, popped.container) } } } case SEQUENCE_START_EVENT: newSlice := make([]any, 0) stack = append(stack, stackEntry{container: newSlice}) case SEQUENCE_END_EVENT: if len(stack) > 0 { popped := stack[len(stack)-1] stack = stack[:len(stack)-1] // Add completed slice to parent or set as root if len(stack) == 0 { root = popped.container } else { parent := &stack[len(stack)-1] if m, ok := parent.container.(map[string]any); ok { m[parent.key] = popped.container parent.key = "" // Reset key after use } else if s, ok := parent.container.([]any); ok { parent.container = append(s, popped.container) } } } case SCALAR_EVENT: value := string(event.Value) // Only coerce plain (unquoted) scalars isQuoted := ScalarStyle(event.Style) != PLAIN_SCALAR_STYLE if len(stack) == 0 { // Scalar at root level if isQuoted { root = value } else { root = coerceScalar(value) } } else { parent := &stack[len(stack)-1] if m, ok := parent.container.(map[string]any); ok { if parent.key == "" { // This scalar is a key - keep as string, don't coerce parent.key = value } else { // This scalar is a value if isQuoted { m[parent.key] = value } else { m[parent.key] = coerceScalar(value) } parent.key = "" } } else if s, ok := parent.container.([]any); ok { // Add to sequence if isQuoted { parent.container = append(s, value) } else { parent.container = append(s, coerceScalar(value)) } } } case DOCUMENT_END_EVENT: // Document end marker, continue processing case ALIAS_EVENT, TAIL_COMMENT_EVENT: // For now, skip aliases and comments (not used in test data) } } return root, nil } golang-go.yaml-yaml-v4-4.0.0~rc4/internal/libyaml/yamldatatest_test.go000066400000000000000000000634261516501332500257670ustar00rootroot00000000000000// 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" "runtime" "testing" "go.yaml.in/yaml/v4/internal/testutil/assert" "go.yaml.in/yaml/v4/internal/testutil/datatest" ) // 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"` Want any `yaml:"want"` 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) } // 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 local LoadYAML from yamldatatest_loader.go rawData, err := LoadYAML(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 golang-go.yaml-yaml-v4-4.0.0~rc4/internal/libyaml/yamlprivate.go000066400000000000000000000162611516501332500245640ustar00rootroot00000000000000// 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 // Internal constants and buffer sizes. // Defines buffer sizes, stack sizes, and other internal configuration // constants for libyaml. package libyaml 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 ) // 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) } 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) } // 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 } golang-go.yaml-yaml-v4-4.0.0~rc4/internal/libyaml/yamlprivate_test.go000066400000000000000000000050661516501332500256240ustar00rootroot00000000000000// Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Tests for internal constants. // Verifies buffer size and configuration constant values. package libyaml import ( "testing" "go.yaml.in/yaml/v4/internal/testutil/assert" ) func TestYAMLPrivate(t *testing.T) { RunTestCases(t, "yamlprivate.yaml", map[string]TestHandler{ "char-predicate": runCharPredicateTest, "char-convert": runCharConvertTest, }) } 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~rc4/internal/testutil/000077500000000000000000000000001516501332500221165ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc4/internal/testutil/assert/000077500000000000000000000000001516501332500234175ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc4/internal/testutil/assert/assert.go000066400000000000000000000157471516501332500252650ustar00rootroot00000000000000// 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" ) 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) } } 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~rc4/internal/testutil/assert/assert_test.go000066400000000000000000000177401516501332500263170ustar00rootroot00000000000000// 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~rc4/internal/testutil/datatest/000077500000000000000000000000001516501332500237275ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc4/internal/testutil/datatest/constants.go000066400000000000000000000050571516501332500263010ustar00rootroot00000000000000// 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~rc4/internal/testutil/datatest/converters.go000066400000000000000000000077361516501332500264650ustar00rootroot00000000000000// 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~rc4/internal/testutil/datatest/helpers.go000066400000000000000000000153551516501332500257310ustar00rootroot00000000000000// 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") } 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 } 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~rc4/internal/testutil/datatest/loader.go000066400000000000000000000274511516501332500255350ustar00rootroot00000000000000// 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~rc4/internal/testutil/datatest/registry.go000066400000000000000000000122711516501332500261310ustar00rootroot00000000000000// 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~rc4/internal/testutil/datatest/runner.go000066400000000000000000000104771516501332500256000ustar00rootroot00000000000000// 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~rc4/loader.go000066400000000000000000000152151516501332500202260ustar00rootroot00000000000000// 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 yaml import ( "bytes" "errors" "io" "reflect" "go.yaml.in/yaml/v4/internal/libyaml" ) // Load decodes 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 decoded 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 decoded values should be compatible with the respective // values in out. If one or more values cannot be decoded 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 := libyaml.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) } // loadAll loads all documents into a slice func loadAll(in []byte, out any, opts *libyaml.Options) error { outVal := reflect.ValueOf(out) if outVal.Kind() != reflect.Pointer || outVal.IsNil() { return &LoadErrors{Errors: []*libyaml.ConstructError{{ Err: errors.New("yaml: WithAllDocuments requires a non-nil pointer to a slice"), }}} } sliceVal := outVal.Elem() if sliceVal.Kind() != reflect.Slice { return &LoadErrors{Errors: []*libyaml.ConstructError{{ Err: errors.New("yaml: WithAllDocuments requires a pointer to a slice"), }}} } // Create a new slice (clear existing content) sliceVal.Set(reflect.MakeSlice(sliceVal.Type(), 0, 0)) l, err := NewLoader(bytes.NewReader(in), func(o *libyaml.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 decoded element to slice sliceVal.Set(reflect.Append(sliceVal, elemPtr.Elem())) } return nil } // loadSingle loads exactly one document (strict) func loadSingle(in []byte, out any, opts *libyaml.Options) error { l, err := NewLoader(bytes.NewReader(in), func(o *libyaml.Options) error { *o = *opts // Copy options return nil }) if err != nil { return err } // Load first document err = l.Load(out) if err == io.EOF { return &LoadErrors{Errors: []*libyaml.ConstructError{{ Err: errors.New("yaml: no documents in stream"), }}} } if err != nil { return err } // 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 return &LoadErrors{Errors: []*libyaml.ConstructError{{ Err: errors.New("yaml: expected single document, found multiple"), }}} } return nil } // A Loader reads and decodes YAML values from an input stream with configurable // options. type Loader struct { composer *libyaml.Composer decoder *libyaml.Constructor opts *libyaml.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 := libyaml.ApplyOptions(opts...) if err != nil { return nil, err } c := libyaml.NewComposerFromReader(r) c.SetStreamNodes(o.StreamNodes) return &Loader{ composer: c, decoder: libyaml.NewConstructor(o), opts: o, }, nil } // 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.opts.SingleDocument && l.docCount > 0 { return io.EOF } node := l.composer.Parse() // *libyaml.Node if node == nil { return io.EOF } l.docCount++ out := reflect.ValueOf(v) if out.Kind() == reflect.Pointer && !out.IsNil() { out = out.Elem() } l.decoder.Construct(node, out) // Pass libyaml.Node directly if len(l.decoder.TypeErrors) > 0 { typeErrors := l.decoder.TypeErrors l.decoder.TypeErrors = nil return &LoadErrors{Errors: typeErrors} } return nil } golang-go.yaml-yaml-v4-4.0.0~rc4/loader_test.go000066400000000000000000000171241516501332500212660ustar00rootroot00000000000000// Tests for the streaming Loader API, including StreamNode functionality // and multi-document streaming. package yaml_test import ( "bytes" "io" "testing" "go.yaml.in/yaml/v4" "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 := yaml.NewLoader(bytes.NewReader(input), yaml.WithStreamNodes()) assert.NoError(t, err) var nodes []yaml.Node for { var node yaml.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, yaml.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 := yaml.NewLoader(bytes.NewReader(input), yaml.WithStreamNodes()) assert.NoError(t, err) var nodes []yaml.Node for { var node yaml.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, yaml.StreamNode, nodes[0].Kind) assert.Equal(t, yaml.DocumentNode, nodes[1].Kind) assert.Equal(t, yaml.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 := yaml.NewLoader(bytes.NewReader(input), yaml.WithStreamNodes()) assert.NoError(t, err) var nodes []yaml.Node for { var node yaml.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, yaml.StreamNode, nodes[0].Kind) assert.Equal(t, yaml.DocumentNode, nodes[1].Kind) assert.Equal(t, yaml.StreamNode, nodes[2].Kind) assert.Equal(t, yaml.DocumentNode, nodes[3].Kind) assert.Equal(t, yaml.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 := yaml.NewLoader(bytes.NewReader(input), yaml.WithStreamNodes()) assert.NoError(t, err) var nodes []yaml.Node for { var node yaml.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, yaml.StreamNode, nodes[0].Kind) // Encoding should be set (non-zero) if nodes[0].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.Version != nil { assert.Equal(t, 1, streamNode.Version.Major) assert.Equal(t, 1, streamNode.Version.Minor) } if len(streamNode.TagDirectives) > 0 { found := false for _, td := range streamNode.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 := yaml.NewLoader(bytes.NewReader(input), yaml.WithStreamNodes()) assert.NoError(t, err) var node yaml.Node err = loader.Load(&node) assert.NoError(t, err) // First node should be a StreamNode with encoding assert.Equal(t, yaml.StreamNode, node.Kind) // Encoding should be set (non-zero) if node.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 := yaml.NewLoader(bytes.NewReader(input)) assert.NoError(t, err) var nodes []yaml.Node for { var node yaml.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, yaml.DocumentNode, nodes[0].Kind) assert.Equal(t, yaml.DocumentNode, nodes[1].Kind) } // TestStreamNodeDisabled tests explicitly disabling stream nodes func TestStreamNodeDisabled(t *testing.T) { input := []byte("key: value\n") loader, err := yaml.NewLoader(bytes.NewReader(input), yaml.WithStreamNodes(false)) assert.NoError(t, err) var node yaml.Node err = loader.Load(&node) assert.NoError(t, err) // Should get a DocumentNode, not a StreamNode assert.Equal(t, yaml.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 := yaml.Load(input, &configs, yaml.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 := yaml.Load(input, &docs, yaml.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 := yaml.Load(input, &docs, yaml.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 := yaml.Load(input, &single, yaml.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 := yaml.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 := yaml.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 := yaml.Load(input, &config) assert.NotNil(t, err) assert.ErrorMatches(t, ".*expected single document, found multiple.*", err) } golang-go.yaml-yaml-v4-4.0.0~rc4/node_test.go000066400000000000000000000452401516501332500207450ustar00rootroot00000000000000// 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, "yaml: cannot represent node with unknown kind 0", err) err = n.Load(&v) assert.ErrorMatches(t, "yaml: 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, "yaml: 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.LoadYAML) }, 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.V4) assert.NoError(t, err) assert.Equal(t, yaml.MappingNode, node1.Kind) // Dump with V3 var node2 yaml.Node err = node2.Dump(value, yaml.V3) 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~rc4/testdata/000077500000000000000000000000001516501332500202365ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc4/testdata/decode.yaml000066400000000000000000000617031516501332500223540ustar00rootroot00000000000000# 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 golang-go.yaml-yaml-v4-4.0.0~rc4/testdata/encode.yaml000066400000000000000000000447171516501332500223740ustar00rootroot00000000000000# 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~rc4/testdata/fuzz_crashers.yaml000066400000000000000000000045151516501332500240170ustar00rootroot00000000000000# 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~rc4/testdata/fuzz_json_roundtrip.yaml000066400000000000000000000032461516501332500252640ustar00rootroot00000000000000# 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~rc4/testdata/limit.yaml000066400000000000000000000101561516501332500222430ustar00rootroot00000000000000# 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: 'yaml: document contains excessive aliasing' - limit-error: name: 1000kb of deeply nested slices data: loop: ['[', 1024000] # 1000*1024 want: 'yaml: while increasing flow level at line 1, column 10001: exceeded max depth of 10000' - limit-error: name: 1000kb of deeply nested maps data: join: - text: 'x: ' - loop: ['{', 1024000] # 1000*1024 want: 'yaml: while increasing flow level at line 1, column 10004: exceeded max depth of 10000' - limit-error: name: 1000kb of deeply nested indents data: loop: ['- ', 1024000] # 1000*1024 want: 'yaml: while increasing indent level at line 1: line 1, column 20001: 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~rc4/testdata/node.yaml000066400000000000000000001517751516501332500220670ustar00rootroot00000000000000# 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 - verifies !!merge tag is preserved in output - node-test: name: merge key with flow mapping value yaml: "!!merge <<: {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 !!merge <<: {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'} golang-go.yaml-yaml-v4-4.0.0~rc4/testdata/parser_events.yaml000066400000000000000000000027461516501332500240130ustar00rootroot00000000000000# 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~rc4/testdata/stream.yaml000066400000000000000000000056331516501332500224240ustar00rootroot00000000000000# 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) # encoding - Encoding name: UTF-8, UTF-16LE, UTF-16BE (for Stream nodes) # version - Version string like "1.1" (for Stream nodes) # tag-directives - List of tag directive objects (for Stream nodes) # Empty stream returns single StreamNode - stream-test: name: empty stream yaml: '' want: - kind: Stream encoding: UTF-8 # Single document returns [Stream, Doc, Stream] - stream-test: name: single document yaml: 'key: value' want: - kind: Stream encoding: UTF-8 - kind: Document - kind: Stream encoding: UTF-8 # Multiple documents return interleaved pattern - stream-test: name: multi document yaml: | --- key1: value1 --- key2: value2 want: - kind: Stream encoding: UTF-8 - kind: Document - kind: Stream encoding: UTF-8 - kind: Document - kind: 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 encoding: UTF-8 version: "1.1" tag-directives: - handle: "!" prefix: "tag:example.com,2000:app/" - kind: Document - kind: Stream encoding: UTF-8 # Encoding is set on all StreamNodes - stream-test: name: encoding check yaml: 'key: value' want: - kind: Stream encoding: UTF-8 - kind: Document - kind: 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~rc4/testdata/unmarshal_errors.yaml000066400000000000000000000141541516501332500245150ustar00rootroot00000000000000# 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: 'yaml: cannot construct !!str `error` as a !!float' - unmarshal-error: name: Incomplete flow sequence yaml: 'v: [A,' want: 'yaml: while parsing a flow node at line 1: did not find expected node content' - unmarshal-error: name: Incomplete flow sequence in list yaml: | v: - [A, want: 'yaml: while parsing a flow node at line 2: did not find expected node content' - unmarshal-error: name: Invalid alias syntax yaml: | a: - b: *, want: 'yaml: while scanning an alias at line 2, column 6: line 2, column 7: did not find expected alphabetic or numeric character' - unmarshal-error: name: Unknown anchor referenced yaml: "a: *b\n" want: "yaml: line 1, column 5: unknown anchor 'b' referenced" - unmarshal-error: name: Anchor contains itself yaml: | a: &a b: *a want: "yaml: anchor 'a' value contains itself" - unmarshal-error: name: Block sequence in invalid context yaml: 'value: -' want: 'yaml: line 1, column 8: block sequence entries are not allowed in this context' - unmarshal-error: name: Invalid base64 in binary yaml: 'a: !!binary ==' want: 'yaml: !!binary value contains invalid base64 data' - unmarshal-error: name: Slice as map key yaml: '{[.]}' want: 'yaml: cannot use ''[]interface {}{"."}'' as a map key; try decoding into yaml.Node' - unmarshal-error: name: Map as map key yaml: '{{.}}' want: 'yaml: 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: "yaml: line 1, column 5: unknown anchor 'a' referenced" - unmarshal-error: name: Invalid TAG directive yaml: | %TAG !%79! tag:yaml.org,2002: --- v: !%79!int '1' want: 'yaml: while scanning a %TAG directive at line 1: line 1, column 7: did not find expected whitespace' - unmarshal-error: name: Missing colon in mapping (multiline) yaml: | a: 1: b 2: want: "yaml: line 4, column 4: 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: "yaml: while scanning a simple key at line 3: line 4: could not find expected ':'" - unmarshal-error: name: Missing colon after comment and list (Issue 665) yaml: | # - { want: "yaml: while parsing a flow node at line 3: did not find expected node content" - unmarshal-error: name: Incomplete UTF-8 sequence (Issue 666) yaml: "0: [:!00 \xEF" want: "yaml: while parsing a flow sequence at : line 1: did not find expected ',' or ']'" - unmarshal-error: name: Anchor with colon (Issue 109) yaml: | foo: &bar: bar *bar: : quz want: 'yaml: line 1, column 10: 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: 'yaml: 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~rc4/util/000077500000000000000000000000001516501332500174025ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc4/util/check-commit-messages000077500000000000000000000065121516501332500235040ustar00rootroot00000000000000#!/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)") # If we add more checks in this loop, we need this for multiple errors: [[ ${error_lines[0]} == "$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~rc4/util/common.bash000066400000000000000000000015051516501332500215320ustar00rootroot00000000000000# 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~rc4/util/test-count000077500000000000000000000035631516501332500214440ustar00rootroot00000000000000#!/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~rc4/util/yaml-test-suite000077500000000000000000000016471516501332500224060ustar00rootroot00000000000000#!/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~rc4/yaml.go000066400000000000000000000540051516501332500177220ustar00rootroot00000000000000// 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 presets (V2, V3, V4) // - Options API (WithIndent, WithKnownFields, etc.) // - Type and constant re-exports from internal/libyaml // - Helper functions for struct field handling // - Classic APIs (Decoder, Encoder, Unmarshal, Marshal) // // For the main API, see: // - loader.go: Load, Loader // - dumper.go: Dump, Dumper package yaml import ( "errors" "fmt" "io" "reflect" "strings" "sync" "go.yaml.in/yaml/v4/internal/libyaml" ) //----------------------------------------------------------------------------- // Version presets //----------------------------------------------------------------------------- // Usage: // yaml.Dump(&data, yaml.V3) // yaml.Dump(&data, yaml.V3, yaml.WithIndent(2), yaml.WithCompactSeqIndent()) // V2 defaults: var V2 = Options( WithIndent(2), WithCompactSeqIndent(false), WithLineWidth(80), WithUnicode(true), WithUniqueKeys(true), WithQuotePreference(QuoteLegacy), ) // V3 defaults: var V3 = Options( WithIndent(4), WithCompactSeqIndent(false), WithLineWidth(80), WithUnicode(true), WithUniqueKeys(true), WithQuotePreference(QuoteLegacy), ) // V4 defaults: var V4 = Options( WithIndent(2), WithCompactSeqIndent(true), WithLineWidth(80), WithUnicode(true), WithUniqueKeys(true), WithQuotePreference(QuoteSingle), ) //----------------------------------------------------------------------------- // Options //----------------------------------------------------------------------------- // Option allows configuring YAML loading and dumping operations. // Re-exported from internal/libyaml. type Option = libyaml.Option var ( // WithIndent sets indentation spaces (2-9). // See internal/libyaml.WithIndent. WithIndent = libyaml.WithIndent // WithCompactSeqIndent configures '- ' as part of indentation. // See internal/libyaml.WithCompactSeqIndent. WithCompactSeqIndent = libyaml.WithCompactSeqIndent // WithKnownFields enables strict field checking during loading. // See internal/libyaml.WithKnownFields. WithKnownFields = libyaml.WithKnownFields // WithSingleDocument only processes first document in stream. // See internal/libyaml.WithSingleDocument. WithSingleDocument = libyaml.WithSingleDocument // WithStreamNodes enables stream boundary nodes when loading. // See internal/libyaml.WithStreamNodes. WithStreamNodes = libyaml.WithStreamNodes // WithAllDocuments enables multi-document mode for Load and Dump. // See internal/libyaml.WithAllDocuments. WithAllDocuments = libyaml.WithAllDocuments // WithLineWidth sets preferred line width for output. // See internal/libyaml.WithLineWidth. WithLineWidth = libyaml.WithLineWidth // WithUnicode controls non-ASCII characters in output. // See internal/libyaml.WithUnicode. WithUnicode = libyaml.WithUnicode // WithUniqueKeys enables duplicate key detection. // See internal/libyaml.WithUniqueKeys. WithUniqueKeys = libyaml.WithUniqueKeys // WithCanonical forces canonical YAML output format. // See internal/libyaml.WithCanonical. WithCanonical = libyaml.WithCanonical // WithLineBreak sets line ending style for output. // See internal/libyaml.WithLineBreak. WithLineBreak = libyaml.WithLineBreak // WithExplicitStart controls document start markers (---). // See internal/libyaml.WithExplicitStart. WithExplicitStart = libyaml.WithExplicitStart // WithExplicitEnd controls document end markers (...). // See internal/libyaml.WithExplicitEnd. WithExplicitEnd = libyaml.WithExplicitEnd // WithFlowSimpleCollections controls flow style for simple collections. // See internal/libyaml.WithFlowSimpleCollections. WithFlowSimpleCollections = libyaml.WithFlowSimpleCollections // WithQuotePreference sets preferred quote style when quoting is required. // See internal/libyaml.WithQuotePreference. 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.V4, yaml.WithIndent(3)) // yaml.Dump(&data, opts) func Options(opts ...Option) Option { return libyaml.CombineOptions(opts...) } // 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) // // 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 // `) // 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"` } 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)") } } return Options(optList...), nil } //----------------------------------------------------------------------------- // Type and constant re-exports //----------------------------------------------------------------------------- type ( // Node represents a YAML node in the document tree. // See internal/libyaml.Node. Node = libyaml.Node // Kind identifies the type of a YAML node. // See internal/libyaml.Kind. Kind = libyaml.Kind // Style controls the presentation of a YAML node. // See internal/libyaml.Style. Style = libyaml.Style // Marshaler is implemented by types with custom YAML marshaling. // See internal/libyaml.Marshaler. Marshaler = libyaml.Marshaler // IsZeroer is implemented by types that can report if they're zero. // See internal/libyaml.IsZeroer. IsZeroer = libyaml.IsZeroer ) // Unmarshaler is the interface implemented by types // that can unmarshal a YAML description of themselves. type Unmarshaler interface { UnmarshalYAML(node *Node) error } // Re-export stream-related types type ( VersionDirective = libyaml.StreamVersionDirective TagDirective = libyaml.StreamTagDirective Encoding = libyaml.Encoding ) // Re-export encoding constants const ( EncodingAny = libyaml.ANY_ENCODING EncodingUTF8 = libyaml.UTF8_ENCODING EncodingUTF16LE = libyaml.UTF16LE_ENCODING EncodingUTF16BE = libyaml.UTF16BE_ENCODING ) // Re-export error types 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 a descriptive message. LoadError = libyaml.ConstructError // 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 // TypeError is an obsolete error type retained for compatibility. // // Deprecated: Use [LoadErrors] instead. // //nolint:staticcheck // we are using deprecated TypeError for compatibility TypeError = libyaml.TypeError ) // Re-export Kind constants const ( DocumentNode = libyaml.DocumentNode SequenceNode = libyaml.SequenceNode MappingNode = libyaml.MappingNode ScalarNode = libyaml.ScalarNode AliasNode = libyaml.AliasNode StreamNode = libyaml.StreamNode ) // Re-export Style constants const ( TaggedStyle = libyaml.TaggedStyle DoubleQuotedStyle = libyaml.DoubleQuotedStyle SingleQuotedStyle = libyaml.SingleQuotedStyle LiteralStyle = libyaml.LiteralStyle FoldedStyle = libyaml.FoldedStyle FlowStyle = libyaml.FlowStyle ) // 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 ) //----------------------------------------------------------------------------- // Helper functions //----------------------------------------------------------------------------- // The code in this section was copied from mgo/bson. var ( structMap = make(map[reflect.Type]*structInfo) fieldMapMutex sync.RWMutex unmarshalerType reflect.Type ) // structInfo holds details for the serialization of fields of // a given struct. 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 // InlineUnmarshalers holds indexes to inlined fields that // contain unmarshaler values. InlineUnmarshalers [][]int } 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 } 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 inlineUnmarshalers := [][]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") } if reflect.PointerTo(ftype).Implements(unmarshalerType) { inlineUnmarshalers = append(inlineUnmarshalers, []int{i}) } else { sinfo, err := getStructInfo(ftype) if err != nil { return nil, err } for _, index := range sinfo.InlineUnmarshalers { inlineUnmarshalers = append(inlineUnmarshalers, 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, InlineUnmarshalers: inlineUnmarshalers, } fieldMapMutex.Lock() structMap[st] = sinfo fieldMapMutex.Unlock() return sinfo, nil } var noWriter io.Writer func handleErr(err *error) { if v := recover(); v != nil { if e, ok := v.(*libyaml.YAMLError); ok { *err = e.Err } else { panic(v) } } } //----------------------------------------------------------------------------- // Classic APIs //----------------------------------------------------------------------------- // A Decoder reads and decodes YAML values from an input stream. type Decoder struct { composer *libyaml.Composer knownFields bool } // 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 { return &Decoder{ composer: libyaml.NewComposerFromReader(r), } } // 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.knownFields = 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) (err error) { d := libyaml.NewConstructor(libyaml.DefaultOptions) d.KnownFields = dec.knownFields defer handleErr(&err) node := dec.composer.Parse() if node == nil { return io.EOF } out := reflect.ValueOf(v) if out.Kind() == reflect.Pointer && !out.IsNil() { out = out.Elem() } d.Construct(node, out) if len(d.TypeErrors) > 0 { return &LoadErrors{Errors: d.TypeErrors} } return nil } // An Encoder writes YAML values to an output stream. type Encoder struct { encoder *libyaml.Representer } // 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 { return &Encoder{ encoder: libyaml.NewRepresenter(w, libyaml.DefaultOptions), } } // 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) (err error) { defer handleErr(&err) e.encoder.MarshalDoc("", reflect.ValueOf(v)) return nil } // SetIndent changes the used indentation used when encoding. func (e *Encoder) SetIndent(spaces int) { if spaces < 0 { panic("yaml: cannot indent to a negative number of spaces") } e.encoder.Indent = spaces } // CompactSeqIndent makes it so that '- ' is considered part of the indentation. func (e *Encoder) CompactSeqIndent() { e.encoder.Emitter.CompactSequenceIndent = true } // DefaultSeqIndent makes it so that '- ' is not considered part of the indentation. func (e *Encoder) DefaultSeqIndent() { e.encoder.Emitter.CompactSequenceIndent = false } // Close closes the encoder by writing any remaining data. // It does not write a stream terminating string "...". func (e *Encoder) Close() (err error) { defer handleErr(&err) e.encoder.Finish() return nil } // 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 unmarshal(in, out, V3) } func unmarshal(in []byte, out any, opts ...Option) (err error) { defer handleErr(&err) o, err := libyaml.ApplyOptions(opts...) if err != nil { return err } // Check if out implements yaml.Unmarshaler if u, ok := out.(Unmarshaler); ok { p := libyaml.NewComposer(in) defer p.Destroy() node := p.Parse() if node != nil { return u.UnmarshalYAML(node) } return nil } return libyaml.Construct(in, out, o) } // 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) { defer handleErr(&err) e := libyaml.NewRepresenter(noWriter, libyaml.DefaultOptions) defer e.Destroy() e.MarshalDoc("", reflect.ValueOf(in)) e.Finish() out = e.Out return out, err } golang-go.yaml-yaml-v4-4.0.0~rc4/yaml_test.go000066400000000000000000002266701516501332500207720ustar00rootroot00000000000000// 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 } ) // 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{}) // 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)}, }, // 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.LoadYAML) }, 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, `yaml: offset 0: 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, "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.LoadYAML) }, 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.LoadYAML) }, 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.ScannerError assert.ErrorAs(t, err, &asErr) expectedErr := libyaml.ScannerError{ ContextMark: libyaml.Mark{ Index: 5, Line: 2, Column: 0, }, ContextMessage: "while scanning a simple key", Mark: libyaml.Mark{ Index: 7, Line: 3, Column: 0, }, 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.ScannerError assert.ErrorAs(t, err, &asErr) expectedErr := libyaml.ScannerError{ Mark: libyaml.Mark{ Index: 7, Line: 1, Column: 7, }, 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 obsoleteUnmarshalerType struct { value any } func (o *obsoleteUnmarshalerType) 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 obsoleteUnmarshalerPointer struct { Field *obsoleteUnmarshalerType `yaml:"_"` } type obsoleteUnmarshalerValue struct { Field obsoleteUnmarshalerType `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 := &obsoleteUnmarshalerPointer{} 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 := &obsoleteUnmarshalerValue{} 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 := &obsoleteUnmarshalerType{} 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 TestUnmarshalerLoadErrors(t *testing.T) { unmarshalerResult[2] = &yaml.LoadErrors{Errors: []*yaml.LoadError{{Err: errors.New("foo"), Line: 1, Column: 1}}} unmarshalerResult[4] = &yaml.LoadErrors{Errors: []*yaml.LoadError{{Err: errors.New("bar"), 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:\n" + " line 1: cannot construct !!str `A` into int\n" + " line 1: foo\n" + " line 1: bar\n" + " 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 TestObsoleteUnmarshalerLoadErrors(t *testing.T) { unmarshalerResult[2] = &yaml.LoadErrors{Errors: []*yaml.LoadError{{Err: errors.New("foo"), Line: 1, Column: 1}}} unmarshalerResult[4] = &yaml.LoadErrors{Errors: []*yaml.LoadError{{Err: errors.New("bar"), Line: 1, Column: 1}}} defer func() { delete(unmarshalerResult, 2) delete(unmarshalerResult, 4) }() type T struct { Before int After int M map[string]*obsoleteUnmarshalerType } 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:\n" + " line 1: cannot construct !!str `A` into int\n" + " line 1: foo\n" + " line 1: bar\n" + " 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.LoadError{ Line: 1, Column: 2, Err: errSentinel, } errUnmarshal2 := &yaml.LoadError{ Line: 2, Column: 2, Err: 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.LoadError{ Line: 1, Column: 2, Err: errSentinel, } errUnmarshal2 := &yaml.LoadError{ Line: 2, Column: 2, Err: 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:\n" + " line 1: cannot construct !!str `A` into int\n" + " line 1: cannot construct !!str `a` into int32\n" + " line 1: cannot construct !!str `b` into int64\n" + " line 1: cannot construct !!str `B` into int" assert.ErrorMatches(t, expectedError, err) } type obsoleteProxyTypeError struct{} func (v *obsoleteProxyTypeError) 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 TestObsoleteUnmarshalerLoadErrorsProxying(t *testing.T) { type T struct { Before int After int M map[string]*obsoleteProxyTypeError } var v T data := `{before: A, m: {abc: a, def: b}, after: B}` err := yaml.Unmarshal([]byte(data), &v) expectedError := "" + "yaml: construct errors:\n" + " line 1: cannot construct !!str `A` into int\n" + " line 1: cannot construct !!str `a` into int32\n" + " line 1: cannot construct !!str `b` into int64\n" + " 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{ {Line: 1, Column: 17, Err: 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 obsoleteFailingUnmarshaler struct{} func (ft *obsoleteFailingUnmarshaler) UnmarshalYAML(unmarshal func(any) error) error { return errFailing } func TestObsoleteUnmarshalerError(t *testing.T) { data := `{foo: 123, bar: {}, spam: "test"}` dst := struct { Foo int Bar *obsoleteFailingUnmarshaler Spam string }{} err := yaml.Unmarshal([]byte(data), &dst) expectedErr := &yaml.LoadErrors{ Errors: []*yaml.LoadError{ {Line: 1, Column: 17, Err: errFailing}, }, } assert.DeepEqual(t, expectedErr, err) // whatever could be unmarshaled must be unmarshaled assert.Equal(t, 123, dst.Foo) assert.DeepEqual(t, &obsoleteFailingUnmarshaler{}, 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{ {Line: 1, Column: 17, Err: 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.LoadError{ Line: 1, Column: 2, Err: 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 obsoleteSliceUnmarshaler []int func (su *obsoleteSliceUnmarshaler) 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 TestObsoleteUnmarshalerRetry(t *testing.T) { var su obsoleteSliceUnmarshaler err := yaml.Unmarshal([]byte("[1, 2, 3]"), &su) assert.NoError(t, err) assert.DeepEqual(t, obsoleteSliceUnmarshaler([]int{1, 2, 3}), su) err = yaml.Unmarshal([]byte("1"), &su) assert.NoError(t, err) assert.DeepEqual(t, obsoleteSliceUnmarshaler([]int{1}), su) } // 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:\n 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:\n 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:\n 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:\n 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:\n 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:\n 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.LoadYAML) }, 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.ParserError) assert.ErrorAs(t, err, &asErr) expected := &libyaml.ParserError{ 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.LoadYAML) }, 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, `yaml: write error: some write error`, err) // Data not flushed yet } type errorWriter struct{} func (errorWriter) Write([]byte) (int, error) { return 0, fmt.Errorf("some write error") } var marshalErrorTests = []struct { value any error string panic string }{{ 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 panic: `duplicated key 'b' in struct struct \{ B int; .*`, }, { value: &struct { A int B map[string]int `yaml:",inline"` }{1, map[string]int{"a": 2}}, panic: `cannot have key "a" in inlined map: conflicts with struct field`, }} func TestMarshalErrors(t *testing.T) { for _, item := range marshalErrorTests { t.Run(item.panic, func(t *testing.T) { if item.panic != "" { assert.PanicMatches(t, item.panic, func() { yaml.Marshal(item.value) }) } else { _, err := yaml.Marshal(item.value) assert.ErrorMatches(t, item.error, err) } }) } } 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, errFailing, err) } 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.LoadYAML) 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 TestLimits(t *testing.T) { datatest.RunTestCases(t, func() ([]map[string]any, error) { return datatest.LoadTestCasesFromFile("testdata/limit.yaml", libyaml.LoadYAML) }, 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.Equal(t, expectedError, err.Error()) 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 BenchmarkLimits(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.LoadYAML) }, 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~rc4/yts/000077500000000000000000000000001516501332500172445ustar00rootroot00000000000000golang-go.yaml-yaml-v4-4.0.0~rc4/yts/known-failing-tests000066400000000000000000000205041516501332500230730ustar00rootroot00000000000000TestYAMLSuite/27NA/UnmarshalTest TestYAMLSuite/27NA/EventComparisonTest TestYAMLSuite/229Q/JSONComparisonTest TestYAMLSuite/2JQS/UnmarshalTest TestYAMLSuite/2JQS/EventComparisonTest TestYAMLSuite/2LFX/UnmarshalTest TestYAMLSuite/2LFX/EventComparisonTest TestYAMLSuite/2SXE/UnmarshalTest TestYAMLSuite/2SXE/EventComparisonTest TestYAMLSuite/35KP/JSONComparisonTest TestYAMLSuite/3HFZ/UnmarshalTest TestYAMLSuite/3UYS/UnmarshalTest TestYAMLSuite/3UYS/EventComparisonTest TestYAMLSuite/4ABK/EventComparisonTest TestYAMLSuite/4ABK/MarshalTest TestYAMLSuite/4FJ6/UnmarshalTest TestYAMLSuite/4H7K/UnmarshalTest TestYAMLSuite/00#02/UnmarshalTest TestYAMLSuite/00#02/EventComparisonTest TestYAMLSuite/01#02/UnmarshalTest TestYAMLSuite/01#02/EventComparisonTest TestYAMLSuite/02#02/UnmarshalTest TestYAMLSuite/02#02/EventComparisonTest TestYAMLSuite/565N/JSONComparisonTest TestYAMLSuite/58MP/UnmarshalTest TestYAMLSuite/58MP/EventComparisonTest TestYAMLSuite/5MUD/UnmarshalTest TestYAMLSuite/5MUD/EventComparisonTest TestYAMLSuite/5T43/UnmarshalTest TestYAMLSuite/5T43/EventComparisonTest TestYAMLSuite/5TYM/JSONComparisonTest TestYAMLSuite/652Z/JSONComparisonTest TestYAMLSuite/6BCT/UnmarshalTest TestYAMLSuite/6BCT/EventComparisonTest TestYAMLSuite/6BFJ/UnmarshalTest TestYAMLSuite/6BFJ/EventComparisonTest TestYAMLSuite/6CA3/UnmarshalTest TestYAMLSuite/6CA3/EventComparisonTest TestYAMLSuite/6LVF/UnmarshalTest TestYAMLSuite/6LVF/EventComparisonTest TestYAMLSuite/6M2F/UnmarshalTest TestYAMLSuite/6M2F/EventComparisonTest TestYAMLSuite/6PBE/UnmarshalTest TestYAMLSuite/6WLZ/JSONComparisonTest TestYAMLSuite/6XDY/JSONComparisonTest TestYAMLSuite/6ZKB/EventComparisonTest TestYAMLSuite/6ZKB/JSONComparisonTest TestYAMLSuite/7Z25/EventComparisonTest TestYAMLSuite/7Z25/JSONComparisonTest TestYAMLSuite/8G76/JSONComparisonTest TestYAMLSuite/8XYN/UnmarshalTest TestYAMLSuite/8XYN/EventComparisonTest TestYAMLSuite/00#03/UnmarshalTest TestYAMLSuite/00#03/EventComparisonTest TestYAMLSuite/01#03/UnmarshalTest TestYAMLSuite/01#03/EventComparisonTest TestYAMLSuite/98YD/JSONComparisonTest TestYAMLSuite/9C9N/UnmarshalTest TestYAMLSuite/9C9N/EventComparisonTest TestYAMLSuite/9DXL/EventComparisonTest TestYAMLSuite/9DXL/JSONComparisonTest TestYAMLSuite/9HCY/UnmarshalTest TestYAMLSuite/9HCY/EventComparisonTest TestYAMLSuite/9JBA/UnmarshalTest TestYAMLSuite/9JBA/EventComparisonTest TestYAMLSuite/9KAX/JSONComparisonTest TestYAMLSuite/9MMW/UnmarshalTest TestYAMLSuite/01#04/JSONComparisonTest TestYAMLSuite/9SA2/UnmarshalTest TestYAMLSuite/9SA2/EventComparisonTest TestYAMLSuite/9WXW/JSONComparisonTest TestYAMLSuite/A2M4/UnmarshalTest TestYAMLSuite/A2M4/EventComparisonTest TestYAMLSuite/AVM7/JSONComparisonTest TestYAMLSuite/BEC7/UnmarshalTest TestYAMLSuite/BEC7/EventComparisonTest TestYAMLSuite/BS4K/UnmarshalTest TestYAMLSuite/C4HZ/EventComparisonTest TestYAMLSuite/CFD4/UnmarshalTest TestYAMLSuite/CFD4/EventComparisonTest TestYAMLSuite/CN3R/EventComparisonTest TestYAMLSuite/CVW2/UnmarshalTest TestYAMLSuite/CVW2/EventComparisonTest TestYAMLSuite/DBG4/JSONComparisonTest TestYAMLSuite/DFF7/MarshalTest TestYAMLSuite/DK3J/EventComparisonTest TestYAMLSuite/DK3J/MarshalTest TestYAMLSuite/DK3J/JSONComparisonTest TestYAMLSuite/00#06/UnmarshalTest TestYAMLSuite/00#06/EventComparisonTest TestYAMLSuite/01#06/UnmarshalTest TestYAMLSuite/01#06/EventComparisonTest TestYAMLSuite/01#06/JSONComparisonTest TestYAMLSuite/03#03/UnmarshalTest TestYAMLSuite/03#03/EventComparisonTest TestYAMLSuite/04#02/UnmarshalTest TestYAMLSuite/04#02/EventComparisonTest TestYAMLSuite/06/JSONComparisonTest TestYAMLSuite/07/UnmarshalTest TestYAMLSuite/07/EventComparisonTest TestYAMLSuite/EB22/UnmarshalTest TestYAMLSuite/EHF6/EventComparisonTest TestYAMLSuite/FP8R/EventComparisonTest TestYAMLSuite/FP8R/MarshalTest TestYAMLSuite/FP8R/JSONComparisonTest TestYAMLSuite/FRK4/UnmarshalTest TestYAMLSuite/FRK4/EventComparisonTest TestYAMLSuite/G4RS/EventComparisonTest TestYAMLSuite/G5U8/UnmarshalTest TestYAMLSuite/G5U8/EventComparisonTest TestYAMLSuite/HRE5/UnmarshalTest TestYAMLSuite/HRE5/EventComparisonTest TestYAMLSuite/HWV9/UnmarshalTest TestYAMLSuite/HWV9/EventComparisonTest TestYAMLSuite/02#05/EventComparisonTest TestYAMLSuite/02#05/MarshalTest TestYAMLSuite/02#05/JSONComparisonTest TestYAMLSuite/JHB9/JSONComparisonTest TestYAMLSuite/JR7V/UnmarshalTest TestYAMLSuite/JR7V/EventComparisonTest TestYAMLSuite/K3WX/UnmarshalTest TestYAMLSuite/K3WX/EventComparisonTest TestYAMLSuite/KK5P/UnmarshalTest TestYAMLSuite/KS4U/UnmarshalTest TestYAMLSuite/KSS4/JSONComparisonTest TestYAMLSuite/01#10/EventComparisonTest TestYAMLSuite/01#10/JSONComparisonTest TestYAMLSuite/L383/JSONComparisonTest TestYAMLSuite/LX3P/UnmarshalTest TestYAMLSuite/00#11/UnmarshalTest TestYAMLSuite/00#11/EventComparisonTest TestYAMLSuite/01#11/UnmarshalTest TestYAMLSuite/M5DY/UnmarshalTest TestYAMLSuite/M7A3/EventComparisonTest TestYAMLSuite/M7A3/JSONComparisonTest TestYAMLSuite/00#12/UnmarshalTest TestYAMLSuite/00#12/EventComparisonTest TestYAMLSuite/05#03/UnmarshalTest TestYAMLSuite/05#03/EventComparisonTest TestYAMLSuite/06#01/UnmarshalTest TestYAMLSuite/06#01/EventComparisonTest TestYAMLSuite/NHX8/UnmarshalTest TestYAMLSuite/NHX8/EventComparisonTest TestYAMLSuite/NJ66/UnmarshalTest TestYAMLSuite/NJ66/EventComparisonTest TestYAMLSuite/NKF9/UnmarshalTest TestYAMLSuite/NKF9/EventComparisonTest TestYAMLSuite/PUW8/JSONComparisonTest TestYAMLSuite/Q5MG/UnmarshalTest TestYAMLSuite/Q5MG/EventComparisonTest TestYAMLSuite/Q9WF/UnmarshalTest TestYAMLSuite/QB6E/UnmarshalTest TestYAMLSuite/QB6E/EventComparisonTest TestYAMLSuite/QLJ7/UnmarshalTest TestYAMLSuite/QT73/UnmarshalTest TestYAMLSuite/QT73/EventComparisonTest TestYAMLSuite/R4YG/UnmarshalTest TestYAMLSuite/R4YG/EventComparisonTest TestYAMLSuite/RHX7/UnmarshalTest TestYAMLSuite/RTP8/UnmarshalTest TestYAMLSuite/RTP8/EventComparisonTest TestYAMLSuite/RZP5/UnmarshalTest TestYAMLSuite/RZT7/JSONComparisonTest TestYAMLSuite/S3PD/UnmarshalTest TestYAMLSuite/S3PD/EventComparisonTest TestYAMLSuite/S4JQ/JSONComparisonTest TestYAMLSuite/S98Z/UnmarshalTest TestYAMLSuite/S98Z/EventComparisonTest TestYAMLSuite/SBG9/UnmarshalTest TestYAMLSuite/01#13/UnmarshalTest TestYAMLSuite/01#13/EventComparisonTest TestYAMLSuite/SU5Z/UnmarshalTest TestYAMLSuite/SU5Z/EventComparisonTest TestYAMLSuite/U9NS/JSONComparisonTest TestYAMLSuite/UGM3/JSONComparisonTest TestYAMLSuite/00#14/UnmarshalTest TestYAMLSuite/00#14/EventComparisonTest TestYAMLSuite/UT92/UnmarshalTest TestYAMLSuite/UT92/EventComparisonTest TestYAMLSuite/V9D5/UnmarshalTest TestYAMLSuite/01#15/UnmarshalTest TestYAMLSuite/01#15/EventComparisonTest TestYAMLSuite/W4TN/UnmarshalTest TestYAMLSuite/W4TN/EventComparisonTest TestYAMLSuite/W5VH/UnmarshalTest TestYAMLSuite/W5VH/EventComparisonTest TestYAMLSuite/WZ62/UnmarshalTest TestYAMLSuite/WZ62/EventComparisonTest TestYAMLSuite/X38W/UnmarshalTest TestYAMLSuite/X38W/EventComparisonTest TestYAMLSuite/X4QW/UnmarshalTest TestYAMLSuite/X4QW/EventComparisonTest TestYAMLSuite/XW4D/UnmarshalTest TestYAMLSuite/Y2GN/EventComparisonTest TestYAMLSuite/Y2GN/JSONComparisonTest TestYAMLSuite/001/UnmarshalTest TestYAMLSuite/001/EventComparisonTest TestYAMLSuite/003/UnmarshalTest TestYAMLSuite/003/EventComparisonTest TestYAMLSuite/003/MarshalTest TestYAMLSuite/004/MarshalTest TestYAMLSuite/005/MarshalTest TestYAMLSuite/006/MarshalTest TestYAMLSuite/007/MarshalTest TestYAMLSuite/008/MarshalTest TestYAMLSuite/009/MarshalTest TestYAMLSuite/010/UnmarshalTest TestYAMLSuite/010/EventComparisonTest TestYAMLSuite/YJV2/UnmarshalTest TestYAMLSuite/YJV2/EventComparisonTest TestYAMLSuite/2AUY/JSONComparisonTest TestYAMLSuite/33X3/JSONComparisonTest TestYAMLSuite/4RWC/JSONComparisonTest TestYAMLSuite/6SLA/JSONComparisonTest TestYAMLSuite/74H7/JSONComparisonTest TestYAMLSuite/9U5K/JSONComparisonTest TestYAMLSuite/AZ63/JSONComparisonTest TestYAMLSuite/C4HZ/JSONComparisonTest TestYAMLSuite/DHP8/JSONComparisonTest TestYAMLSuite/05#02/JSONComparisonTest TestYAMLSuite/F2C7/JSONComparisonTest TestYAMLSuite/H2RW/JSONComparisonTest TestYAMLSuite/J7PZ/JSONComparisonTest TestYAMLSuite/J7VC/JSONComparisonTest TestYAMLSuite/K4SU/JSONComparisonTest TestYAMLSuite/KMK3/JSONComparisonTest TestYAMLSuite/L94M/JSONComparisonTest TestYAMLSuite/M6YH/JSONComparisonTest TestYAMLSuite/RLU9/JSONComparisonTest TestYAMLSuite/RR7F/JSONComparisonTest TestYAMLSuite/SYW4/JSONComparisonTest TestYAMLSuite/UGM3/MarshalTest TestYAMLSuite/YD5X/JSONComparisonTest TestYAMLSuite/ZF4X/JSONComparisonTest TestYAMLSuite/ZWK4/JSONComparisonTest golang-go.yaml-yaml-v4-4.0.0~rc4/yts/test_suite_test.go000066400000000000000000000172571516501332500230360ustar00rootroot00000000000000// 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 test-data' to download them first, or just run the tests with 'make test'.`, testDir) } runTestsInDir(t, testDir) } func runTestsInDir(t *testing.T, dirPath string) { t.Helper() entries, err := os.ReadDir(dirPath) if err != nil { t.Fatalf("Failed to read directory %s: %v", dirPath, err) } for _, entry := range entries { entryPath := filepath.Join(dirPath, entry.Name()) if entry.IsDir() { // Check if it's a test case directory (contains in.yaml) if _, err := os.Stat(filepath.Join(entryPath, "in.yaml")); err == nil { t.Run(entry.Name(), func(t *testing.T) { runTest(t, entryPath) }) } else { // Otherwise, recurse into the subdirectory runTestsInDir(t, entryPath) } } } } 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("UnmarshalTest", 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) } }) }