123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346 |
- /*
- Copyright 2014 The Kubernetes Authors.
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- */
- package yaml
- import (
- "bufio"
- "bytes"
- "encoding/json"
- "fmt"
- "io"
- "io/ioutil"
- "strings"
- "unicode"
- "github.com/ghodss/yaml"
- "github.com/golang/glog"
- )
- // ToJSON converts a single YAML document into a JSON document
- // or returns an error. If the document appears to be JSON the
- // YAML decoding path is not used (so that error messages are
- // JSON specific).
- func ToJSON(data []byte) ([]byte, error) {
- if hasJSONPrefix(data) {
- return data, nil
- }
- return yaml.YAMLToJSON(data)
- }
- // YAMLToJSONDecoder decodes YAML documents from an io.Reader by
- // separating individual documents. It first converts the YAML
- // body to JSON, then unmarshals the JSON.
- type YAMLToJSONDecoder struct {
- reader Reader
- }
- // NewYAMLToJSONDecoder decodes YAML documents from the provided
- // stream in chunks by converting each document (as defined by
- // the YAML spec) into its own chunk, converting it to JSON via
- // yaml.YAMLToJSON, and then passing it to json.Decoder.
- func NewYAMLToJSONDecoder(r io.Reader) *YAMLToJSONDecoder {
- reader := bufio.NewReader(r)
- return &YAMLToJSONDecoder{
- reader: NewYAMLReader(reader),
- }
- }
- // Decode reads a YAML document as JSON from the stream or returns
- // an error. The decoding rules match json.Unmarshal, not
- // yaml.Unmarshal.
- func (d *YAMLToJSONDecoder) Decode(into interface{}) error {
- bytes, err := d.reader.Read()
- if err != nil && err != io.EOF {
- return err
- }
- if len(bytes) != 0 {
- err := yaml.Unmarshal(bytes, into)
- if err != nil {
- return YAMLSyntaxError{err}
- }
- }
- return err
- }
- // YAMLDecoder reads chunks of objects and returns ErrShortBuffer if
- // the data is not sufficient.
- type YAMLDecoder struct {
- r io.ReadCloser
- scanner *bufio.Scanner
- remaining []byte
- }
- // NewDocumentDecoder decodes YAML documents from the provided
- // stream in chunks by converting each document (as defined by
- // the YAML spec) into its own chunk. io.ErrShortBuffer will be
- // returned if the entire buffer could not be read to assist
- // the caller in framing the chunk.
- func NewDocumentDecoder(r io.ReadCloser) io.ReadCloser {
- scanner := bufio.NewScanner(r)
- scanner.Split(splitYAMLDocument)
- return &YAMLDecoder{
- r: r,
- scanner: scanner,
- }
- }
- // Read reads the previous slice into the buffer, or attempts to read
- // the next chunk.
- // TODO: switch to readline approach.
- func (d *YAMLDecoder) Read(data []byte) (n int, err error) {
- left := len(d.remaining)
- if left == 0 {
- // return the next chunk from the stream
- if !d.scanner.Scan() {
- err := d.scanner.Err()
- if err == nil {
- err = io.EOF
- }
- return 0, err
- }
- out := d.scanner.Bytes()
- d.remaining = out
- left = len(out)
- }
- // fits within data
- if left <= len(data) {
- copy(data, d.remaining)
- d.remaining = nil
- return left, nil
- }
- // caller will need to reread
- copy(data, d.remaining[:len(data)])
- d.remaining = d.remaining[len(data):]
- return len(data), io.ErrShortBuffer
- }
- func (d *YAMLDecoder) Close() error {
- return d.r.Close()
- }
- const yamlSeparator = "\n---"
- const separator = "---"
- // splitYAMLDocument is a bufio.SplitFunc for splitting YAML streams into individual documents.
- func splitYAMLDocument(data []byte, atEOF bool) (advance int, token []byte, err error) {
- if atEOF && len(data) == 0 {
- return 0, nil, nil
- }
- sep := len([]byte(yamlSeparator))
- if i := bytes.Index(data, []byte(yamlSeparator)); i >= 0 {
- // We have a potential document terminator
- i += sep
- after := data[i:]
- if len(after) == 0 {
- // we can't read any more characters
- if atEOF {
- return len(data), data[:len(data)-sep], nil
- }
- return 0, nil, nil
- }
- if j := bytes.IndexByte(after, '\n'); j >= 0 {
- return i + j + 1, data[0 : i-sep], nil
- }
- return 0, nil, nil
- }
- // If we're at EOF, we have a final, non-terminated line. Return it.
- if atEOF {
- return len(data), data, nil
- }
- // Request more data.
- return 0, nil, nil
- }
- // decoder is a convenience interface for Decode.
- type decoder interface {
- Decode(into interface{}) error
- }
- // YAMLOrJSONDecoder attempts to decode a stream of JSON documents or
- // YAML documents by sniffing for a leading { character.
- type YAMLOrJSONDecoder struct {
- r io.Reader
- bufferSize int
- decoder decoder
- rawData []byte
- }
- type JSONSyntaxError struct {
- Line int
- Err error
- }
- func (e JSONSyntaxError) Error() string {
- return fmt.Sprintf("json: line %d: %s", e.Line, e.Err.Error())
- }
- type YAMLSyntaxError struct {
- err error
- }
- func (e YAMLSyntaxError) Error() string {
- return e.err.Error()
- }
- // NewYAMLOrJSONDecoder returns a decoder that will process YAML documents
- // or JSON documents from the given reader as a stream. bufferSize determines
- // how far into the stream the decoder will look to figure out whether this
- // is a JSON stream (has whitespace followed by an open brace).
- func NewYAMLOrJSONDecoder(r io.Reader, bufferSize int) *YAMLOrJSONDecoder {
- return &YAMLOrJSONDecoder{
- r: r,
- bufferSize: bufferSize,
- }
- }
- // Decode unmarshals the next object from the underlying stream into the
- // provide object, or returns an error.
- func (d *YAMLOrJSONDecoder) Decode(into interface{}) error {
- if d.decoder == nil {
- buffer, origData, isJSON := GuessJSONStream(d.r, d.bufferSize)
- if isJSON {
- glog.V(4).Infof("decoding stream as JSON")
- d.decoder = json.NewDecoder(buffer)
- d.rawData = origData
- } else {
- glog.V(4).Infof("decoding stream as YAML")
- d.decoder = NewYAMLToJSONDecoder(buffer)
- }
- }
- err := d.decoder.Decode(into)
- if jsonDecoder, ok := d.decoder.(*json.Decoder); ok {
- if syntax, ok := err.(*json.SyntaxError); ok {
- data, readErr := ioutil.ReadAll(jsonDecoder.Buffered())
- if readErr != nil {
- glog.V(4).Infof("reading stream failed: %v", readErr)
- }
- js := string(data)
- // if contents from io.Reader are not complete,
- // use the original raw data to prevent panic
- if int64(len(js)) <= syntax.Offset {
- js = string(d.rawData)
- }
- start := strings.LastIndex(js[:syntax.Offset], "\n") + 1
- line := strings.Count(js[:start], "\n")
- return JSONSyntaxError{
- Line: line,
- Err: fmt.Errorf(syntax.Error()),
- }
- }
- }
- return err
- }
- type Reader interface {
- Read() ([]byte, error)
- }
- type YAMLReader struct {
- reader Reader
- }
- func NewYAMLReader(r *bufio.Reader) *YAMLReader {
- return &YAMLReader{
- reader: &LineReader{reader: r},
- }
- }
- // Read returns a full YAML document.
- func (r *YAMLReader) Read() ([]byte, error) {
- var buffer bytes.Buffer
- for {
- line, err := r.reader.Read()
- if err != nil && err != io.EOF {
- return nil, err
- }
- sep := len([]byte(separator))
- if i := bytes.Index(line, []byte(separator)); i == 0 {
- // We have a potential document terminator
- i += sep
- after := line[i:]
- if len(strings.TrimRightFunc(string(after), unicode.IsSpace)) == 0 {
- if buffer.Len() != 0 {
- return buffer.Bytes(), nil
- }
- if err == io.EOF {
- return nil, err
- }
- }
- }
- if err == io.EOF {
- if buffer.Len() != 0 {
- // If we're at EOF, we have a final, non-terminated line. Return it.
- return buffer.Bytes(), nil
- }
- return nil, err
- }
- buffer.Write(line)
- }
- }
- type LineReader struct {
- reader *bufio.Reader
- }
- // Read returns a single line (with '\n' ended) from the underlying reader.
- // An error is returned iff there is an error with the underlying reader.
- func (r *LineReader) Read() ([]byte, error) {
- var (
- isPrefix bool = true
- err error = nil
- line []byte
- buffer bytes.Buffer
- )
- for isPrefix && err == nil {
- line, isPrefix, err = r.reader.ReadLine()
- buffer.Write(line)
- }
- buffer.WriteByte('\n')
- return buffer.Bytes(), err
- }
- // GuessJSONStream scans the provided reader up to size, looking
- // for an open brace indicating this is JSON. It will return the
- // bufio.Reader it creates for the consumer.
- func GuessJSONStream(r io.Reader, size int) (io.Reader, []byte, bool) {
- buffer := bufio.NewReaderSize(r, size)
- b, _ := buffer.Peek(size)
- return buffer, b, hasJSONPrefix(b)
- }
- var jsonPrefix = []byte("{")
- // hasJSONPrefix returns true if the provided buffer appears to start with
- // a JSON open brace.
- func hasJSONPrefix(buf []byte) bool {
- return hasPrefix(buf, jsonPrefix)
- }
- // Return true if the first non-whitespace bytes in buf is
- // prefix.
- func hasPrefix(buf []byte, prefix []byte) bool {
- trim := bytes.TrimLeftFunc(buf, unicode.IsSpace)
- return bytes.HasPrefix(trim, prefix)
- }
|