diff --git a/airgapped/airgapped.go b/airgapped/airgapped.go index e183dd6..33a907c 100644 --- a/airgapped/airgapped.go +++ b/airgapped/airgapped.go @@ -216,7 +216,7 @@ func (am *AirgappedMachine) HandleOperation(operation client.Operation) (client. } // HandleQR - gets an operation from a QR code, do necessary things for the operation and returns paths to QR-code images -func (am *AirgappedMachine) HandleQR() (string, error) { +func (am *AirgappedMachine) HandleQR() ([]string, error) { var ( err error @@ -227,28 +227,38 @@ func (am *AirgappedMachine) HandleQR() (string, error) { resultOperation client.Operation ) - if qrData, err = am.qrProcessor.ReadQR(); err != nil { - return "", fmt.Errorf("failed to read QR: %w", err) + if qrData, err = qr.ReadDataFromQRChunks(am.qrProcessor); err != nil { + return nil, fmt.Errorf("failed to read QR: %w", err) } if err = json.Unmarshal(qrData, &operation); err != nil { - return "", fmt.Errorf("failed to unmarshal operation: %w", err) + return nil, fmt.Errorf("failed to unmarshal operation: %w", err) } if resultOperation, err = am.HandleOperation(operation); err != nil { - return "", err + return nil, err } - qrPath := fmt.Sprintf("%s/%s_%s_%s.png", resultQRFolder, resultOperation.Type, resultOperation.ID, resultOperation.To) operationBz, err := json.Marshal(resultOperation) if err != nil { - return "", fmt.Errorf("failed to marshal operation: %w", err) + return nil, fmt.Errorf("failed to marshal operation: %w", err) } - if err = am.qrProcessor.WriteQR(qrPath, operationBz); err != nil { - return "", fmt.Errorf("failed to write QR") + chunks, err := qr.DataToChunks(operationBz) + if err != nil { + return nil, fmt.Errorf("failed to divide a data on chunks: %w", err) + } + qrPaths := make([]string, 0, len(chunks)) + + for idx, chunk := range chunks { + qrPath := fmt.Sprintf("%s/%s_%s_%s-%d.png", resultQRFolder, resultOperation.Type, resultOperation.ID, + resultOperation.To, idx) + if err = am.qrProcessor.WriteQR(qrPath, chunk); err != nil { + return nil, fmt.Errorf("failed to write QR") + } + qrPaths = append(qrPaths, qrPath) } - return qrPath, nil + return qrPaths, nil } func (am *AirgappedMachine) writeErrorRequestToOperation(o *client.Operation, handlerError error) error { diff --git a/client/client.go b/client/client.go index e16696d..f45473f 100644 --- a/client/client.go +++ b/client/client.go @@ -39,7 +39,7 @@ type Poller interface { SendMessage(message storage.Message) error ProcessMessage(message storage.Message) error GetOperations() (map[string]*types.Operation, error) - GetOperationQRPath(operationID string) (string, error) + GetOperationQRPath(operationID string) ([]string, error) ReadProcessedOperation() error StartHTTPServer(listenAddr string) error GetLogger() *logger @@ -253,25 +253,36 @@ func (c *Client) getOperationJSON(operationID string) ([]byte, error) { // GetOperationQRPath returns a path to the image with the QR generated // for the specified operation. It is supposed that the user will open // this file herself. -func (c *Client) GetOperationQRPath(operationID string) (string, error) { +func (c *Client) GetOperationQRPath(operationID string) ([]string, error) { operationJSON, err := c.getOperationJSON(operationID) if err != nil { - return "", fmt.Errorf("failed to get operation in JSON: %w", err) + return nil, fmt.Errorf("failed to get operation in JSON: %w", err) } operationQRPath := filepath.Join(QrCodesDir, operationID) - if err := c.qrProcessor.WriteQR(operationQRPath, operationJSON); err != nil { - return "", fmt.Errorf("failed to WriteQR: %w", err) + chunks, err := qr.DataToChunks(operationJSON) + if err != nil { + return nil, fmt.Errorf("failed to divide a data on chunks: %w", err) } - return operationQRPath, nil + qrs := make([]string, 0, len(chunks)) + + for idx, chunk := range chunks { + qrPath := fmt.Sprintf("%s-%d", operationQRPath, idx) + if err = c.qrProcessor.WriteQR(qrPath, chunk); err != nil { + return nil, err + } + qrs = append(qrs, qrPath) + } + + return qrs, nil } // ReadProcessedOperation reads the processed operation from camera, checks that // the processed operation has its unprocessed counterpart in our state, // posts a Message to the storage and deletes the operation from our state. func (c *Client) ReadProcessedOperation() error { - bz, err := c.qrProcessor.ReadQR() + bz, err := qr.ReadDataFromQRChunks(c.qrProcessor) if err != nil { return fmt.Errorf("failed to ReadQR: %s", err) } diff --git a/client/http_server.go b/client/http_server.go index 3175cf7..f787872 100644 --- a/client/http_server.go +++ b/client/http_server.go @@ -123,13 +123,13 @@ func (c *Client) getOperationQRPathHandler(w http.ResponseWriter, r *http.Reques } operationID := r.URL.Query().Get("operationID") - qrPath, err := c.GetOperationQRPath(operationID) + qrPaths, err := c.GetOperationQRPath(operationID) if err != nil { errorResponse(w, http.StatusInternalServerError, fmt.Sprintf("failed to get operation QR path: %v", err)) return } - successResponse(w, qrPath) + successResponse(w, qrPaths) } func (c *Client) getOperationQRToBodyHandler(w http.ResponseWriter, r *http.Request) { diff --git a/qr/chunk.go b/qr/chunk.go new file mode 100644 index 0000000..7b8f964 --- /dev/null +++ b/qr/chunk.go @@ -0,0 +1,79 @@ +package qr + +import ( + "bytes" + "encoding/gob" + "fmt" + "math" +) + +const chunkSize = 1024 + +type chunk struct { + Data []byte + IsFinal bool +} + +func ReadDataFromQRChunks(p Processor) ([]byte, error) { + var ( + fullData, chunkBz []byte + err error + ) + for { + chunkBz, err = p.ReadQR() + if err != nil { + return nil, err + } + chunk, err := decodeChunk(chunkBz) + if err != nil { + return nil, fmt.Errorf("failed to decode chunk: %w", err) + } + fullData = append(fullData, chunk.Data...) + if chunk.IsFinal { + return fullData, nil + } + } +} + +func DataToChunks(data []byte) ([][]byte, error) { + chunksCount := int(math.Ceil(float64(len(data)) / chunkSize)) + chunks := make([][]byte, 0, chunksCount) + + for offset := 0; offset < len(data); offset += chunkSize { + offsetEnd := offset + chunkSize + if offsetEnd > len(data) { + offsetEnd = len(data) + } + isFinal := offsetEnd == len(data) + chunkBz, err := encodeChunk(chunk{ + Data: data[offset:offsetEnd], + IsFinal: isFinal, + }) + if err != nil { + return nil, fmt.Errorf("failed to encode chunk: %w", err) + } + chunks = append(chunks, chunkBz) + } + return chunks, nil +} + +func decodeChunk(data []byte) (*chunk, error) { + var ( + c chunk + err error + ) + dec := gob.NewDecoder(bytes.NewBuffer(data)) + if err = dec.Decode(&c); err != nil { + return nil, fmt.Errorf("failed to decode chunk: %w", err) + } + return &c, nil +} + +func encodeChunk(c chunk) ([]byte, error) { + buf := bytes.NewBuffer(nil) + enc := gob.NewEncoder(buf) + if err := enc.Encode(c); err != nil { + return nil, fmt.Errorf("failed to encode chunk: %w", err) + } + return buf.Bytes(), nil +} diff --git a/qr/qr.go b/qr/qr.go index 198154f..d189158 100644 --- a/qr/qr.go +++ b/qr/qr.go @@ -93,7 +93,7 @@ func ReadDataFromQR(img image.Image) ([]byte, error) { if err != nil { return nil, fmt.Errorf("failed to decode the QR-code contents: %w", err) } - return result.GetRawBytes(), nil + return []byte(result.String()), nil } func EncodeQR(data []byte) ([]byte, error) { diff --git a/qr/qr_test.go b/qr/qr_test.go new file mode 100644 index 0000000..715d168 --- /dev/null +++ b/qr/qr_test.go @@ -0,0 +1,94 @@ +package qr + +import ( + "fmt" + encoder "github.com/skip2/go-qrcode" + "image" + "math/rand" + "os" + "reflect" + "testing" +) + +type TestQrProcessor struct { + qrs []string +} + +func NewTestQRProcessor() *TestQrProcessor { + return &TestQrProcessor{} +} + +func (p *TestQrProcessor) ReadQR() ([]byte, error) { + if len(p.qrs) == 0 { + return nil, fmt.Errorf("qr not found") + } + qr := p.qrs[0] + file, err := os.Open(qr) + if err != nil { + return nil, err + } + defer file.Close() + + img, _, err := image.Decode(file) + if err != nil { + return nil, err + } + + data, err := ReadDataFromQR(img) + if err != nil { + return nil, err + } + + p.qrs = p.qrs[1:] + if err = os.Remove(qr); err != nil { + return nil, err + } + return data, nil +} + +func (p *TestQrProcessor) WriteQR(path string, data []byte) error { + err := encoder.WriteFile(string(data), encoder.Medium, 512, path) + if err != nil { + return fmt.Errorf("failed to encode the data: %w", err) + } + + p.qrs = append(p.qrs, path) + return nil +} + +func genBytes(n int) []byte { + data := make([]byte, n) + if _, err := rand.Read(data); err != nil { + return nil + } + return data +} + +func TestReadDataFromQRChunks(t *testing.T) { + N := 5000 + + data := genBytes(N) + + chunks, err := DataToChunks(data) + if err != nil { + t.Fatalf(err.Error()) + } + + p := NewTestQRProcessor() + + for idx, chunk := range chunks { + path := fmt.Sprintf("/tmp/%d.png", idx) + if err = p.WriteQR(path, chunk); err != nil { + t.Fatalf(err.Error()) + } + } + + recoveredDataFromQRChunks, err := ReadDataFromQRChunks(p) + if err != nil { + t.Fatalf(err.Error()) + } + + if !reflect.DeepEqual(data, recoveredDataFromQRChunks) { + t.Fatal("recovered data from chunks and initial data are not equal!") + } +}