1/*
2 * MinIO Go Library for Amazon S3 Compatible Cloud Storage
3 * (C) 2018-2020 MinIO, Inc.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *     http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18package minio
19
20import (
21	"bytes"
22	"context"
23	"encoding/binary"
24	"encoding/xml"
25	"errors"
26	"fmt"
27	"hash"
28	"hash/crc32"
29	"io"
30	"net/http"
31	"net/url"
32	"strings"
33
34	"github.com/minio/minio-go/v7/pkg/encrypt"
35	"github.com/minio/minio-go/v7/pkg/s3utils"
36)
37
38// CSVFileHeaderInfo - is the parameter for whether to utilize headers.
39type CSVFileHeaderInfo string
40
41// Constants for file header info.
42const (
43	CSVFileHeaderInfoNone   CSVFileHeaderInfo = "NONE"
44	CSVFileHeaderInfoIgnore                   = "IGNORE"
45	CSVFileHeaderInfoUse                      = "USE"
46)
47
48// SelectCompressionType - is the parameter for what type of compression is
49// present
50type SelectCompressionType string
51
52// Constants for compression types under select API.
53const (
54	SelectCompressionNONE SelectCompressionType = "NONE"
55	SelectCompressionGZIP                       = "GZIP"
56	SelectCompressionBZIP                       = "BZIP2"
57)
58
59// CSVQuoteFields - is the parameter for how CSV fields are quoted.
60type CSVQuoteFields string
61
62// Constants for csv quote styles.
63const (
64	CSVQuoteFieldsAlways   CSVQuoteFields = "Always"
65	CSVQuoteFieldsAsNeeded                = "AsNeeded"
66)
67
68// QueryExpressionType - is of what syntax the expression is, this should only
69// be SQL
70type QueryExpressionType string
71
72// Constants for expression type.
73const (
74	QueryExpressionTypeSQL QueryExpressionType = "SQL"
75)
76
77// JSONType determines json input serialization type.
78type JSONType string
79
80// Constants for JSONTypes.
81const (
82	JSONDocumentType JSONType = "DOCUMENT"
83	JSONLinesType             = "LINES"
84)
85
86// ParquetInputOptions parquet input specific options
87type ParquetInputOptions struct{}
88
89// CSVInputOptions csv input specific options
90type CSVInputOptions struct {
91	FileHeaderInfo    CSVFileHeaderInfo
92	fileHeaderInfoSet bool
93
94	RecordDelimiter    string
95	recordDelimiterSet bool
96
97	FieldDelimiter    string
98	fieldDelimiterSet bool
99
100	QuoteCharacter    string
101	quoteCharacterSet bool
102
103	QuoteEscapeCharacter    string
104	quoteEscapeCharacterSet bool
105
106	Comments    string
107	commentsSet bool
108}
109
110// SetFileHeaderInfo sets the file header info in the CSV input options
111func (c *CSVInputOptions) SetFileHeaderInfo(val CSVFileHeaderInfo) {
112	c.FileHeaderInfo = val
113	c.fileHeaderInfoSet = true
114}
115
116// SetRecordDelimiter sets the record delimiter in the CSV input options
117func (c *CSVInputOptions) SetRecordDelimiter(val string) {
118	c.RecordDelimiter = val
119	c.recordDelimiterSet = true
120}
121
122// SetFieldDelimiter sets the field delimiter in the CSV input options
123func (c *CSVInputOptions) SetFieldDelimiter(val string) {
124	c.FieldDelimiter = val
125	c.fieldDelimiterSet = true
126}
127
128// SetQuoteCharacter sets the quote character in the CSV input options
129func (c *CSVInputOptions) SetQuoteCharacter(val string) {
130	c.QuoteCharacter = val
131	c.quoteCharacterSet = true
132}
133
134// SetQuoteEscapeCharacter sets the quote escape character in the CSV input options
135func (c *CSVInputOptions) SetQuoteEscapeCharacter(val string) {
136	c.QuoteEscapeCharacter = val
137	c.quoteEscapeCharacterSet = true
138}
139
140// SetComments sets the comments character in the CSV input options
141func (c *CSVInputOptions) SetComments(val string) {
142	c.Comments = val
143	c.commentsSet = true
144}
145
146// MarshalXML - produces the xml representation of the CSV input options struct
147func (c CSVInputOptions) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
148	if err := e.EncodeToken(start); err != nil {
149		return err
150	}
151	if c.FileHeaderInfo != "" || c.fileHeaderInfoSet {
152		if err := e.EncodeElement(c.FileHeaderInfo, xml.StartElement{Name: xml.Name{Local: "FileHeaderInfo"}}); err != nil {
153			return err
154		}
155	}
156
157	if c.RecordDelimiter != "" || c.recordDelimiterSet {
158		if err := e.EncodeElement(c.RecordDelimiter, xml.StartElement{Name: xml.Name{Local: "RecordDelimiter"}}); err != nil {
159			return err
160		}
161	}
162
163	if c.FieldDelimiter != "" || c.fieldDelimiterSet {
164		if err := e.EncodeElement(c.FieldDelimiter, xml.StartElement{Name: xml.Name{Local: "FieldDelimiter"}}); err != nil {
165			return err
166		}
167	}
168
169	if c.QuoteCharacter != "" || c.quoteCharacterSet {
170		if err := e.EncodeElement(c.QuoteCharacter, xml.StartElement{Name: xml.Name{Local: "QuoteCharacter"}}); err != nil {
171			return err
172		}
173	}
174
175	if c.QuoteEscapeCharacter != "" || c.quoteEscapeCharacterSet {
176		if err := e.EncodeElement(c.QuoteEscapeCharacter, xml.StartElement{Name: xml.Name{Local: "QuoteEscapeCharacter"}}); err != nil {
177			return err
178		}
179	}
180
181	if c.Comments != "" || c.commentsSet {
182		if err := e.EncodeElement(c.Comments, xml.StartElement{Name: xml.Name{Local: "Comments"}}); err != nil {
183			return err
184		}
185	}
186
187	return e.EncodeToken(xml.EndElement{Name: start.Name})
188}
189
190// CSVOutputOptions csv output specific options
191type CSVOutputOptions struct {
192	QuoteFields    CSVQuoteFields
193	quoteFieldsSet bool
194
195	RecordDelimiter    string
196	recordDelimiterSet bool
197
198	FieldDelimiter    string
199	fieldDelimiterSet bool
200
201	QuoteCharacter    string
202	quoteCharacterSet bool
203
204	QuoteEscapeCharacter    string
205	quoteEscapeCharacterSet bool
206}
207
208// SetQuoteFields sets the quote field parameter in the CSV output options
209func (c *CSVOutputOptions) SetQuoteFields(val CSVQuoteFields) {
210	c.QuoteFields = val
211	c.quoteFieldsSet = true
212}
213
214// SetRecordDelimiter sets the record delimiter character in the CSV output options
215func (c *CSVOutputOptions) SetRecordDelimiter(val string) {
216	c.RecordDelimiter = val
217	c.recordDelimiterSet = true
218}
219
220// SetFieldDelimiter sets the field delimiter character in the CSV output options
221func (c *CSVOutputOptions) SetFieldDelimiter(val string) {
222	c.FieldDelimiter = val
223	c.fieldDelimiterSet = true
224}
225
226// SetQuoteCharacter sets the quote character in the CSV output options
227func (c *CSVOutputOptions) SetQuoteCharacter(val string) {
228	c.QuoteCharacter = val
229	c.quoteCharacterSet = true
230}
231
232// SetQuoteEscapeCharacter sets the quote escape character in the CSV output options
233func (c *CSVOutputOptions) SetQuoteEscapeCharacter(val string) {
234	c.QuoteEscapeCharacter = val
235	c.quoteEscapeCharacterSet = true
236}
237
238// MarshalXML - produces the xml representation of the CSVOutputOptions struct
239func (c CSVOutputOptions) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
240	if err := e.EncodeToken(start); err != nil {
241		return err
242	}
243
244	if c.QuoteFields != "" || c.quoteFieldsSet {
245		if err := e.EncodeElement(c.QuoteFields, xml.StartElement{Name: xml.Name{Local: "QuoteFields"}}); err != nil {
246			return err
247		}
248	}
249
250	if c.RecordDelimiter != "" || c.recordDelimiterSet {
251		if err := e.EncodeElement(c.RecordDelimiter, xml.StartElement{Name: xml.Name{Local: "RecordDelimiter"}}); err != nil {
252			return err
253		}
254	}
255
256	if c.FieldDelimiter != "" || c.fieldDelimiterSet {
257		if err := e.EncodeElement(c.FieldDelimiter, xml.StartElement{Name: xml.Name{Local: "FieldDelimiter"}}); err != nil {
258			return err
259		}
260	}
261
262	if c.QuoteCharacter != "" || c.quoteCharacterSet {
263		if err := e.EncodeElement(c.QuoteCharacter, xml.StartElement{Name: xml.Name{Local: "QuoteCharacter"}}); err != nil {
264			return err
265		}
266	}
267
268	if c.QuoteEscapeCharacter != "" || c.quoteEscapeCharacterSet {
269		if err := e.EncodeElement(c.QuoteEscapeCharacter, xml.StartElement{Name: xml.Name{Local: "QuoteEscapeCharacter"}}); err != nil {
270			return err
271		}
272	}
273
274	return e.EncodeToken(xml.EndElement{Name: start.Name})
275}
276
277// JSONInputOptions json input specific options
278type JSONInputOptions struct {
279	Type    JSONType
280	typeSet bool
281}
282
283// SetType sets the JSON type in the JSON input options
284func (j *JSONInputOptions) SetType(typ JSONType) {
285	j.Type = typ
286	j.typeSet = true
287}
288
289// MarshalXML - produces the xml representation of the JSONInputOptions struct
290func (j JSONInputOptions) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
291	if err := e.EncodeToken(start); err != nil {
292		return err
293	}
294
295	if j.Type != "" || j.typeSet {
296		if err := e.EncodeElement(j.Type, xml.StartElement{Name: xml.Name{Local: "Type"}}); err != nil {
297			return err
298		}
299	}
300
301	return e.EncodeToken(xml.EndElement{Name: start.Name})
302}
303
304// JSONOutputOptions - json output specific options
305type JSONOutputOptions struct {
306	RecordDelimiter    string
307	recordDelimiterSet bool
308}
309
310// SetRecordDelimiter sets the record delimiter in the JSON output options
311func (j *JSONOutputOptions) SetRecordDelimiter(val string) {
312	j.RecordDelimiter = val
313	j.recordDelimiterSet = true
314}
315
316// MarshalXML - produces the xml representation of the JSONOutputOptions struct
317func (j JSONOutputOptions) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
318	if err := e.EncodeToken(start); err != nil {
319		return err
320	}
321
322	if j.RecordDelimiter != "" || j.recordDelimiterSet {
323		if err := e.EncodeElement(j.RecordDelimiter, xml.StartElement{Name: xml.Name{Local: "RecordDelimiter"}}); err != nil {
324			return err
325		}
326	}
327
328	return e.EncodeToken(xml.EndElement{Name: start.Name})
329}
330
331// SelectObjectInputSerialization - input serialization parameters
332type SelectObjectInputSerialization struct {
333	CompressionType SelectCompressionType
334	Parquet         *ParquetInputOptions `xml:"Parquet,omitempty"`
335	CSV             *CSVInputOptions     `xml:"CSV,omitempty"`
336	JSON            *JSONInputOptions    `xml:"JSON,omitempty"`
337}
338
339// SelectObjectOutputSerialization - output serialization parameters.
340type SelectObjectOutputSerialization struct {
341	CSV  *CSVOutputOptions  `xml:"CSV,omitempty"`
342	JSON *JSONOutputOptions `xml:"JSON,omitempty"`
343}
344
345// SelectObjectOptions - represents the input select body
346type SelectObjectOptions struct {
347	XMLName              xml.Name           `xml:"SelectObjectContentRequest" json:"-"`
348	ServerSideEncryption encrypt.ServerSide `xml:"-"`
349	Expression           string
350	ExpressionType       QueryExpressionType
351	InputSerialization   SelectObjectInputSerialization
352	OutputSerialization  SelectObjectOutputSerialization
353	RequestProgress      struct {
354		Enabled bool
355	}
356}
357
358// Header returns the http.Header representation of the SelectObject options.
359func (o SelectObjectOptions) Header() http.Header {
360	headers := make(http.Header)
361	if o.ServerSideEncryption != nil && o.ServerSideEncryption.Type() == encrypt.SSEC {
362		o.ServerSideEncryption.Marshal(headers)
363	}
364	return headers
365}
366
367// SelectObjectType - is the parameter which defines what type of object the
368// operation is being performed on.
369type SelectObjectType string
370
371// Constants for input data types.
372const (
373	SelectObjectTypeCSV     SelectObjectType = "CSV"
374	SelectObjectTypeJSON                     = "JSON"
375	SelectObjectTypeParquet                  = "Parquet"
376)
377
378// preludeInfo is used for keeping track of necessary information from the
379// prelude.
380type preludeInfo struct {
381	totalLen  uint32
382	headerLen uint32
383}
384
385// SelectResults is used for the streaming responses from the server.
386type SelectResults struct {
387	pipeReader *io.PipeReader
388	resp       *http.Response
389	stats      *StatsMessage
390	progress   *ProgressMessage
391}
392
393// ProgressMessage is a struct for progress xml message.
394type ProgressMessage struct {
395	XMLName xml.Name `xml:"Progress" json:"-"`
396	StatsMessage
397}
398
399// StatsMessage is a struct for stat xml message.
400type StatsMessage struct {
401	XMLName        xml.Name `xml:"Stats" json:"-"`
402	BytesScanned   int64
403	BytesProcessed int64
404	BytesReturned  int64
405}
406
407// messageType represents the type of message.
408type messageType string
409
410const (
411	errorMsg  messageType = "error"
412	commonMsg             = "event"
413)
414
415// eventType represents the type of event.
416type eventType string
417
418// list of event-types returned by Select API.
419const (
420	endEvent      eventType = "End"
421	recordsEvent            = "Records"
422	progressEvent           = "Progress"
423	statsEvent              = "Stats"
424)
425
426// contentType represents content type of event.
427type contentType string
428
429const (
430	xmlContent contentType = "text/xml"
431)
432
433// SelectObjectContent is a implementation of http://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectSELECTContent.html AWS S3 API.
434func (c Client) SelectObjectContent(ctx context.Context, bucketName, objectName string, opts SelectObjectOptions) (*SelectResults, error) {
435	// Input validation.
436	if err := s3utils.CheckValidBucketName(bucketName); err != nil {
437		return nil, err
438	}
439	if err := s3utils.CheckValidObjectName(objectName); err != nil {
440		return nil, err
441	}
442
443	selectReqBytes, err := xml.Marshal(opts)
444	if err != nil {
445		return nil, err
446	}
447
448	urlValues := make(url.Values)
449	urlValues.Set("select", "")
450	urlValues.Set("select-type", "2")
451
452	// Execute POST on bucket/object.
453	resp, err := c.executeMethod(ctx, http.MethodPost, requestMetadata{
454		bucketName:       bucketName,
455		objectName:       objectName,
456		queryValues:      urlValues,
457		customHeader:     opts.Header(),
458		contentMD5Base64: sumMD5Base64(selectReqBytes),
459		contentSHA256Hex: sum256Hex(selectReqBytes),
460		contentBody:      bytes.NewReader(selectReqBytes),
461		contentLength:    int64(len(selectReqBytes)),
462	})
463	if err != nil {
464		return nil, err
465	}
466
467	return NewSelectResults(resp, bucketName)
468}
469
470// NewSelectResults creates a Select Result parser that parses the response
471// and returns a Reader that will return parsed and assembled select output.
472func NewSelectResults(resp *http.Response, bucketName string) (*SelectResults, error) {
473	if resp.StatusCode != http.StatusOK {
474		return nil, httpRespToErrorResponse(resp, bucketName, "")
475	}
476
477	pipeReader, pipeWriter := io.Pipe()
478	streamer := &SelectResults{
479		resp:       resp,
480		stats:      &StatsMessage{},
481		progress:   &ProgressMessage{},
482		pipeReader: pipeReader,
483	}
484	streamer.start(pipeWriter)
485	return streamer, nil
486}
487
488// Close - closes the underlying response body and the stream reader.
489func (s *SelectResults) Close() error {
490	defer closeResponse(s.resp)
491	return s.pipeReader.Close()
492}
493
494// Read - is a reader compatible implementation for SelectObjectContent records.
495func (s *SelectResults) Read(b []byte) (n int, err error) {
496	return s.pipeReader.Read(b)
497}
498
499// Stats - information about a request's stats when processing is complete.
500func (s *SelectResults) Stats() *StatsMessage {
501	return s.stats
502}
503
504// Progress - information about the progress of a request.
505func (s *SelectResults) Progress() *ProgressMessage {
506	return s.progress
507}
508
509// start is the main function that decodes the large byte array into
510// several events that are sent through the eventstream.
511func (s *SelectResults) start(pipeWriter *io.PipeWriter) {
512	go func() {
513		for {
514			var prelude preludeInfo
515			var headers = make(http.Header)
516			var err error
517
518			// Create CRC code
519			crc := crc32.New(crc32.IEEETable)
520			crcReader := io.TeeReader(s.resp.Body, crc)
521
522			// Extract the prelude(12 bytes) into a struct to extract relevant information.
523			prelude, err = processPrelude(crcReader, crc)
524			if err != nil {
525				pipeWriter.CloseWithError(err)
526				closeResponse(s.resp)
527				return
528			}
529
530			// Extract the headers(variable bytes) into a struct to extract relevant information
531			if prelude.headerLen > 0 {
532				if err = extractHeader(io.LimitReader(crcReader, int64(prelude.headerLen)), headers); err != nil {
533					pipeWriter.CloseWithError(err)
534					closeResponse(s.resp)
535					return
536				}
537			}
538
539			// Get the actual payload length so that the appropriate amount of
540			// bytes can be read or parsed.
541			payloadLen := prelude.PayloadLen()
542
543			m := messageType(headers.Get("message-type"))
544
545			switch m {
546			case errorMsg:
547				pipeWriter.CloseWithError(errors.New(headers.Get("error-code") + ":\"" + headers.Get("error-message") + "\""))
548				closeResponse(s.resp)
549				return
550			case commonMsg:
551				// Get content-type of the payload.
552				c := contentType(headers.Get("content-type"))
553
554				// Get event type of the payload.
555				e := eventType(headers.Get("event-type"))
556
557				// Handle all supported events.
558				switch e {
559				case endEvent:
560					pipeWriter.Close()
561					closeResponse(s.resp)
562					return
563				case recordsEvent:
564					if _, err = io.Copy(pipeWriter, io.LimitReader(crcReader, payloadLen)); err != nil {
565						pipeWriter.CloseWithError(err)
566						closeResponse(s.resp)
567						return
568					}
569				case progressEvent:
570					switch c {
571					case xmlContent:
572						if err = xmlDecoder(io.LimitReader(crcReader, payloadLen), s.progress); err != nil {
573							pipeWriter.CloseWithError(err)
574							closeResponse(s.resp)
575							return
576						}
577					default:
578						pipeWriter.CloseWithError(fmt.Errorf("Unexpected content-type %s sent for event-type %s", c, progressEvent))
579						closeResponse(s.resp)
580						return
581					}
582				case statsEvent:
583					switch c {
584					case xmlContent:
585						if err = xmlDecoder(io.LimitReader(crcReader, payloadLen), s.stats); err != nil {
586							pipeWriter.CloseWithError(err)
587							closeResponse(s.resp)
588							return
589						}
590					default:
591						pipeWriter.CloseWithError(fmt.Errorf("Unexpected content-type %s sent for event-type %s", c, statsEvent))
592						closeResponse(s.resp)
593						return
594					}
595				}
596			}
597
598			// Ensures that the full message's CRC is correct and
599			// that the message is not corrupted
600			if err := checkCRC(s.resp.Body, crc.Sum32()); err != nil {
601				pipeWriter.CloseWithError(err)
602				closeResponse(s.resp)
603				return
604			}
605
606		}
607	}()
608}
609
610// PayloadLen is a function that calculates the length of the payload.
611func (p preludeInfo) PayloadLen() int64 {
612	return int64(p.totalLen - p.headerLen - 16)
613}
614
615// processPrelude is the function that reads the 12 bytes of the prelude and
616// ensures the CRC is correct while also extracting relevant information into
617// the struct,
618func processPrelude(prelude io.Reader, crc hash.Hash32) (preludeInfo, error) {
619	var err error
620	var pInfo = preludeInfo{}
621
622	// reads total length of the message (first 4 bytes)
623	pInfo.totalLen, err = extractUint32(prelude)
624	if err != nil {
625		return pInfo, err
626	}
627
628	// reads total header length of the message (2nd 4 bytes)
629	pInfo.headerLen, err = extractUint32(prelude)
630	if err != nil {
631		return pInfo, err
632	}
633
634	// checks that the CRC is correct (3rd 4 bytes)
635	preCRC := crc.Sum32()
636	if err := checkCRC(prelude, preCRC); err != nil {
637		return pInfo, err
638	}
639
640	return pInfo, nil
641}
642
643// extracts the relevant information from the Headers.
644func extractHeader(body io.Reader, myHeaders http.Header) error {
645	for {
646		// extracts the first part of the header,
647		headerTypeName, err := extractHeaderType(body)
648		if err != nil {
649			// Since end of file, we have read all of our headers
650			if err == io.EOF {
651				break
652			}
653			return err
654		}
655
656		// reads the 7 present in the header and ignores it.
657		extractUint8(body)
658
659		headerValueName, err := extractHeaderValue(body)
660		if err != nil {
661			return err
662		}
663
664		myHeaders.Set(headerTypeName, headerValueName)
665
666	}
667	return nil
668}
669
670// extractHeaderType extracts the first half of the header message, the header type.
671func extractHeaderType(body io.Reader) (string, error) {
672	// extracts 2 bit integer
673	headerNameLen, err := extractUint8(body)
674	if err != nil {
675		return "", err
676	}
677	// extracts the string with the appropriate number of bytes
678	headerName, err := extractString(body, int(headerNameLen))
679	if err != nil {
680		return "", err
681	}
682	return strings.TrimPrefix(headerName, ":"), nil
683}
684
685// extractsHeaderValue extracts the second half of the header message, the
686// header value
687func extractHeaderValue(body io.Reader) (string, error) {
688	bodyLen, err := extractUint16(body)
689	if err != nil {
690		return "", err
691	}
692	bodyName, err := extractString(body, int(bodyLen))
693	if err != nil {
694		return "", err
695	}
696	return bodyName, nil
697}
698
699// extracts a string from byte array of a particular number of bytes.
700func extractString(source io.Reader, lenBytes int) (string, error) {
701	myVal := make([]byte, lenBytes)
702	_, err := source.Read(myVal)
703	if err != nil {
704		return "", err
705	}
706	return string(myVal), nil
707}
708
709// extractUint32 extracts a 4 byte integer from the byte array.
710func extractUint32(r io.Reader) (uint32, error) {
711	buf := make([]byte, 4)
712	_, err := readFull(r, buf)
713	if err != nil {
714		return 0, err
715	}
716	return binary.BigEndian.Uint32(buf), nil
717}
718
719// extractUint16 extracts a 2 byte integer from the byte array.
720func extractUint16(r io.Reader) (uint16, error) {
721	buf := make([]byte, 2)
722	_, err := readFull(r, buf)
723	if err != nil {
724		return 0, err
725	}
726	return binary.BigEndian.Uint16(buf), nil
727}
728
729// extractUint8 extracts a 1 byte integer from the byte array.
730func extractUint8(r io.Reader) (uint8, error) {
731	buf := make([]byte, 1)
732	_, err := readFull(r, buf)
733	if err != nil {
734		return 0, err
735	}
736	return buf[0], nil
737}
738
739// checkCRC ensures that the CRC matches with the one from the reader.
740func checkCRC(r io.Reader, expect uint32) error {
741	msgCRC, err := extractUint32(r)
742	if err != nil {
743		return err
744	}
745
746	if msgCRC != expect {
747		return fmt.Errorf("Checksum Mismatch, MessageCRC of 0x%X does not equal expected CRC of 0x%X", msgCRC, expect)
748
749	}
750	return nil
751}
752