1// Copyright 2019 CUE Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15// Package source contains utility functions that standardize reading source
16// bytes across cue packages.
17package source
18
19import (
20 "bytes"
21 "fmt"
22 "io"
23 "math"
24 "os"
25 "strings"
26)
27
28// ReadAll loads the source bytes for the given arguments. If src != nil,
29// ReadAll converts src to a []byte if possible; otherwise it returns an
30// error. If src == nil, ReadAll returns the result of reading the file
31// specified by filename.
32func ReadAll(filename string, src any) ([]byte, error) {
33 if src != nil {
34 switch src := src.(type) {
35 case string:
36 return []byte(src), nil
37 case []byte:
38 return src, nil
39 case *bytes.Buffer:
40 // is io.Reader, but src is already available in []byte form
41 return src.Bytes(), nil
42 case io.Reader:
43 return io.ReadAll(src)
44 }
45 return nil, fmt.Errorf("invalid source type %T", src)
46 }
47 return os.ReadFile(filename)
48}
49
50// ReadAllSize is like [io.ReadAll] while taking advantage of a size hint for the input reader,
51// much like [os.ReadFile] does when reading regular files with a known size.
52// When the size hint is negative, it simply uses [io.ReadAll].
53func ReadAllSize(r io.Reader, size int) ([]byte, error) {
54 if size >= 0 {
55 // We use a [bytes.Buffer] here, because the given size is a hint,
56 // and not guaranteed to be exactly correct.
57 //
58 // Before each read, [bytes.Buffer] ensures that the internal buffer
59 // has enough available capacity to read at least [bytes.MinRead] bytes.
60 // Many readers tend to signal EOF via a final (0, EOF) read,
61 // which then triggers growing the slice to accomodate [bytes.MinRead].
62 buf := bytes.NewBuffer(make([]byte, 0, size+bytes.MinRead))
63 _, err := buf.ReadFrom(r)
64 return buf.Bytes(), err
65 }
66 return io.ReadAll(r)
67}
68
69// Open creates a source reader for the given arguments.
70// If src != nil, Open converts src to an [io.Reader] if possible; otherwise it returns an error.
71// If src == nil, Open returns the result of opening the file specified by filename.
72//
73// The caller must check if the result is an [io.Closer], and if so, close it when done.
74// The size of the opened reader is returned if possible, or -1 otherwise.
75func Open(filename string, src any) (_ io.Reader, size int, _ error) {
76 if src != nil {
77 switch src := src.(type) {
78 case string:
79 return strings.NewReader(src), len(src), nil
80 case []byte:
81 return bytes.NewReader(src), len(src), nil
82 case *os.File:
83 return fileWithSize(src)
84 case io.Reader:
85 return src, -1, nil
86 }
87 return nil, -1, fmt.Errorf("invalid source type %T", src)
88 }
89 f, err := os.Open(filename)
90 if err != nil {
91 return nil, -1, err
92 }
93 return fileWithSize(f)
94}
95
96func fileWithSize(f *os.File) (io.Reader, int, error) {
97 // If we just opened a regular file, return its size too.
98 // If we can't get its size, such as non-regular files, don't give one.
99 stat, err := f.Stat()
100 if err != nil || !stat.Mode().IsRegular() {
101 return f, -1, nil
102 }
103 size := stat.Size()
104 // If the size would overflow an int, it won't fit in memory anyway.
105 if size > math.MaxInt {
106 return f, -1, nil
107 }
108 return f, int(size), nil
109}