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