Skip to content
Merged
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
4 changes: 4 additions & 0 deletions doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,10 @@
// ModeRaw (MSH-boundary detection). ReadMessage returns one parsed message
// at a time; EachMessage iterates until EOF.
//
// WithMaxFrameSize caps the MLLP frame size; ReadMessage returns
// ErrFrameTooLarge if the frame body exceeds the limit. A limit of 0
// (the default) means no maximum.
//
// # Stream Writing
//
// w := hl7.NewWriter(conn, hl7.WithMLLP())
Expand Down
1 change: 1 addition & 0 deletions error.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ var (
ErrDelimiterMismatch = errors.New("hl7: delimiter mismatch between messages")
ErrCannotDeleteMSH = errors.New("hl7: cannot delete MSH segment")
ErrInvalidADDContinuation = errors.New("hl7: ADD cannot continue segment")
ErrFrameTooLarge = errors.New("hl7: MLLP frame exceeds maximum size")
)

// ParseError provides detailed context about a parsing failure.
Expand Down
17 changes: 14 additions & 3 deletions reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,10 @@ const defaultBufSize = 32 * 1024 // 32 KB
// Reader reads HL7 messages from an io.Reader.
// It supports MLLP-framed and raw message streams.
type Reader struct {
r *bufio.Reader
mode ReaderMode
buf bytes.Buffer // Reusable buffer to reduce allocations.
r *bufio.Reader
mode ReaderMode
buf bytes.Buffer // Reusable buffer to reduce allocations.
maxFrameSize int
}

// ReaderOption configures a Reader.
Expand All @@ -63,6 +64,13 @@ func WithMode(m ReaderMode) ReaderOption {
}
}

// WithMaxFrameSize sets the maximum allowed MLLP frame size in bytes.
// If a frame exceeds this size during reading, ReadMessage returns
// ErrFrameTooLarge. A value of 0 (the default) imposes no limit.
func WithMaxFrameSize(n int) ReaderOption {
return func(r *Reader) { r.maxFrameSize = n }
}

// WithBufferSize sets the underlying bufio.Reader size.
func WithBufferSize(n int) ReaderOption {
return func(r *Reader) {
Expand Down Expand Up @@ -190,6 +198,9 @@ func (rd *Reader) readMLLP() ([]byte, error) {
break
}
rd.buf.WriteByte(b)
if rd.maxFrameSize > 0 && rd.buf.Len() > rd.maxFrameSize {
return nil, ErrFrameTooLarge
}
}

// Read and verify trailing CR.
Expand Down
81 changes: 81 additions & 0 deletions reader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,87 @@ func TestReaderRawCROnlyTerminator(t *testing.T) {
}
}

func TestWithMaxFrameSizeMLLPWithinLimit(t *testing.T) {
msgData := "MSH|^~\\&|S|F|R|RF|20240101||ADT^A01|1|P|2.5.1"
wrapped := mllpWrap(msgData)
reader := NewReader(bytes.NewReader(wrapped), WithMode(ModeMLLP), WithMaxFrameSize(len(msgData)+100))

msg, err := reader.ReadMessage()
if err != nil {
t.Fatalf("ReadMessage() error: %v", err)
}
if got := msg.Get("MSH-10").String(); got != "1" {
t.Errorf("MSH-10 = %q, want 1", got)
}
}

func TestWithMaxFrameSizeMLLPExceedsLimit(t *testing.T) {
msgData := "MSH|^~\\&|S|F|R|RF|20240101||ADT^A01|1|P|2.5.1"
wrapped := mllpWrap(msgData)
// Set max size smaller than the message.
reader := NewReader(bytes.NewReader(wrapped), WithMode(ModeMLLP), WithMaxFrameSize(10))

_, err := reader.ReadMessage()
if !errors.Is(err, ErrFrameTooLarge) {
t.Errorf("expected ErrFrameTooLarge, got %v", err)
}
}

func TestWithMaxFrameSizeZeroMeansNoLimit(t *testing.T) {
msgData := "MSH|^~\\&|S|F|R|RF|20240101||ADT^A01|1|P|2.5.1"
wrapped := mllpWrap(msgData)
// maxFrameSize=0 (default) means unlimited.
reader := NewReader(bytes.NewReader(wrapped), WithMode(ModeMLLP), WithMaxFrameSize(0))

msg, err := reader.ReadMessage()
if err != nil {
t.Fatalf("ReadMessage() error: %v", err)
}
if got := msg.Get("MSH-10").String(); got != "1" {
t.Errorf("MSH-10 = %q, want 1", got)
}
}

func TestWithMaxFrameSizeExactLimit(t *testing.T) {
msgData := "MSH|^~\\&|S|F|R|RF|20240101||ADT^A01|1|P|2.5.1"
wrapped := mllpWrap(msgData)
// Limit set to exactly the message size — should succeed.
reader := NewReader(bytes.NewReader(wrapped), WithMode(ModeMLLP), WithMaxFrameSize(len(msgData)))

msg, err := reader.ReadMessage()
if err != nil {
t.Fatalf("ReadMessage() error: %v", err)
}
if got := msg.Get("MSH-10").String(); got != "1" {
t.Errorf("MSH-10 = %q, want 1", got)
}
}

func TestWithMaxFrameSizeOneByteBelowLimit(t *testing.T) {
msgData := "MSH|^~\\&|S|F|R|RF|20240101||ADT^A01|1|P|2.5.1"
wrapped := mllpWrap(msgData)
// One byte under size — should fail on the last byte written.
reader := NewReader(bytes.NewReader(wrapped), WithMode(ModeMLLP), WithMaxFrameSize(len(msgData)-1))

_, err := reader.ReadMessage()
if !errors.Is(err, ErrFrameTooLarge) {
t.Errorf("expected ErrFrameTooLarge, got %v", err)
}
}

func BenchmarkReaderMLLPWithMaxFrameSize(b *testing.B) {
msgData := "MSH|^~\\&|S|F|R|RF|20240101||ADT^A01|1|P|2.5.1"
wrapped := mllpWrap(msgData)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
reader := NewReader(bytes.NewReader(wrapped), WithMode(ModeMLLP), WithMaxFrameSize(len(msgData)+100))
if _, err := reader.ReadMessage(); err != nil {
b.Fatal(err)
}
}
}

func TestBufioReaderWrapper(t *testing.T) {
data := []byte("hello world")
br := bufio.NewReader(bytes.NewReader(data))
Expand Down
Loading