From 423d8f3fd2b216153e31f5d498cb61e1fb8cc5ed Mon Sep 17 00:00:00 2001 From: Joshua Jones Date: Mon, 23 Feb 2026 22:00:08 -0500 Subject: [PATCH] add max frame size option --- doc.go | 4 +++ error.go | 1 + reader.go | 17 +++++++++-- reader_test.go | 81 ++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 100 insertions(+), 3 deletions(-) diff --git a/doc.go b/doc.go index 854a99e..afc086b 100644 --- a/doc.go +++ b/doc.go @@ -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()) diff --git a/error.go b/error.go index be75fc7..d077e9b 100644 --- a/error.go +++ b/error.go @@ -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. diff --git a/reader.go b/reader.go index 5942b95..d53b570 100644 --- a/reader.go +++ b/reader.go @@ -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. @@ -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) { @@ -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. diff --git a/reader_test.go b/reader_test.go index b424236..eba0c75 100644 --- a/reader_test.go +++ b/reader_test.go @@ -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))