Add support for embedded text, json and yaml checksums
This commit is contained in:
parent
ed928f9a3e
commit
9c7f6954e6
7 changed files with 850 additions and 1 deletions
111
cmd/cmd-checksum.go
Normal file
111
cmd/cmd-checksum.go
Normal file
|
@ -0,0 +1,111 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/safing/jess/filesig"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(checksum)
|
||||
checksum.AddCommand(checksumAdd)
|
||||
checksum.AddCommand(checksumVerify)
|
||||
}
|
||||
|
||||
var (
|
||||
checksum = &cobra.Command{
|
||||
Use: "checksum",
|
||||
Short: "add or verify embedded checksums",
|
||||
}
|
||||
|
||||
checksumAddUsage = "usage: checksum add <file>"
|
||||
checksumAdd = &cobra.Command{
|
||||
Use: "add <file>",
|
||||
Short: "add an embedded checksum to a file",
|
||||
Long: "add an embedded checksum to a file (support file types: txt, json, yaml)",
|
||||
RunE: handleChecksumAdd,
|
||||
}
|
||||
|
||||
checksumVerifyUsage = "usage: checksum verify <file>"
|
||||
checksumVerify = &cobra.Command{
|
||||
Use: "verify <file>",
|
||||
Short: "verify the embedded checksum of a file",
|
||||
Long: "verify the embedded checksum of a file (support file types: txt, json, yaml)",
|
||||
RunE: handleChecksumVerify,
|
||||
}
|
||||
)
|
||||
|
||||
func handleChecksumAdd(cmd *cobra.Command, args []string) error {
|
||||
// Check args.
|
||||
if len(args) != 1 {
|
||||
return errors.New(checksumAddUsage)
|
||||
}
|
||||
filename := args[0]
|
||||
|
||||
data, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read file: %w", err)
|
||||
}
|
||||
|
||||
switch filepath.Ext(filename) {
|
||||
case ".json":
|
||||
data, err = filesig.AddJSONChecksum(data)
|
||||
case ".yml", ".yaml":
|
||||
data, err = filesig.AddYAMLChecksum(data, filesig.TextPlacementAfterComment)
|
||||
case ".txt":
|
||||
data, err = filesig.AddTextFileChecksum(data, "#", filesig.TextPlacementAfterComment)
|
||||
default:
|
||||
return errors.New("unsupported file format")
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Write back to disk.
|
||||
fileInfo, err := os.Stat(filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to stat file: %w", err)
|
||||
}
|
||||
err = os.WriteFile(filename, data, fileInfo.Mode().Perm())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write back file with checksum: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("checksum added")
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleChecksumVerify(cmd *cobra.Command, args []string) error {
|
||||
// Check args.
|
||||
if len(args) != 1 {
|
||||
return errors.New(checksumVerifyUsage)
|
||||
}
|
||||
filename := args[0]
|
||||
|
||||
data, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read file: %w", err)
|
||||
}
|
||||
|
||||
switch filepath.Ext(filename) {
|
||||
case ".json":
|
||||
err = filesig.VerifyJSONChecksum(data)
|
||||
case ".yml", ".yaml":
|
||||
err = filesig.VerifyYAMLChecksum(data)
|
||||
case ".txt":
|
||||
err = filesig.VerifyTextFileChecksum(data, "#")
|
||||
default:
|
||||
return errors.New("unsupported file format")
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("checksum verified")
|
||||
return nil
|
||||
}
|
197
filesig/json.go
Normal file
197
filesig/json.go
Normal file
|
@ -0,0 +1,197 @@
|
|||
package filesig
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/pretty"
|
||||
"github.com/tidwall/sjson"
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
"github.com/safing/jess/lhash"
|
||||
)
|
||||
|
||||
// JSON file metadata keys.
|
||||
const (
|
||||
JSONKeyPrefix = "_jess-"
|
||||
JSONChecksumKey = JSONKeyPrefix + "checksum"
|
||||
JSONSignatureKey = JSONKeyPrefix + "signature"
|
||||
)
|
||||
|
||||
// AddJSONChecksum adds a checksum to a text file.
|
||||
func AddJSONChecksum(data []byte) ([]byte, error) {
|
||||
// Extract content and metadata from json.
|
||||
content, checksums, signatures, err := jsonSplit(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Calculate checksum.
|
||||
h := lhash.BLAKE2b_256.Digest(content)
|
||||
checksums = append(checksums, h.Base58())
|
||||
|
||||
// Sort and deduplicate checksums and sigs.
|
||||
slices.Sort[[]string, string](checksums)
|
||||
checksums = slices.Compact[[]string, string](checksums)
|
||||
slices.Sort[[]string, string](signatures)
|
||||
signatures = slices.Compact[[]string, string](signatures)
|
||||
|
||||
// Add metadata and return.
|
||||
return jsonAddMeta(content, checksums, signatures)
|
||||
}
|
||||
|
||||
// VerifyJSONChecksum checks a checksum in a text file.
|
||||
func VerifyJSONChecksum(data []byte) error {
|
||||
// Extract content and metadata from json.
|
||||
content, checksums, _, err := jsonSplit(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Verify all checksums.
|
||||
var checksumsVerified int
|
||||
for _, checksum := range checksums {
|
||||
// Parse checksum.
|
||||
h, err := lhash.FromBase58(checksum)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: failed to parse labeled hash: %w", ErrChecksumFailed, err)
|
||||
}
|
||||
// Verify checksum.
|
||||
if !h.Matches(content) {
|
||||
return ErrChecksumFailed
|
||||
}
|
||||
checksumsVerified++
|
||||
}
|
||||
|
||||
// Fail when no checksums were verified.
|
||||
if checksumsVerified == 0 {
|
||||
return ErrChecksumMissing
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func jsonSplit(data []byte) (
|
||||
content []byte,
|
||||
checksums []string,
|
||||
signatures []string,
|
||||
err error,
|
||||
) {
|
||||
// Check json.
|
||||
if !gjson.ValidBytes(data) {
|
||||
return nil, nil, nil, errors.New("invalid json")
|
||||
}
|
||||
content = data
|
||||
|
||||
// Get checksums.
|
||||
result := gjson.GetBytes(content, JSONChecksumKey)
|
||||
if result.Exists() {
|
||||
if result.IsArray() {
|
||||
array := result.Array()
|
||||
checksums = make([]string, 0, len(array))
|
||||
for _, result := range array {
|
||||
if result.Type == gjson.String {
|
||||
checksums = append(checksums, result.String())
|
||||
}
|
||||
}
|
||||
} else if result.Type == gjson.String {
|
||||
checksums = []string{result.String()}
|
||||
}
|
||||
|
||||
// Delete key.
|
||||
content, err = sjson.DeleteBytes(content, JSONChecksumKey)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Get signatures.
|
||||
result = gjson.GetBytes(content, JSONSignatureKey)
|
||||
if result.Exists() {
|
||||
if result.IsArray() {
|
||||
array := result.Array()
|
||||
signatures = make([]string, 0, len(array))
|
||||
for _, result := range array {
|
||||
if result.Type == gjson.String {
|
||||
signatures = append(signatures, result.String())
|
||||
}
|
||||
}
|
||||
} else if result.Type == gjson.String {
|
||||
signatures = []string{result.String()}
|
||||
}
|
||||
|
||||
// Delete key.
|
||||
content, err = sjson.DeleteBytes(content, JSONSignatureKey)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Format for reproducible checksums and signatures.
|
||||
content = pretty.PrettyOptions(content, &pretty.Options{
|
||||
Width: 200, // Must not change!
|
||||
Prefix: "", // Must not change!
|
||||
Indent: " ", // Must not change!
|
||||
SortKeys: true, // Must not change!
|
||||
})
|
||||
|
||||
return content, checksums, signatures, nil
|
||||
}
|
||||
|
||||
func jsonAddMeta(data []byte, checksums, signatures []string) ([]byte, error) {
|
||||
var (
|
||||
err error
|
||||
opts = &sjson.Options{
|
||||
ReplaceInPlace: true,
|
||||
}
|
||||
)
|
||||
|
||||
// Add checksums.
|
||||
switch len(checksums) {
|
||||
case 0:
|
||||
// Skip
|
||||
case 1:
|
||||
// Add single checksum.
|
||||
data, err = sjson.SetBytesOptions(
|
||||
data, JSONChecksumKey, checksums[0], opts,
|
||||
)
|
||||
default:
|
||||
// Add multiple checksums.
|
||||
data, err = sjson.SetBytesOptions(
|
||||
data, JSONChecksumKey, checksums, opts,
|
||||
)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Add signatures.
|
||||
switch len(signatures) {
|
||||
case 0:
|
||||
// Skip
|
||||
case 1:
|
||||
// Add single signature.
|
||||
data, err = sjson.SetBytesOptions(
|
||||
data, JSONSignatureKey, signatures[0], opts,
|
||||
)
|
||||
default:
|
||||
// Add multiple signatures.
|
||||
data, err = sjson.SetBytesOptions(
|
||||
data, JSONSignatureKey, signatures, opts,
|
||||
)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Final pretty print.
|
||||
data = pretty.PrettyOptions(data, &pretty.Options{
|
||||
Width: 200, // Must not change!
|
||||
Prefix: "", // Must not change!
|
||||
Indent: " ", // Must not change!
|
||||
SortKeys: true, // Must not change!
|
||||
})
|
||||
|
||||
return data, nil
|
||||
}
|
119
filesig/json_test.go
Normal file
119
filesig/json_test.go
Normal file
|
@ -0,0 +1,119 @@
|
|||
package filesig
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestJSONChecksums(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Base test text file.
|
||||
json := `{"a": "b", "c": 1}`
|
||||
|
||||
// Test with checksum after comment.
|
||||
|
||||
jsonWithChecksum := `{
|
||||
"_jess-checksum": "ZwtAd75qvioh6uf1NAq64KRgTbqeehFVYmhLmrwu1s7xJo",
|
||||
"a": "b",
|
||||
"c": 1
|
||||
}
|
||||
`
|
||||
|
||||
testJSONWithChecksum, err := AddJSONChecksum([]byte(json))
|
||||
assert.NoError(t, err, "should be able to add checksum")
|
||||
assert.Equal(t, jsonWithChecksum, string(testJSONWithChecksum), "should match")
|
||||
assert.NoError(t,
|
||||
VerifyJSONChecksum(testJSONWithChecksum),
|
||||
"checksum should be correct",
|
||||
)
|
||||
|
||||
jsonWithChecksum = `{
|
||||
"c": 1, "a":"b",
|
||||
"_jess-checksum": "ZwtAd75qvioh6uf1NAq64KRgTbqeehFVYmhLmrwu1s7xJo"
|
||||
}`
|
||||
assert.NoError(t,
|
||||
VerifyJSONChecksum([]byte(jsonWithChecksum)),
|
||||
"checksum should be correct",
|
||||
)
|
||||
|
||||
jsonWithMultiChecksum := `{
|
||||
"_jess-checksum": [
|
||||
"PTV7S3Ca81aRk2kdNw7q2RfjLfEdPPT5Px5d211nhZedZC",
|
||||
"PTV7S3Ca81aRk2kdNw7q2RfjLfEdPPT5Px5d211nhZedZC",
|
||||
"CyDGH55DZUwa556DiYztMXaKZVBDjzWeFETiGmABMbvC3V"
|
||||
],
|
||||
"a": "b",
|
||||
"c": 1
|
||||
}
|
||||
`
|
||||
assert.NoError(t,
|
||||
VerifyJSONChecksum([]byte(jsonWithMultiChecksum)),
|
||||
"checksum should be correct",
|
||||
)
|
||||
|
||||
jsonWithMultiChecksumOutput := `{
|
||||
"_jess-checksum": ["CyDGH55DZUwa556DiYztMXaKZVBDjzWeFETiGmABMbvC3V", "PTV7S3Ca81aRk2kdNw7q2RfjLfEdPPT5Px5d211nhZedZC", "ZwtAd75qvioh6uf1NAq64KRgTbqeehFVYmhLmrwu1s7xJo"],
|
||||
"a": "b",
|
||||
"c": 1
|
||||
}
|
||||
`
|
||||
|
||||
testJSONWithMultiChecksum, err := AddJSONChecksum([]byte(jsonWithMultiChecksum))
|
||||
assert.NoError(t, err, "should be able to add checksum")
|
||||
assert.Equal(t, jsonWithMultiChecksumOutput, string(testJSONWithMultiChecksum), "should match")
|
||||
assert.NoError(t,
|
||||
VerifyJSONChecksum(testJSONWithMultiChecksum),
|
||||
"checksum should be correct",
|
||||
)
|
||||
|
||||
// // Test with multiple checksums.
|
||||
|
||||
// textWithMultiChecksum := `# jess-checksum: PTNktssvYCYjZXLFL2QoBk7DYoSz1qF7DJd5XNvtptd41B
|
||||
// #!/bin/bash
|
||||
// # Initial
|
||||
// # Comment
|
||||
// # Block
|
||||
// # jess-checksum: Cy2TyVDjEStUqX3wCzCCKTfy228KaQK25ZDbHNmKiF8SPf
|
||||
|
||||
// do_something()
|
||||
|
||||
// # jess-checksum: YdgJFzuvFduk1MwRjZ2JkWQ6tCE1wkjn9xubSggKAdJSX5
|
||||
// `
|
||||
// assert.NoError(t,
|
||||
// VerifyTextFileChecksum([]byte(textWithMultiChecksum), "#"),
|
||||
// "checksum should be correct",
|
||||
// )
|
||||
|
||||
// textWithMultiChecksumOutput := `#!/bin/bash
|
||||
// # Initial
|
||||
// # Comment
|
||||
// # Block
|
||||
// # jess-checksum: Cy2TyVDjEStUqX3wCzCCKTfy228KaQK25ZDbHNmKiF8SPf
|
||||
// # jess-checksum: PTNktssvYCYjZXLFL2QoBk7DYoSz1qF7DJd5XNvtptd41B
|
||||
// # jess-checksum: YdgJFzuvFduk1MwRjZ2JkWQ6tCE1wkjn9xubSggKAdJSX5
|
||||
// # jess-checksum: ZwngYUfUBeUn99HSdrNxkWSNjqrgZuSpVrexeEYttBso5o
|
||||
|
||||
// do_something()
|
||||
// `
|
||||
// testTextWithMultiChecksumOutput, err := AddTextFileChecksum([]byte(textWithMultiChecksum), "#", AfterComment)
|
||||
// assert.NoError(t, err, "should be able to add checksum")
|
||||
// assert.Equal(t, textWithMultiChecksumOutput, string(testTextWithMultiChecksumOutput), "should match")
|
||||
|
||||
// // Test failing checksums.
|
||||
|
||||
// textWithFailingChecksums := `#!/bin/bash
|
||||
// # Initial
|
||||
// # Comment
|
||||
// # Block
|
||||
// # jess-checksum: Cy2TyVDjEStUqX3wCzCCKTfy228KaQK25ZDbHNmKiF8SPf
|
||||
// # jess-checksum: PTNktssvYCYjZXLFL2QoBk7DYoSz1qF7DJd5XNvtptd41B
|
||||
// # jess-checksum: YdgJFzuvFduk1MwRjZ2JkWQ6tCE1wkjn9xubSggKAdJSX5
|
||||
// # jess-checksum: ZwngYUfUBeUn99HSdrNxkWSNjaaaaaaaaaaaaaaaaaaaaa
|
||||
|
||||
// do_something()
|
||||
// `
|
||||
//
|
||||
// assert.Error(t, VerifyTextFileChecksum([]byte(textWithFailingChecksums), "#"), "should fail")
|
||||
}
|
232
filesig/text.go
Normal file
232
filesig/text.go
Normal file
|
@ -0,0 +1,232 @@
|
|||
package filesig
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
"github.com/safing/jess/lhash"
|
||||
)
|
||||
|
||||
// Text file metadata keys.
|
||||
const (
|
||||
TextKeyPrefix = "jess-"
|
||||
TextChecksumKey = TextKeyPrefix + "checksum"
|
||||
TextSignatureKey = TextKeyPrefix + "signature"
|
||||
)
|
||||
|
||||
// Text Operation Errors.
|
||||
var (
|
||||
ErrChecksumMissing = errors.New("no checksum found")
|
||||
ErrChecksumFailed = errors.New("checksum does not match")
|
||||
ErrSignatureMissing = errors.New("signature not found")
|
||||
ErrSignatureFailed = errors.New("signature does not match")
|
||||
)
|
||||
|
||||
// TextPlacement signifies where jess metadata is put in text files.
|
||||
type TextPlacement string
|
||||
|
||||
const (
|
||||
// TextPlacementTop places the metadata at end of file.
|
||||
TextPlacementTop TextPlacement = "top"
|
||||
// TextPlacementBottom places the metadata at end of file.
|
||||
TextPlacementBottom TextPlacement = "bottom"
|
||||
// TextPlacementAfterComment places the metadata at end of the top comment
|
||||
// block, or at the top, if the first line is not a comment.
|
||||
TextPlacementAfterComment TextPlacement = "after-comment"
|
||||
|
||||
defaultMetaPlacement = TextPlacementAfterComment
|
||||
)
|
||||
|
||||
// AddTextFileChecksum adds a checksum to a text file.
|
||||
func AddTextFileChecksum(data []byte, commentSign string, placement TextPlacement) ([]byte, error) {
|
||||
// Split text file into content and jess metadata lines.
|
||||
content, metaLines, err := textSplit(data, commentSign)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Calculate checksum.
|
||||
h := lhash.BLAKE2b_256.Digest(content)
|
||||
metaLines = append(metaLines, TextChecksumKey+": "+h.Base58())
|
||||
|
||||
// Sort and deduplicate meta lines.
|
||||
slices.Sort[[]string, string](metaLines)
|
||||
metaLines = slices.Compact[[]string, string](metaLines)
|
||||
|
||||
// Add meta lines and return.
|
||||
return textAddMeta(content, metaLines, commentSign, placement)
|
||||
}
|
||||
|
||||
// VerifyTextFileChecksum checks a checksum in a text file.
|
||||
func VerifyTextFileChecksum(data []byte, commentSign string) error {
|
||||
// Split text file into content and jess metadata lines.
|
||||
content, metaLines, err := textSplit(data, commentSign)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Verify all checksums.
|
||||
var checksumsVerified int
|
||||
for _, line := range metaLines {
|
||||
if strings.HasPrefix(line, TextChecksumKey) {
|
||||
// Clean key, delimiters and space.
|
||||
line = strings.TrimPrefix(line, TextChecksumKey)
|
||||
line = strings.TrimSpace(line) // Spaces and newlines.
|
||||
line = strings.Trim(line, ":= ") // Delimiters and spaces.
|
||||
// Parse checksum.
|
||||
h, err := lhash.FromBase58(line)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: failed to parse labeled hash: %w", ErrChecksumFailed, err)
|
||||
}
|
||||
// Verify checksum.
|
||||
if !h.Matches(content) {
|
||||
return ErrChecksumFailed
|
||||
}
|
||||
checksumsVerified++
|
||||
}
|
||||
}
|
||||
|
||||
// Fail when no checksums were verified.
|
||||
if checksumsVerified == 0 {
|
||||
return ErrChecksumMissing
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func textSplit(data []byte, commentSign string) (content []byte, metaLines []string, err error) {
|
||||
metaLinePrefix := commentSign + " " + TextKeyPrefix
|
||||
contentBuf := bytes.NewBuffer(make([]byte, 0, len(data)))
|
||||
metaLines = make([]string, 0, 1)
|
||||
|
||||
// Find jess metadata lines.
|
||||
s := bufio.NewScanner(bytes.NewReader(data))
|
||||
s.Split(scanRawLines)
|
||||
for s.Scan() {
|
||||
if strings.HasPrefix(s.Text(), metaLinePrefix) {
|
||||
metaLines = append(metaLines, strings.TrimSpace(strings.TrimPrefix(s.Text(), commentSign)))
|
||||
} else {
|
||||
_, _ = contentBuf.Write(s.Bytes())
|
||||
}
|
||||
}
|
||||
if s.Err() != nil {
|
||||
return nil, nil, s.Err()
|
||||
}
|
||||
|
||||
return bytes.TrimSpace(contentBuf.Bytes()), metaLines, nil
|
||||
}
|
||||
|
||||
func detectLineEndFormat(data []byte) (lineEnd string) {
|
||||
i := bytes.IndexByte(data, '\n')
|
||||
switch i {
|
||||
case -1:
|
||||
// Default to just newline.
|
||||
return "\n"
|
||||
case 0:
|
||||
// File start with a newline.
|
||||
return "\n"
|
||||
default:
|
||||
// First newline is at second byte or later.
|
||||
if bytes.Equal(data[i-1:i+1], []byte("\r\n")) {
|
||||
return "\r\n"
|
||||
}
|
||||
return "\n"
|
||||
}
|
||||
}
|
||||
|
||||
func textAddMeta(data []byte, metaLines []string, commentSign string, position TextPlacement) ([]byte, error) {
|
||||
// Prepare new buffer.
|
||||
requiredSize := len(data)
|
||||
for _, line := range metaLines {
|
||||
requiredSize += len(line) + len(commentSign) + 3 // space + CRLF
|
||||
}
|
||||
contentBuf := bytes.NewBuffer(make([]byte, 0, requiredSize))
|
||||
|
||||
// Find line ending.
|
||||
lineEnd := detectLineEndFormat(data)
|
||||
|
||||
// Find jess metadata lines.
|
||||
if position == "" {
|
||||
position = defaultMetaPlacement
|
||||
}
|
||||
|
||||
switch position {
|
||||
case TextPlacementTop:
|
||||
textWriteMetaLines(metaLines, commentSign, lineEnd, contentBuf)
|
||||
contentBuf.Write(data)
|
||||
// Add final newline.
|
||||
contentBuf.WriteString(lineEnd)
|
||||
|
||||
case TextPlacementBottom:
|
||||
contentBuf.Write(data)
|
||||
// Add to newlines when appending, as content is first whitespace-stripped.
|
||||
contentBuf.WriteString(lineEnd)
|
||||
contentBuf.WriteString(lineEnd)
|
||||
textWriteMetaLines(metaLines, commentSign, lineEnd, contentBuf)
|
||||
|
||||
case TextPlacementAfterComment:
|
||||
metaWritten := false
|
||||
s := bufio.NewScanner(bytes.NewReader(data))
|
||||
s.Split(scanRawLines)
|
||||
for s.Scan() {
|
||||
switch {
|
||||
case metaWritten:
|
||||
_, _ = contentBuf.Write(s.Bytes())
|
||||
case strings.HasPrefix(s.Text(), commentSign):
|
||||
_, _ = contentBuf.Write(s.Bytes())
|
||||
default:
|
||||
textWriteMetaLines(metaLines, commentSign, lineEnd, contentBuf)
|
||||
metaWritten = true
|
||||
_, _ = contentBuf.Write(s.Bytes())
|
||||
}
|
||||
}
|
||||
if s.Err() != nil {
|
||||
return nil, s.Err()
|
||||
}
|
||||
// If we have scanned through the file, and meta was not written, write it now.
|
||||
if !metaWritten {
|
||||
textWriteMetaLines(metaLines, commentSign, lineEnd, contentBuf)
|
||||
}
|
||||
// Add final newline.
|
||||
contentBuf.WriteString(lineEnd)
|
||||
}
|
||||
|
||||
return contentBuf.Bytes(), nil
|
||||
}
|
||||
|
||||
func textWriteMetaLines(metaLines []string, commentSign string, lineEnd string, writer io.StringWriter) {
|
||||
for _, line := range metaLines {
|
||||
_, _ = writer.WriteString(commentSign)
|
||||
_, _ = writer.WriteString(" ")
|
||||
_, _ = writer.WriteString(line)
|
||||
_, _ = writer.WriteString(lineEnd)
|
||||
}
|
||||
}
|
||||
|
||||
// scanRawLines is a split function for a Scanner that returns each line of
|
||||
// text, including any trailing end-of-line marker. The returned line may
|
||||
// be empty. The end-of-line marker is one optional carriage return followed
|
||||
// by one mandatory newline. In regular expression notation, it is `\r?\n`.
|
||||
// The last non-empty line of input will be returned even if it has no
|
||||
// newline.
|
||||
func scanRawLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
||||
if atEOF && len(data) == 0 {
|
||||
return 0, nil, nil
|
||||
}
|
||||
if i := bytes.IndexByte(data, '\n'); i >= 0 {
|
||||
// We have a full newline-terminated line.
|
||||
return i + 1, data[0 : i+1], 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
|
||||
}
|
179
filesig/text_test.go
Normal file
179
filesig/text_test.go
Normal file
|
@ -0,0 +1,179 @@
|
|||
package filesig
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestTextChecksums(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Base test text file.
|
||||
text := `#!/bin/bash
|
||||
# Initial
|
||||
# Comment
|
||||
# Block
|
||||
|
||||
do_something()`
|
||||
|
||||
// Test with checksum after comment.
|
||||
|
||||
textWithChecksumAfterComment := `#!/bin/bash
|
||||
# Initial
|
||||
# Comment
|
||||
# Block
|
||||
# jess-checksum: ZwngYUfUBeUn99HSdrNxkWSNjqrgZuSpVrexeEYttBso5o
|
||||
|
||||
do_something()
|
||||
`
|
||||
|
||||
testTextWithChecksumAfterComment, err := AddTextFileChecksum([]byte(text), "#", TextPlacementAfterComment)
|
||||
assert.NoError(t, err, "should be able to add checksum")
|
||||
assert.Equal(t, textWithChecksumAfterComment, string(testTextWithChecksumAfterComment), "should match")
|
||||
assert.NoError(t,
|
||||
VerifyTextFileChecksum(testTextWithChecksumAfterComment, "#"),
|
||||
"checksum should be correct",
|
||||
)
|
||||
assert.NoError(t,
|
||||
VerifyTextFileChecksum(append(
|
||||
[]byte("\n\n \r\n"),
|
||||
testTextWithChecksumAfterComment...,
|
||||
), "#"),
|
||||
"checksum should be correct",
|
||||
)
|
||||
assert.NoError(t,
|
||||
VerifyTextFileChecksum(append(
|
||||
testTextWithChecksumAfterComment,
|
||||
[]byte("\r\n \n \n")...,
|
||||
), "#"),
|
||||
"checksum should be correct",
|
||||
)
|
||||
|
||||
// Test with checksum at top.
|
||||
|
||||
textWithChecksumAtTop := `# jess-checksum: ZwngYUfUBeUn99HSdrNxkWSNjqrgZuSpVrexeEYttBso5o
|
||||
#!/bin/bash
|
||||
# Initial
|
||||
# Comment
|
||||
# Block
|
||||
|
||||
do_something()
|
||||
`
|
||||
|
||||
testTextWithChecksumAtTop, err := AddTextFileChecksum([]byte(text), "#", TextPlacementTop)
|
||||
assert.NoError(t, err, "should be able to add checksum")
|
||||
assert.Equal(t, textWithChecksumAtTop, string(testTextWithChecksumAtTop), "should match")
|
||||
assert.NoError(t,
|
||||
VerifyTextFileChecksum(testTextWithChecksumAtTop, "#"),
|
||||
"checksum should be correct",
|
||||
)
|
||||
|
||||
// Test with checksum at bottom.
|
||||
|
||||
textWithChecksumAtBottom := `#!/bin/bash
|
||||
# Initial
|
||||
# Comment
|
||||
# Block
|
||||
|
||||
do_something()
|
||||
|
||||
# jess-checksum: ZwngYUfUBeUn99HSdrNxkWSNjqrgZuSpVrexeEYttBso5o
|
||||
`
|
||||
|
||||
testTextWithChecksumAtBottom, err := AddTextFileChecksum([]byte(text), "#", TextPlacementBottom)
|
||||
assert.NoError(t, err, "should be able to add checksum")
|
||||
assert.Equal(t, textWithChecksumAtBottom, string(testTextWithChecksumAtBottom), "should match")
|
||||
assert.NoError(t,
|
||||
VerifyTextFileChecksum(testTextWithChecksumAtBottom, "#"),
|
||||
"checksum should be correct",
|
||||
)
|
||||
|
||||
// Test with multiple checksums.
|
||||
|
||||
textWithMultiChecksum := `# jess-checksum: PTNktssvYCYjZXLFL2QoBk7DYoSz1qF7DJd5XNvtptd41B
|
||||
#!/bin/bash
|
||||
# Initial
|
||||
# Comment
|
||||
# Block
|
||||
# jess-checksum: Cy2TyVDjEStUqX3wCzCCKTfy228KaQK25ZDbHNmKiF8SPf
|
||||
|
||||
do_something()
|
||||
|
||||
# jess-checksum: YdgJFzuvFduk1MwRjZ2JkWQ6tCE1wkjn9xubSggKAdJSX5
|
||||
`
|
||||
assert.NoError(t,
|
||||
VerifyTextFileChecksum([]byte(textWithMultiChecksum), "#"),
|
||||
"checksum should be correct",
|
||||
)
|
||||
|
||||
textWithMultiChecksumOutput := `#!/bin/bash
|
||||
# Initial
|
||||
# Comment
|
||||
# Block
|
||||
# jess-checksum: Cy2TyVDjEStUqX3wCzCCKTfy228KaQK25ZDbHNmKiF8SPf
|
||||
# jess-checksum: PTNktssvYCYjZXLFL2QoBk7DYoSz1qF7DJd5XNvtptd41B
|
||||
# jess-checksum: YdgJFzuvFduk1MwRjZ2JkWQ6tCE1wkjn9xubSggKAdJSX5
|
||||
# jess-checksum: ZwngYUfUBeUn99HSdrNxkWSNjqrgZuSpVrexeEYttBso5o
|
||||
|
||||
do_something()
|
||||
`
|
||||
testTextWithMultiChecksumOutput, err := AddTextFileChecksum([]byte(textWithMultiChecksum), "#", TextPlacementAfterComment)
|
||||
assert.NoError(t, err, "should be able to add checksum")
|
||||
assert.Equal(t, textWithMultiChecksumOutput, string(testTextWithMultiChecksumOutput), "should match")
|
||||
|
||||
// Test failing checksums.
|
||||
|
||||
textWithFailingChecksums := `#!/bin/bash
|
||||
# Initial
|
||||
# Comment
|
||||
# Block
|
||||
# jess-checksum: Cy2TyVDjEStUqX3wCzCCKTfy228KaQK25ZDbHNmKiF8SPf
|
||||
# jess-checksum: PTNktssvYCYjZXLFL2QoBk7DYoSz1qF7DJd5XNvtptd41B
|
||||
# jess-checksum: YdgJFzuvFduk1MwRjZ2JkWQ6tCE1wkjn9xubSggKAdJSX5
|
||||
# jess-checksum: ZwngYUfUBeUn99HSdrNxkWSNjaaaaaaaaaaaaaaaaaaaaa
|
||||
|
||||
do_something()
|
||||
`
|
||||
assert.Error(t, VerifyTextFileChecksum([]byte(textWithFailingChecksums), "#"), "should fail")
|
||||
}
|
||||
|
||||
func TestLineEndDetection(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert.Equal(t,
|
||||
"\n",
|
||||
detectLineEndFormat(nil),
|
||||
"empty data should default to simple lf ending",
|
||||
)
|
||||
assert.Equal(t,
|
||||
"\n",
|
||||
detectLineEndFormat([]byte("\n")),
|
||||
"shoud detect lf ending with empty first line",
|
||||
)
|
||||
assert.Equal(t,
|
||||
"\r\n",
|
||||
detectLineEndFormat([]byte("\r\n")),
|
||||
"shoud detect crlf ending with empty first line",
|
||||
)
|
||||
assert.Equal(t,
|
||||
"\n",
|
||||
detectLineEndFormat([]byte("abc\n")),
|
||||
"shoud detect lf ending with data on single line",
|
||||
)
|
||||
assert.Equal(t,
|
||||
"\r\n",
|
||||
detectLineEndFormat([]byte("abc\r\n")),
|
||||
"shoud detect crlf ending with data on single line",
|
||||
)
|
||||
assert.Equal(t,
|
||||
"\n",
|
||||
detectLineEndFormat([]byte("abc\nabc\r\n")),
|
||||
"shoud detect lf ending with data on first line",
|
||||
)
|
||||
assert.Equal(t,
|
||||
"\r\n",
|
||||
detectLineEndFormat([]byte("abc\r\nabc\n")),
|
||||
"shoud detect crlf ending with data on first line",
|
||||
)
|
||||
}
|
11
filesig/text_yaml.go
Normal file
11
filesig/text_yaml.go
Normal file
|
@ -0,0 +1,11 @@
|
|||
package filesig
|
||||
|
||||
// AddYAMLChecksum adds a checksum to a yaml file.
|
||||
func AddYAMLChecksum(data []byte, placement TextPlacement) ([]byte, error) {
|
||||
return AddTextFileChecksum(data, "#", placement)
|
||||
}
|
||||
|
||||
// VerifyYAMLChecksum checks a checksum in a yaml file.
|
||||
func VerifyYAMLChecksum(data []byte) error {
|
||||
return VerifyTextFileChecksum(data, "#")
|
||||
}
|
2
go.mod
2
go.mod
|
@ -1,6 +1,6 @@
|
|||
module github.com/safing/jess
|
||||
|
||||
go 1.15
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/AlecAivazis/survey/v2 v2.3.6
|
||||
|
|
Loading…
Add table
Reference in a new issue