Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Comment thread
mlibanori marked this conversation as resolved.
if err != nil {
return nil, fmt.Errorf("could not peek at next 100 bytes, so cannot infer transfer syntax: %w", err)
}

syntaxes := []struct {
Expand Down
59 changes: 59 additions & 0 deletions parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Comment thread
mlibanori marked this conversation as resolved.
// 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) {
Expand Down
15 changes: 15 additions & 0 deletions pkg/dicomio/reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Comment thread
mlibanori marked this conversation as resolved.
return peeked, nil
}
return peeked, err
}
82 changes: 82 additions & 0 deletions pkg/dicomio/reader_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}