Fuzion Logo
fuzion-lang.dev — The Fuzion Language Portal
JavaScript seems to be disabled. Functionality is limited.

http/body_readers.fz


# This file is part of the Fuzion language implementation.
#
# The Fuzion language implementation is free software: you can redistribute it
# and/or modify it under the terms of the GNU General Public License as published
# by the Free Software Foundation, version 3 of the License.
#
# The Fuzion language implementation is distributed in the hope that it will be
# useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public
# License for more details.
#
# You should have received a copy of the GNU General Public License along with The
# Fuzion language implementation.  If not, see <https://www.gnu.org/licenses/>.


# -----------------------------------------------------------------------
#
#  Tokiwa Software GmbH, Germany
#
#  Source code of Fuzion http library message body readers
#
# -----------------------------------------------------------------------


# version of body reader that reads until connection closes
#
module body_reader(LM type : mutate) : io.Read_Handler is
  public redef read(max_count i32) choice (Sequence u8) io.end_of_file error =>
    # NYI: CLEANUP: ugly that this match is necessary
    match (io.buffered LM).reader.env.read
      s Sequence =>
        (io.buffered LM).reader.env.discard
        s
      o outcome =>
        match o
          e error => e
          io.end_of_file => io.end_of_file


# version of body reader that reads up to reader_size
#
module body_reader(LM type : mutate, reader_size i32, _ unit) : io.Read_Handler is

  remaining := LM.env.new reader_size

  public redef read(max_count i32) choice (Sequence u8) io.end_of_file error =>
    if remaining.get <= 0
      io.end_of_file
    else
      match (io.buffered LM).reader.env.read
        s Sequence =>
          c := s.take (min remaining.get max_count)
          remaining <- remaining.get - c.count
          (io.buffered LM).reader.env.discard c.count
          c
        o outcome =>
          match o
            io.end_of_file =>
              if remaining.get > 0
                error """
                  Connection closed before complete content was read.
                  Content-Length: {reader_size} bytes
                  remaining bytes to read: {remaining.get} bytes
                  """
              else
                io.end_of_file
            e error => e


chunk_start is
chunk_size is
chunk_extension is
chunk_post_size is
chunk_end_start is
chunk_end_finalize is
chunk_body is
chunk_post_body_start is
chunk_post_body_finalize is
chunk_end_of_file is


chunk_enum : choice
  chunk_start
  chunk_size
  chunk_extension
  chunk_post_size
  chunk_end_start
  chunk_end_finalize
  chunk_body
  chunk_post_body_start
  chunk_post_body_finalize
  chunk_end_of_file is


# version of body reader that handles connections with Transfer-Encoding:
# chunked
#
# content with chunked transfer encoding arrives at the client in chunks of
# data, where each chunk specifies a length (encoded in hex digits) and its
# content, which must not exceed the given length. chunks are separated by CR
# LF. upon receiving a chunk with length zero, the connection can be deemed
# terminated.
#
# relevant RFC sections:
# https://datatracker.ietf.org/doc/html/rfc2616#section-3.6.1
# https://datatracker.ietf.org/doc/html/rfc7230#section-4.1
# https://datatracker.ietf.org/doc/html/rfc9112#section-7.1
#
module body_reader_chunked(LM type : mutate) : io.Read_Handler is

  extension_limit := (u64 1024) * 64 # arbitrary value, see below

  position := LM.env.new chunk_enum chunk_start
  current_chunk_size := LM.env.new u64 0
  extension_size := LM.env.new u64 0

  update_current_chunk_size (new option u64) choice (Sequence u8) io.end_of_file error =>
    match new
      nil => error "overflow while parsing chunk size"
      i u64 =>
        current_chunk_size <- i
        position <- chunk_size
        Sequence u8 .empty

  handle_start (s Sequence u8) choice (Sequence u8) io.end_of_file error =>
    c := s.first.or_panic .as_u64

    (io.buffered LM).reader.env.discard 1

    current_chunk_size <- 0

    if 0x30 ? c ? 0x39
      update_current_chunk_size ((current_chunk_size.get *? 16) +? (c - 0x30))
    else if 0x41 ? c ? 0x5a
      update_current_chunk_size ((current_chunk_size.get *? 16) +? (c + 10 - 0x41))
    else if 0x61 ? c ? 0x7a
      update_current_chunk_size ((current_chunk_size.get *? 16) +? (c + 10 - 0x61))
    else
      error "no chunk size provided"


  handle_size (s Sequence u8) choice (Sequence u8) io.end_of_file error =>
    c := s.first.or_panic .as_u64

    (io.buffered LM).reader.env.discard 1

    if 0x30 ? c ? 0x39
      update_current_chunk_size ((current_chunk_size.get *? 16) +? (c - 0x30))
    else if 0x41 ? c ? 0x5a
      update_current_chunk_size ((current_chunk_size.get *? 16) +? (c + 10 - 0x41))
    else if 0x61 ? c ? 0x7a
      update_current_chunk_size ((current_chunk_size.get *? 16) +? (c + 10 - 0x61))
    else if c = 0x0d
      position <- chunk_post_size
      Sequence u8 .empty
    else if c = 0x3b
      position <- chunk_extension
      Sequence u8 .empty
    else
      error "chunk size invalid"


  handle_post_size (s Sequence u8) choice (Sequence u8) io.end_of_file error =>
    # current_chunk_size is finalized now
    c := s.first.or_panic

    (io.buffered LM).reader.env.discard 1

    if c = 0x0a
      if current_chunk_size.get = 0
        position <- chunk_end_start
      else
        position <- chunk_body

      Sequence u8 .empty
    else
      error "invalid termination for chunk size, expected LF found {c}"


  handle_end_start (s Sequence u8) choice (Sequence u8) io.end_of_file error =>
    c := s.first.or_panic

    (io.buffered LM).reader.env.discard 1

    if c = 0x0d
      position <- chunk_end_finalize
      Sequence u8 .empty
    else
      error "NYI: UNDER DEVELOPMENT: trailers"


  handle_end_finalize (s Sequence u8) choice (Sequence u8) io.end_of_file error =>
    c := s.first.or_panic

    (io.buffered LM).reader.env.discard 1

    if c = 0x0a
      position <- chunk_end_of_file
      io.end_of_file
    else
      error "invalid termination of chunked transaction, expected LF found {c}"


  handle_body (s Sequence u8) choice (Sequence u8) io.end_of_file error =>
    chunk := s.take current_chunk_size.get
    count := chunk.count

    (io.buffered LM).reader.env.discard count

    match current_chunk_size.get -? count.as_u64
      new u64 =>
        current_chunk_size <- new

        if new > 0
          position <- chunk_body
        else
          position <- chunk_post_body_start
      nil => panic "should not happen, underflow when calculating new chunk size"

    chunk


  handle_post_body_start (s Sequence u8) choice (Sequence u8) io.end_of_file error =>
    c := s.first.or_panic

    (io.buffered LM).reader.env.discard 1

    if c = 0x0d
      position <- chunk_post_body_finalize
      Sequence u8 .empty
    else
      error "invalid termination of body, expected CR found {c}"


  handle_post_body_finalize (s Sequence u8) choice (Sequence u8) io.end_of_file error =>
    c := s.first.or_panic

    (io.buffered LM).reader.env.discard 1

    if c = 0x0a
      position <- chunk_start
      Sequence u8 .empty
    else
      error "invalid termination of body, expected LF found {c}"


  handle_extension (s Sequence u8) choice (Sequence u8) io.end_of_file error =>
    c := s.first.or_panic

    (io.buffered LM).reader.env.discard 1

    if c = 0x0d
      position <- chunk_end_finalize
      Sequence u8 .empty
    else if c = 0x0a
      error "invalid newline in chunk extension"
    else
      extension_size <- extension_size.get + 1

      if extension_size.get > extension_limit
        # this static limit is kinda arbitrary, we could add more magic to allow
        # extensions proportional to the content size, but this does not matter
        # yet
        #
        # the limit exists to protect against DoS attacks where the sender of
        # the message sends a small chunk with a long extension part
        error "chunk extensions too long"
      else
        # we do nothing with the extensions, as per the spec they must be
        # ignored if they are not understood
        position <- chunk_extension
        Sequence u8 .empty


  public redef read(max_count i32) choice (Sequence u8) io.end_of_file error =>
    match (io.buffered LM).reader.env.read
      s Sequence =>
        if s.is_empty
          Sequence u8 .empty # do nothing for now, wait for buffered reader to buffer
        else
          match position.get
            chunk_start => handle_start s
            chunk_size => handle_size s
            chunk_extension => handle_extension s
            chunk_post_size => handle_post_size s
            chunk_end_start => handle_end_start s
            chunk_end_finalize => handle_end_finalize s
            chunk_body => handle_body s
            chunk_post_body_start => handle_post_body_start s
            chunk_post_body_finalize => handle_post_body_finalize s
            chunk_end_of_file => io.end_of_file
      o outcome =>
        match o
          e error => e
          io.end_of_file =>
            match position.get
              chunk_end_of_file => io.end_of_file
              * => error "unexpected end of file"


# create a body_reader for the specified options
#
module body_reader(
  # mutate effect from which to read
  LM type : mutate,
  # size that should be read if known, nil to read until connection closes
  reader_size option u64) io.Read_Handler
=>
  reader_size
    .bind (io.Read_Handler) rs->
      body_reader LM rs.as_i32 unit
    .or_else (body_reader LM)

last changed: 2026-06-12