mirror of
https://github.com/FiloSottile/age.git
synced 2025-12-23 13:35:14 +00:00
armor: don't leave an empty line before the footer
Closes #264 Fixes #263
This commit is contained in:
@@ -28,7 +28,7 @@ const (
|
|||||||
|
|
||||||
type armoredWriter struct {
|
type armoredWriter struct {
|
||||||
started, closed bool
|
started, closed bool
|
||||||
encoder io.WriteCloser
|
encoder *format.WrappedBase64Encoder
|
||||||
dst io.Writer
|
dst io.Writer
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,15 +50,20 @@ func (a *armoredWriter) Close() error {
|
|||||||
if err := a.encoder.Close(); err != nil {
|
if err := a.encoder.Close(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_, err := io.WriteString(a.dst, "\n"+Footer+"\n")
|
footer := Footer + "\n"
|
||||||
|
if !a.encoder.LastLineIsEmpty() {
|
||||||
|
footer = "\n" + footer
|
||||||
|
}
|
||||||
|
_, err := io.WriteString(a.dst, footer)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewWriter(dst io.Writer) io.WriteCloser {
|
func NewWriter(dst io.Writer) io.WriteCloser {
|
||||||
// TODO: write a test with aligned and misaligned sizes, and 8 and 10 steps.
|
// TODO: write a test with aligned and misaligned sizes, and 8 and 10 steps.
|
||||||
return &armoredWriter{dst: dst,
|
return &armoredWriter{
|
||||||
encoder: base64.NewEncoder(base64.StdEncoding.Strict(),
|
dst: dst,
|
||||||
format.NewlineWriter(dst))}
|
encoder: format.NewWrappedBase64Encoder(base64.StdEncoding, dst),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type armoredReader struct {
|
type armoredReader struct {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ package armor_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"crypto/rand"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -18,6 +19,7 @@ import (
|
|||||||
|
|
||||||
"filippo.io/age"
|
"filippo.io/age"
|
||||||
"filippo.io/age/armor"
|
"filippo.io/age/armor"
|
||||||
|
"filippo.io/age/internal/format"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ExampleNewWriter() {
|
func ExampleNewWriter() {
|
||||||
@@ -87,9 +89,15 @@ kB/RRusYjn+KVJ+KTioxj0THtzZPXcjFKuQ1
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestArmor(t *testing.T) {
|
func TestArmor(t *testing.T) {
|
||||||
|
t.Run("PartialLine", func(t *testing.T) { testArmor(t, 611) })
|
||||||
|
t.Run("FullLine", func(t *testing.T) { testArmor(t, 10*format.BytesPerLine) })
|
||||||
|
}
|
||||||
|
|
||||||
|
func testArmor(t *testing.T, size int) {
|
||||||
buf := &bytes.Buffer{}
|
buf := &bytes.Buffer{}
|
||||||
w := armor.NewWriter(buf)
|
w := armor.NewWriter(buf)
|
||||||
plain := make([]byte, 611)
|
plain := make([]byte, size)
|
||||||
|
rand.Read(plain)
|
||||||
if _, err := w.Write(plain); err != nil {
|
if _, err := w.Write(plain); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -101,9 +109,18 @@ func TestArmor(t *testing.T) {
|
|||||||
if block == nil {
|
if block == nil {
|
||||||
t.Fatal("PEM decoding failed")
|
t.Fatal("PEM decoding failed")
|
||||||
}
|
}
|
||||||
|
if len(block.Headers) != 0 {
|
||||||
|
t.Error("unexpected headers")
|
||||||
|
}
|
||||||
|
if block.Type != "AGE ENCRYPTED FILE" {
|
||||||
|
t.Errorf("unexpected type %q", block.Type)
|
||||||
|
}
|
||||||
if !bytes.Equal(block.Bytes, plain) {
|
if !bytes.Equal(block.Bytes, plain) {
|
||||||
t.Error("PEM decoded value doesn't match")
|
t.Error("PEM decoded value doesn't match")
|
||||||
}
|
}
|
||||||
|
if !bytes.Equal(buf.Bytes(), pem.EncodeToMemory(block)) {
|
||||||
|
t.Error("PEM re-encoded value doesn't match")
|
||||||
|
}
|
||||||
|
|
||||||
r := armor.NewReader(buf)
|
r := armor.NewReader(buf)
|
||||||
out, err := ioutil.ReadAll(r)
|
out, err := ioutil.ReadAll(r)
|
||||||
|
|||||||
@@ -43,25 +43,40 @@ func DecodeString(s string) ([]byte, error) {
|
|||||||
var EncodeToString = b64.EncodeToString
|
var EncodeToString = b64.EncodeToString
|
||||||
|
|
||||||
const ColumnsPerLine = 64
|
const ColumnsPerLine = 64
|
||||||
|
|
||||||
const BytesPerLine = ColumnsPerLine / 4 * 3
|
const BytesPerLine = ColumnsPerLine / 4 * 3
|
||||||
|
|
||||||
// NewlineWriter returns a Writer that writes to dst, inserting an LF character
|
// NewWrappedBase64Encoder returns a WrappedBase64Encoder that writes to dst.
|
||||||
// every ColumnsPerLine bytes. It does not insert a newline neither at the
|
func NewWrappedBase64Encoder(enc *base64.Encoding, dst io.Writer) *WrappedBase64Encoder {
|
||||||
// beginning nor at the end of the stream, but it ensures the last line is
|
w := &WrappedBase64Encoder{dst: dst}
|
||||||
// shorter than ColumnsPerLine, which means it might be empty.
|
w.enc = base64.NewEncoder(enc, WriterFunc(w.writeWrapped))
|
||||||
func NewlineWriter(dst io.Writer) io.Writer {
|
return w
|
||||||
return &newlineWriter{dst: dst}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type newlineWriter struct {
|
type WriterFunc func(p []byte) (int, error)
|
||||||
|
|
||||||
|
func (f WriterFunc) Write(p []byte) (int, error) { return f(p) }
|
||||||
|
|
||||||
|
// WrappedBase64Encoder is a standard base64 encoder that inserts an LF
|
||||||
|
// character every ColumnsPerLine bytes. It does not insert a newline neither at
|
||||||
|
// the beginning nor at the end of the stream, but it ensures the last line is
|
||||||
|
// shorter than ColumnsPerLine, which means it might be empty.
|
||||||
|
type WrappedBase64Encoder struct {
|
||||||
|
enc io.WriteCloser
|
||||||
dst io.Writer
|
dst io.Writer
|
||||||
written int
|
written int
|
||||||
buf bytes.Buffer
|
buf bytes.Buffer
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *newlineWriter) Write(p []byte) (int, error) {
|
func (w *WrappedBase64Encoder) Write(p []byte) (int, error) { return w.enc.Write(p) }
|
||||||
|
|
||||||
|
func (w *WrappedBase64Encoder) Close() error {
|
||||||
|
return w.enc.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WrappedBase64Encoder) writeWrapped(p []byte) (int, error) {
|
||||||
if w.buf.Len() != 0 {
|
if w.buf.Len() != 0 {
|
||||||
panic("age: internal error: non-empty newlineWriter.buf")
|
panic("age: internal error: non-empty WrappedBase64Encoder.buf")
|
||||||
}
|
}
|
||||||
for len(p) > 0 {
|
for len(p) > 0 {
|
||||||
toWrite := ColumnsPerLine - (w.written % ColumnsPerLine)
|
toWrite := ColumnsPerLine - (w.written % ColumnsPerLine)
|
||||||
@@ -84,9 +99,18 @@ func (w *newlineWriter) Write(p []byte) (int, error) {
|
|||||||
return len(p), nil
|
return len(p), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LastLineIsEmpty returns whether the last output line was empty, either
|
||||||
|
// because no input was written, or because a multiple of BytesPerLine was.
|
||||||
|
//
|
||||||
|
// Calling LastLineIsEmpty before Close is meaningless.
|
||||||
|
func (w *WrappedBase64Encoder) LastLineIsEmpty() bool {
|
||||||
|
return w.written%ColumnsPerLine == 0
|
||||||
|
}
|
||||||
|
|
||||||
const intro = "age-encryption.org/v1\n"
|
const intro = "age-encryption.org/v1\n"
|
||||||
|
|
||||||
var recipientPrefix = []byte("->")
|
var recipientPrefix = []byte("->")
|
||||||
|
|
||||||
var footerPrefix = []byte("---")
|
var footerPrefix = []byte("---")
|
||||||
|
|
||||||
func (r *Stanza) Marshal(w io.Writer) error {
|
func (r *Stanza) Marshal(w io.Writer) error {
|
||||||
@@ -101,7 +125,7 @@ func (r *Stanza) Marshal(w io.Writer) error {
|
|||||||
if _, err := io.WriteString(w, "\n"); err != nil {
|
if _, err := io.WriteString(w, "\n"); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
ww := base64.NewEncoder(b64, NewlineWriter(w))
|
ww := NewWrappedBase64Encoder(b64, w)
|
||||||
if _, err := ww.Write(r.Body); err != nil {
|
if _, err := ww.Write(r.Body); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user