Skip to content

Commit 894e3fa

Browse files
committed
Refactor ota package (#97)
Return a specific 'Econder' struct instead of a 'WriteCloser' interface. The 'Encoder' wraps a 'io.Writer' that's where the encoded file will be written to. In this way the consumer can decide when and how to open/close the writer. Add tests.
1 parent dfb0d83 commit 894e3fa

13 files changed

+101
-96
lines changed

command/ota/generate.go

+6-7
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@
1818
package ota
1919

2020
import (
21-
"bytes"
2221
"errors"
22+
"fmt"
2323
"io/ioutil"
2424
"os"
2525

@@ -51,17 +51,16 @@ func Generate(binFile string, outFile string, fqbn string) error {
5151
return err
5252
}
5353

54-
var w bytes.Buffer
55-
otaWriter := inota.NewWriter(&w, arduinoVendorID, productID)
56-
_, err = otaWriter.Write(data)
54+
out, err := os.Create(outFile)
5755
if err != nil {
5856
return err
5957
}
60-
otaWriter.Close()
58+
defer out.Close()
6159

62-
err = ioutil.WriteFile(outFile, w.Bytes(), os.FileMode(0644))
60+
enc := inota.NewEncoder(out, arduinoVendorID, productID)
61+
err = enc.Encode(data)
6362
if err != nil {
64-
return err
63+
return fmt.Errorf("failed to encode binary file: %w", err)
6564
}
6665

6766
return nil

command/ota/massupload.go

+1
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ func run(uploader otaUploader, ids []string, otaFile string, expiration int) []R
171171
results = append(results, r)
172172
continue
173173
}
174+
defer file.Close()
174175
jobs <- job{id: id, file: file}
175176
}
176177
close(jobs)

command/ota/upload.go

+1
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ func Upload(params *UploadParams, cred *config.Credentials) error {
7171
if err != nil {
7272
return fmt.Errorf("%s: %w", "cannot open ota file", err)
7373
}
74+
defer file.Close()
7475

7576
expiration := otaExpirationMins
7677
if params.Deferred {

internal/ota/encoder.go

+36-66
Original file line numberDiff line numberDiff line change
@@ -18,60 +18,48 @@
1818
package ota
1919

2020
import (
21-
"bufio"
2221
"encoding/binary"
22+
"fmt"
2323
"hash/crc32"
2424
"io"
2525
"strconv"
2626

2727
"github.com/arduino/arduino-cloud-cli/internal/lzss"
28-
"github.com/juju/errors"
2928
)
3029

31-
// A writer is a buffered, flushable writer.
32-
type writer interface {
33-
io.Writer
34-
Flush() error
35-
}
36-
37-
// encoder encodes a binary into an .ota file.
38-
type encoder struct {
39-
// w is the writer that compressed bytes are written to.
40-
w writer
30+
// Encoder writes a binary to an output stream in the ota format.
31+
type Encoder struct {
32+
// w is the stream where encoded bytes are written.
33+
w io.Writer
4134

42-
// vendorID is the ID of the board vendor
35+
// vendorID is the ID of the board vendor.
4336
vendorID string
4437

45-
// is the ID of the board vendor is the ID of the board model
38+
// productID is the ID of the board model.
4639
productID string
4740
}
4841

49-
// NewWriter creates a new `WriteCloser` for the the given VID/PID.
50-
func NewWriter(w io.Writer, vendorID, productID string) io.WriteCloser {
51-
bw, ok := w.(writer)
52-
if !ok {
53-
bw = bufio.NewWriter(w)
54-
}
55-
return &encoder{
56-
w: bw,
42+
// NewEncoder creates a new ota encoder.
43+
func NewEncoder(w io.Writer, vendorID, productID string) *Encoder {
44+
return &Encoder{
45+
w: w,
5746
vendorID: vendorID,
5847
productID: productID,
5948
}
6049
}
6150

62-
// Write writes a compressed representation of p to e's underlying writer.
63-
func (e *encoder) Write(binaryData []byte) (int, error) {
64-
//log.Println("original binaryData is", len(binaryData), "bytes length")
65-
66-
// Magic number (VID/PID)
51+
// Encode compresses data using a lzss algorithm, encodes the result
52+
// in ota format and writes it to e's underlying writer.
53+
func (e *Encoder) Encode(data []byte) error {
54+
// Compute the magic number (VID/PID)
6755
magicNumber := make([]byte, 4)
6856
vid, err := strconv.ParseUint(e.vendorID, 16, 16)
6957
if err != nil {
70-
return 0, errors.Annotate(err, "OTA encoder: failed to parse vendorID")
58+
return fmt.Errorf("cannot parse vendorID: %w", err)
7159
}
7260
pid, err := strconv.ParseUint(e.productID, 16, 16)
7361
if err != nil {
74-
return 0, errors.Annotate(err, "OTA encoder: failed to parse productID")
62+
return fmt.Errorf("cannot parse productID: %w", err)
7563
}
7664

7765
binary.LittleEndian.PutUint16(magicNumber[0:2], uint16(pid))
@@ -82,61 +70,43 @@ func (e *encoder) Write(binaryData []byte) (int, error) {
8270
Compression: true,
8371
}
8472

85-
// Compress the compiled binary
86-
compressed := lzss.Encode(binaryData)
87-
73+
compressed := lzss.Encode(data)
8874
// Prepend magic number and version field to payload
89-
var binDataComplete []byte
90-
binDataComplete = append(binDataComplete, magicNumber...)
91-
binDataComplete = append(binDataComplete, version.AsBytes()...)
92-
binDataComplete = append(binDataComplete, compressed...)
93-
//log.Println("binDataComplete is", len(binDataComplete), "bytes length")
75+
var outData []byte
76+
outData = append(outData, magicNumber...)
77+
outData = append(outData, version.Bytes()...)
78+
outData = append(outData, compressed...)
9479

95-
headerSize, err := e.writeHeader(binDataComplete)
80+
err = e.writeHeader(outData)
9681
if err != nil {
97-
return headerSize, err
82+
return fmt.Errorf("cannot write data header to output stream: %w", err)
9883
}
9984

100-
payloadSize, err := e.writePayload(binDataComplete)
85+
_, err = e.w.Write(outData)
10186
if err != nil {
102-
return payloadSize, err
87+
return fmt.Errorf("cannot write encoded data to output stream: %w", err)
10388
}
10489

105-
return headerSize + payloadSize, nil
106-
}
107-
108-
// Close closes the encoder, flushing any pending output. It does not close or
109-
// flush e's underlying writer.
110-
func (e *encoder) Close() error {
111-
return e.w.Flush()
90+
return nil
11291
}
11392

114-
func (e *encoder) writeHeader(binDataComplete []byte) (int, error) {
115-
93+
func (e *Encoder) writeHeader(data []byte) error {
11694
// Write the length of the content
11795
lengthAsBytes := make([]byte, 4)
118-
binary.LittleEndian.PutUint32(lengthAsBytes, uint32(len(binDataComplete)))
119-
120-
n, err := e.w.Write(lengthAsBytes)
96+
binary.LittleEndian.PutUint32(lengthAsBytes, uint32(len(data)))
97+
_, err := e.w.Write(lengthAsBytes)
12198
if err != nil {
122-
return n, err
99+
return err
123100
}
124101

125-
// Calculate the checksum for binDataComplete
126-
crc := crc32.ChecksumIEEE(binDataComplete)
127-
128-
// encode the checksum uint32 value as 4 bytes
102+
// Write the checksum uint32 value as 4 bytes
103+
crc := crc32.ChecksumIEEE(data)
129104
crcAsBytes := make([]byte, 4)
130105
binary.LittleEndian.PutUint32(crcAsBytes, crc)
131-
132-
n, err = e.w.Write(crcAsBytes)
106+
_, err = e.w.Write(crcAsBytes)
133107
if err != nil {
134-
return n, err
108+
return err
135109
}
136110

137-
return len(lengthAsBytes) + len(crcAsBytes), nil
138-
}
139-
140-
func (e *encoder) writePayload(data []byte) (int, error) {
141-
return e.w.Write(data)
111+
return nil
142112
}

internal/ota/encoder_test.go

+54-11
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ package ota
2020
import (
2121
"bytes"
2222
"encoding/hex"
23-
"log"
23+
"io/ioutil"
2424

2525
"fmt"
2626
"hash/crc32"
@@ -37,30 +37,25 @@ func TestComputeCrc32Checksum(t *testing.T) {
3737
assert.Equal(t, crc, uint32(2090640218))
3838
}
3939

40-
func TestEncoderWrite(t *testing.T) {
41-
40+
func TestEncode(t *testing.T) {
4241
// Setup test data
4342
data, _ := hex.DecodeString("DEADBEEF") // uncompressed, or 'ef 6b 77 de f0' (compressed w/ LZSS)
4443

4544
var w bytes.Buffer
4645
vendorID := "2341" // Arduino
4746
productID := "8054" // MRK Wifi 1010
4847

49-
otaWriter := NewWriter(&w, vendorID, productID)
50-
defer otaWriter.Close()
48+
enc := NewEncoder(&w, vendorID, productID)
5149

52-
n, err := otaWriter.Write(data)
50+
err := enc.Encode(data)
5351
if err != nil {
5452
t.Error(err)
55-
t.Fail()
5653
}
57-
log.Println("written ota of", n, "bytes length")
5854

59-
otaWriter.Close()
6055
actual := w.Bytes()
6156

62-
// You can get the expected result creating an `.ota` file using Alex's tools:
63-
// https://github.com/arduino-libraries/ArduinoIoTCloud/tree/master/extras/tools
57+
// Expected result has been computed with the following tool:
58+
// https://github.com/arduino-libraries/ArduinoIoTCloud/tree/master/extras/tools .
6459
expected, _ := hex.DecodeString("11000000a1744bd4548041230000000000000040ef6b77def0")
6560

6661
res := bytes.Compare(expected, actual)
@@ -72,3 +67,51 @@ func TestEncoderWrite(t *testing.T) {
7267

7368
assert.Assert(t, res == 0) // 0 means equal
7469
}
70+
71+
// Expected '.ota' files contained in testdata have been computed with the following tool:
72+
// https://github.com/arduino-libraries/ArduinoIoTCloud/tree/master/extras/tools .
73+
func TestEncodeFiles(t *testing.T) {
74+
tests := []struct {
75+
name string
76+
infile string
77+
outfile string
78+
}{
79+
{
80+
name: "blink",
81+
infile: "testdata/blink.bin",
82+
outfile: "testdata/blink.ota",
83+
},
84+
{
85+
name: "cloud sketch",
86+
infile: "testdata/cloud.bin",
87+
outfile: "testdata/cloud.ota",
88+
},
89+
}
90+
91+
for _, tt := range tests {
92+
t.Run(tt.name, func(t *testing.T) {
93+
input, err := ioutil.ReadFile(tt.infile)
94+
if err != nil {
95+
t.Fatal("couldn't open test file")
96+
}
97+
98+
want, err := ioutil.ReadFile(tt.outfile)
99+
if err != nil {
100+
t.Fatal("couldn't open test file")
101+
}
102+
103+
var got bytes.Buffer
104+
vendorID := "2341" // Arduino
105+
productID := "8057" // Nano 33 IoT
106+
otaenc := NewEncoder(&got, vendorID, productID)
107+
err = otaenc.Encode(input)
108+
if err != nil {
109+
t.Error(err)
110+
}
111+
112+
if !bytes.Equal(want, got.Bytes()) {
113+
t.Error("encoding failed")
114+
}
115+
})
116+
}
117+
}

internal/ota/testdata/blink.bin

11.2 KB
Binary file not shown.

internal/ota/testdata/blink.ota

9.38 KB
Binary file not shown.

internal/ota/testdata/cloud.bin

90 KB
Binary file not shown.

internal/ota/testdata/cloud.ota

73.2 KB
Binary file not shown.

internal/ota/testdata/lorem.lzss

-1.7 KB
Binary file not shown.

internal/ota/testdata/lorem.txt

-9
This file was deleted.

internal/ota/version.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ type Version struct {
3232
PayloadBuildNum uint32
3333
}
3434

35-
// AsBytes builds a 8 byte length representation of the Version Struct for the OTA update.
36-
func (v *Version) AsBytes() []byte {
35+
// Bytes builds a 8 byte length representation of the Version Struct for the OTA update.
36+
func (v *Version) Bytes() []byte {
3737
version := []byte{0, 0, 0, 0, 0, 0, 0, 0}
3838

3939
// Set compression

internal/ota/version_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ func TestVersionWithCompressionEnabled(t *testing.T) {
3434
}
3535

3636
expected := []byte{0, 0, 0, 0, 0, 0, 0, 0x40}
37-
actual := version.AsBytes()
37+
actual := version.Bytes()
3838

3939
// create a tabwriter for formatting the output
4040
w := new(tabwriter.Writer)

0 commit comments

Comments
 (0)