diff --git a/parse.go b/parse.go index c09faa0b..984becc0 100644 --- a/parse.go +++ b/parse.go @@ -178,10 +178,9 @@ func NewParser(in io.Reader, bytesToRead int64, frameChannel chan *frame.Frame, // No transfer syntax found, so let's try to infer the transfer syntax by // trying to read the next element under various transfer syntaxes. - next100, err := p.reader.rawReader.Peek(100) - if errors.Is(err, io.EOF) { - // DICOM is shorter than 100 bytes. - return nil, fmt.Errorf("dicom with missing transfer syntax metadata is shorter than 100 bytes, so cannot infer transfer syntax") + next100, err := p.reader.rawReader.PeekAtMost(100) + if err != nil { + return nil, fmt.Errorf("could not peek at next 100 bytes, so cannot infer transfer syntax: %w", err) } syntaxes := []struct { diff --git a/parse_test.go b/parse_test.go index 498fd892..b3f40e3d 100644 --- a/parse_test.go +++ b/parse_test.go @@ -72,6 +72,65 @@ func TestParseUntilEOF(t *testing.T) { } } +// TestParse_CEchoRQ is a test for parsing a CEchoRQ command. It checks that +// the command is parsed correctly and that the expected tags are present in +// the dataset. These commands are typically very small dicom fragments, and +// ensure that our parsing and transfer syntax inference logic work correctly. +func TestParse_CEchoRQ(t *testing.T) { + commandBytes := []byte{ + // Command Group Length + 0x00, 0x00, //Tag Group + 0x00, 0x00, //Tag Element + 0x04, 0x00, 0x00, 0x00, //VL + 0x38, 0x00, 0x00, 0x00, //Value + + // AffectedSOPClassUID + 0x00, 0x00, //Tag Group + 0x02, 0x00, //Tag Element + 0x12, 0x00, 0x00, 0x00, //VL + 0x31, 0x2E, 0x32, 0x2E, 0x38, 0x34, 0x30, 0x2E, 0x31, //Value + 0x30, 0x30, 0x30, 0x38, 0x2E, 0x31, 0x2E, 0x31, 0x00, //Value + + //CommandField + 0x00, 0x00, //Tag Group + 0x00, 0x01, //Tag Element + 0x02, 0x00, 0x00, 0x00, //VL + 0x30, 0x00, //Value + + //MessageID + 0x00, 0x00, //Tag Group + 0x10, 0x01, //Tag Element + 0x02, 0x00, 0x00, 0x00, //VL + 0x01, 0x00, //Value + + //CommandDataSetType + 0x00, 0x00, //Tag Group + 0x00, 0x08, //Tag Element + 0x02, 0x00, 0x00, 0x00, //VL + 0x01, 0x01, //Value + } + + ioReader := bytes.NewReader(commandBytes) + dataset, err := dicom.Parse(ioReader, int64(len(commandBytes)), nil, dicom.SkipPixelData(), dicom.SkipMetadataReadOnNewParserInit()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + tags := []tag.Tag{ + {Group: 0x0000, Element: 0x0000}, + {Group: 0x0000, Element: 0x0002}, + {Group: 0x0000, Element: 0x0100}, + {Group: 0x0000, Element: 0x0110}, + {Group: 0x0000, Element: 0x0800}, + } + for _, tag := range tags { + _, err := dataset.FindElementByTag(tag) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + } + +} + // TestNewParserSkipMetadataReadOnNewParserInit tests that NewParser with the SkipMetadataReadOnNewParserInit option // parses the specified dataset but not its header metadata. func TestNewParserSkipMetadataReadOnNewParserInit(t *testing.T) { diff --git a/pkg/dicomio/reader.go b/pkg/dicomio/reader.go index dd64c2a2..3de44379 100644 --- a/pkg/dicomio/reader.go +++ b/pkg/dicomio/reader.go @@ -231,3 +231,18 @@ func (r *Reader) Peek(n int) ([]byte, error) { func (r *Reader) ByteOrder() binary.ByteOrder { return r.bo } + +// PeekAtMost peeks up to n bytes from the buffer without consuming them. +// If no bytes are available, the function propagates the error, including io.EOF and bufio.ErrBufferFull. +// However, if some bytes are returned, io.EOF and bufio.ErrBufferFull are ignored. +// All other errors are always propagated. +func (r *Reader) PeekAtMost(n int) ([]byte, error) { + peeked, err := r.in.Peek(n) + if len(peeked) == 0 { + return nil, err + } + if err == io.EOF || err == bufio.ErrBufferFull { + return peeked, nil + } + return peeked, err +} diff --git a/pkg/dicomio/reader_test.go b/pkg/dicomio/reader_test.go new file mode 100644 index 00000000..19ccf17e --- /dev/null +++ b/pkg/dicomio/reader_test.go @@ -0,0 +1,82 @@ +package dicomio + +import ( + "bufio" + "encoding/binary" + "errors" + "io" + "strings" + "testing" +) + +func TestReader_PeekAtMost(t *testing.T) { + tests := []struct { + name string + data string + peekCount int + wantData string + wantErr error + }{ + { + name: "peek_valid_count_within_buffer", + data: "abcdefghijklmnop", + peekCount: 3, + wantData: "abc", + wantErr: nil, + }, + { + name: "peek_zero_bytes", + data: "abcdefghijklmnop", + peekCount: 0, + wantData: "", + wantErr: nil, + }, + { + name: "peek_negative_count_returns_error", + data: "abcdefghijklmnop", + peekCount: -1, + wantData: "", + wantErr: bufio.ErrNegativeCount, + }, + { + name: "peek_count_larger_than_available_data", + data: "abcdefghijklmnop", + peekCount: 32, + wantData: "abcdefghijklmnop", + wantErr: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + buf := bufio.NewReaderSize(strings.NewReader(tt.data), 16) + r := NewReader(buf, binary.LittleEndian, 0) + + data, err := r.PeekAtMost(tt.peekCount) + + if !errors.Is(err, tt.wantErr) { + t.Errorf("got error %v, want %v", err, tt.wantErr) + } + + if string(data) != tt.wantData { + t.Errorf("got data %q, want %q", string(data), tt.wantData) + } + }) + } +} + +func TestReader_PeekAtMost_AfterConsumingAllData(t *testing.T) { + buf := bufio.NewReaderSize(strings.NewReader("abcdefghijklmnop"), 16) + r := NewReader(buf, binary.LittleEndian, 0) + + // Consume all data + n, err := r.in.Read(make([]byte, 16)) + if n != 16 || err != nil { + t.Fatalf("failed to consume all data: got %d bytes, err=%v", n, err) + } + // Now try to peek - should return empty slice with EOF error + data, err := r.PeekAtMost(1) + if len(data) != 0 || !errors.Is(err, io.EOF) { + t.Errorf("got len=%d, err=%v, want len=0, err=EOF", len(data), err) + } +}