blob: 58e5603a3b161a6e493b03661337bebdfff2b6a4 [file] [edit]
// Copyright 2020-2024 Buf Technologies, Inc.
//
// 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 bufconfig
import (
"context"
"errors"
"io"
"io/fs"
"path/filepath"
"github.com/bufbuild/buf/private/pkg/encoding"
"github.com/bufbuild/buf/private/pkg/normalpath"
"github.com/bufbuild/buf/private/pkg/storage"
"github.com/bufbuild/buf/private/pkg/syserror"
"go.uber.org/multierr"
)
// File is the common interface shared by all config files.
type File interface {
FileInfo
// ObjectData returns the underlying ObjectData.
//
// This is non-nil on Files if they were created from storage.ReadBuckets. It is nil
// if the File was created via a New constructor or Read method.
//
// This ObjectData is used for digest calculations.
ObjectData() ObjectData
isFile()
}
// *** PRIVATE ***
func getFileForPrefix[F File](
ctx context.Context,
bucket storage.ReadBucket,
prefix string,
fileNames []string,
fileNameToSupportedFileVersions map[string]map[FileVersion]struct{},
readFileFunc func(
data []byte,
objectData ObjectData,
allowJSON bool,
) (F, error),
) (F, error) {
for _, fileName := range fileNames {
path := normalpath.Join(prefix, fileName)
data, err := storage.ReadPath(ctx, bucket, path)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
continue
}
var f F
return f, err
}
f, err := readFileFunc(data, newObjectData(fileName, data), false)
if err != nil {
return f, newDecodeError(path, err)
}
if err := validateSupportedFileVersion(fileName, f.FileVersion(), fileNameToSupportedFileVersions); err != nil {
return f, newDecodeError(path, err)
}
return f, nil
}
var f F
return f, &fs.PathError{Op: "read", Path: normalpath.Join(prefix, fileNames[0]), Err: fs.ErrNotExist}
}
func getFileVersionForPrefix(
ctx context.Context,
bucket storage.ReadBucket,
prefix string,
fileNames []string,
fileNameToSupportedFileVersions map[string]map[FileVersion]struct{},
fileVersionRequired bool,
suggestedFileVersion FileVersion,
defaultFileVersion FileVersion,
) (FileVersion, error) {
for _, fileName := range fileNames {
path := normalpath.Join(prefix, fileName)
data, err := storage.ReadPath(ctx, bucket, path)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
continue
}
return 0, err
}
fileVersion, err := getFileVersionForData(data, false, fileVersionRequired, fileNameToSupportedFileVersions, suggestedFileVersion, defaultFileVersion)
if err != nil {
return 0, newDecodeError(path, err)
}
if err := validateSupportedFileVersion(fileName, fileVersion, fileNameToSupportedFileVersions); err != nil {
return 0, newDecodeError(path, err)
}
return fileVersion, nil
}
return 0, &fs.PathError{Op: "read", Path: normalpath.Join(prefix, fileNames[0]), Err: fs.ErrNotExist}
}
func putFileForPrefix[F File](
ctx context.Context,
bucket storage.WriteBucket,
prefix string,
f F,
fileName string,
fileNameToSupportedFileVersions map[string]map[FileVersion]struct{},
writeFileFunc func(
writer io.Writer,
f F,
) error,
) (retErr error) {
if err := validateSupportedFileVersion(fileName, f.FileVersion(), fileNameToSupportedFileVersions); err != nil {
// This is effectively a system error. We should be able to write with whatever file name we have.
return syserror.Wrap(newEncodeError(fileName, err))
}
path := normalpath.Join(prefix, fileName)
writeObjectCloser, err := bucket.Put(ctx, path, storage.PutWithAtomic())
if err != nil {
return err
}
defer func() {
retErr = multierr.Append(retErr, writeObjectCloser.Close())
}()
return writeFileFunc(writeObjectCloser, f)
}
func readFile[F File](
reader io.Reader,
fileName string,
readFileFunc func(
data []byte,
objectData ObjectData,
allowJSON bool,
) (F, error),
) (F, error) {
data, err := io.ReadAll(reader)
if err != nil {
var f F
return f, err
}
f, err := readFileFunc(data, nil, true)
if err != nil {
return f, newDecodeError(fileName, err)
}
return f, nil
}
func writeFile[F File](
writer io.Writer,
f F,
writeFileFunc func(
writer io.Writer,
f F,
) error,
) error {
if err := writeFileFunc(writer, f); err != nil {
var fileName string
if objectData := f.ObjectData(); objectData != nil {
fileName = objectData.Name()
}
return newDecodeError(fileName, err)
}
return nil
}
func getUnmarshalStrict(allowJSON bool) func([]byte, interface{}) error {
if allowJSON {
return encoding.UnmarshalJSONOrYAMLStrict
}
return encoding.UnmarshalYAMLStrict
}
func getUnmarshalNonStrict(allowJSON bool) func([]byte, interface{}) error {
if allowJSON {
return encoding.UnmarshalJSONOrYAMLNonStrict
}
return encoding.UnmarshalYAMLNonStrict
}
func newDecodeError(fileName string, err error) error {
if fileName == "" {
fileName = "config file"
}
// We intercept PathErrors in buffetch to deal with fixing of paths.
// We return a cleaned, unnormalized path in the error for clarity with user's filesystem.
return &fs.PathError{Op: "decode", Path: filepath.Clean(normalpath.Unnormalize(fileName)), Err: err}
}
func newEncodeError(fileName string, err error) error {
if fileName == "" {
fileName = "config file"
}
// We intercept PathErrors in buffetch to deal with fixing of paths.
// We return a cleaned, unnormalized path in the error for clarity with user's filesystem.
return &fs.PathError{Op: "encode", Path: filepath.Clean(normalpath.Unnormalize(fileName)), Err: err}
}