UF2 (USB Flashing Format): Ruby parsing library

UF2 is a file format, developed by Microsoft for PXT (also known as Microsoft MakeCode), that is particularly suitable for flashing microcontrollers over MSC (Mass Storage Class; aka removable flash drive).

The list of family IDs is stored in a separate JSON file:

https://github.com/microsoft/uf2/blob/master/utils/uf2families.json

This JSON file is regularly updated. The family_id enum should be kept in sync with it. For simplicity and consistency, it's strongly recommended to auto-generate this enum from uf2families.json directly. This was last done using the following commands:

$ curl -fsSLO https://github.com/microsoft/uf2/raw/90e9741f217f5a40c98ba74d663e408041037578/utils/uf2families.json
$ jq -r '
  .[]
  | "    \(.id | ascii_downcase):\n"
  + "      id: \(.short_name | ascii_downcase | gsub("-"; "_"))\n"
  + "      doc: \(.description | tojson)"
' uf2families.json

Test files, picked to cover as many different shapes as possible:

File extension

uf2

KS implementation details

License: CC0-1.0

References

This page hosts a formal specification of UF2 (USB Flashing Format) using Kaitai Struct. This specification can be automatically translated into a variety of programming languages to get a parsing library.

Usage

Runtime library

All Ruby code generated by Kaitai Struct depends on the Kaitai Struct runtime library for Ruby. You must add this dependency to your project before you can parse or serialize any data.

The Ruby runtime library can be installed from RubyGems:

gem install kaitai-struct

Code

Parse a local file and get structure in memory:

data = Uf2.from_file("path/to/local/file.uf2")

Or parse structure from a string of bytes:

bytes = "\x00\x01\x02..."
data = Uf2.new(Kaitai::Struct::Stream.new(bytes))

After that, one can get various attributes from the structure by invoking getter methods like:

data.first_block # => get first block

Ruby source code to parse UF2 (USB Flashing Format)

uf2.rb

# This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild

require 'kaitai/struct/struct'

unless Gem::Version.new(Kaitai::Struct::VERSION) >= Gem::Version.new('0.11')
  raise "Incompatible Kaitai Struct Ruby API: 0.11 or later is required, but you have #{Kaitai::Struct::VERSION}"
end


##
# UF2 is a file format, developed by Microsoft for PXT (also known as
# Microsoft MakeCode), that is particularly suitable for flashing
# microcontrollers over MSC (Mass Storage Class; aka removable flash drive).
# 
# The list of family IDs is stored in a separate JSON file:
# 
# <https://github.com/microsoft/uf2/blob/master/utils/uf2families.json>
# 
# This JSON file is regularly updated. The `family_id` enum should be kept in
# sync with it. For simplicity and consistency, it's strongly recommended to
# auto-generate this enum from `uf2families.json` directly. This was last done
# using the following commands:
# 
# ```bash
# $ curl -fsSLO https://github.com/microsoft/uf2/raw/90e9741f217f5a40c98ba74d663e408041037578/utils/uf2families.json
# $ jq -r '
#   .[]
#   | "    \(.id | ascii_downcase):\n"
#   + "      id: \(.short_name | ascii_downcase | gsub("-"; "_"))\n"
#   + "      doc: \(.description | tojson)"
# ' uf2families.json
# ```
# 
# Test files, picked to cover as many different shapes as possible:
# 
# * <https://micropython.org/download/RPI_PICO/> - a typical case: all blocks
#   have the same family ID and `num_blocks`.
# * <https://micropython.org/download/RPI_PICO2/> - these .uf2 files are
#   actually two UF2 files concatenated. The first UF2 file is the standalone
#   `family_id::rp2xxx_absolute` block that `picotool` prepends (see the
#   `is_rp2350_e10_block` value instance in the `block` type).
# * <https://circuitpython.org/downloads> - the builds for SAMD boards (for
#   example
#   [Feather M0 Express](https://circuitpython.org/board/feather_m0_express/))
#   set no flags at all, so the field at offset 28 is read as `file_size` rather
#   than `family_id`.
# * <https://github.com/raspberrypi/pico-sdk-prebuilts/releases> -
#   [Universal UF2](https://github.com/raspberrypi/pico-examples/blob/c81c855ffdedc825975a40ba357723a71358ddf0/universal/README.md#universal-binary-vs-universal-uf2)
#   files, which are again two UF2 files concatenated. What's interesting about
#   these is that the last block of the first file (with the family ID
#   `family_id::rp2040`) is moved to the end of the second file, which was
#   intended for `family_id::rp2xxx_absolute` (see
#   <https://github.com/raspberrypi/pico-examples/blob/c81c855ffdedc825975a40ba357723a71358ddf0/universal/CMakeLists.txt#L159-L169>).
# * <https://github.com/microsoft/pxt-microsoft-boot-sequence/releases> - some
#   files contain blocks with the `flags.is_file_container` flag set (for
#   example
#   <https://github.com/microsoft/pxt-microsoft-boot-sequence/releases/download/v0.0.4/arcade-p0.uf2>
#   is a pure file container for
#   `file_name == "Projects/microsoft-boot-sequence.elf"`). Generated by
#   MakeCode, the only known producer of file container UF2s.
# * <https://github.com/umi-eng/uftwo/tree/35bccf75b4f81c43f088696a8c4a9912f1f4104e/uftwo/tests> -
#   synthetic test files for features that real firmware doesn't seem to use
#   (e.g. MD5 checksums).
# @see https://github.com/microsoft/uf2/blob/90e9741f217f5a40c98ba74d663e408041037578/README.md Source
class Uf2 < Kaitai::Struct::Struct

  EXTENSION_TAG_TYPE = {
    0 => :extension_tag_type_end,
    780791 => :extension_tag_type_page_size,
    6622621 => :extension_tag_type_description,
    10049507 => :extension_tag_type_rp2_ignore_block,
    10471356 => :extension_tag_type_version,
    11824560 => :extension_tag_type_sha2_checksum,
    13149993 => :extension_tag_type_device_type_id,
  }
  I__EXTENSION_TAG_TYPE = EXTENSION_TAG_TYPE.invert

  FAMILY_ID = {
    16738585 => :family_id_stm32l4,
    69471199 => :family_id_stm32l5,
    114362747 => :family_id_stm32f411xc,
    299792458 => :family_id_m0sense,
    374814231 => :family_id_atmega32,
    407992330 => :family_id_saml21,
    458716255 => :family_id_nrf52,
    475996592 => :family_id_esp32,
    505365293 => :family_id_stm32l1,
    539900561 => :family_id_stm32l0,
    558239728 => :family_id_stm32wl,
    585160444 => :family_id_rtl8710b,
    716994540 => :family_id_lpc55,
    730387100 => :family_id_esp32c2,
    767756741 => :family_id_stm32f411xe,
    806311475 => :family_id_stm32g0,
    822212545 => :family_id_esp32s31,
    835856582 => :family_id_gd32f350,
    858203894 => :family_id_esp32h2,
    863621090 => :family_id_rtl8720d,
    1026592404 => :family_id_esp32p4,
    1265126769 => :family_id_maixplay_u4,
    1282483210 => :family_id_stm32g4,
    1318001757 => :family_id_stm32h5,
    1332399698 => :family_id_csk4,
    1337120189 => :family_id_mimxrt10xx,
    1374225320 => :family_id_xr809,
    1404571392 => :family_id_stm32f7,
    1410195298 => :family_id_esp32c6,
    1427194976 => :family_id_samd51,
    1467308631 => :family_id_stm32f4,
    1511523995 => :family_id_fx2,
    1561987630 => :family_id_stm32f2,
    1591873650 => :family_id_stm32f1,
    1646171002 => :family_id_nrf52833,
    1685595318 => :family_id_stm32f0,
    1733968048 => :family_id_bk7231u,
    1760373640 => :family_id_samd21,
    1771791084 => :family_id_ch32v,
    1786956866 => :family_id_bk7251,
    1803837832 => :family_id_stm32f3,
    1829315322 => :family_id_stm32f407,
    1840668802 => :family_id_stm32h7,
    1853049000 => :family_id_csk6,
    1869948536 => :family_id_nrf52832xxab,
    1892771411 => :family_id_stm32wb,
    1920081230 => :family_id_nrf52832xxaa,
    1947226634 => :family_id_max32690,
    2010665156 => :family_id_esp32c61,
    2067722800 => :family_id_bk7231n,
    2078840685 => :family_id_ra4m1,
    2105173743 => :family_id_py32f071_uvk5_v3,
    2125160941 => :family_id_esp8266,
    2139350931 => :family_id_kl32l2,
    2181929567 => :family_id_nrf52820,
    2410701054 => :family_id_stm32f407vg,
    2446589208 => :family_id_max78002,
    2501329455 => :family_id_rza1lu,
    2599435827 => :family_id_gd32vf103,
    2651564682 => :family_id_esp32h4,
    2684343619 => :family_id_rtl8710a,
    2697558926 => :family_id_at32f415,
    2913282112 => :family_id_nrf52840,
    3067936943 => :family_id_esp32h21,
    3218951918 => :family_id_esp32s2,
    3296614247 => :family_id_esp32s3,
    3559628908 => :family_id_esp32c3,
    3594487346 => :family_id_max32650,
    3725750455 => :family_id_bl602,
    3767498084 => :family_id_rtl8720c,
    3834380118 => :family_id_rp2040,
    3834380119 => :family_id_rp2xxx_absolute,
    3834380120 => :family_id_rp2xxx_data,
    3834380121 => :family_id_rp2350_arm_s,
    3834380122 => :family_id_rp2350_riscv,
    3834380123 => :family_id_rp2350_arm_ns,
    4039314801 => :family_id_max32666,
    4145808195 => :family_id_esp32c5,
  }
  I__FAMILY_ID = FAMILY_ID.invert
  def initialize(_io, _parent = nil, _root = nil)
    super(_io, _parent, _root || self)
    _read
  end

  def _read
    @first_block = Block.new(@_io, self, @_root)
    @blocks = []
    (first_block.num_blocks - 1).times { |i|
      @blocks << Block.new(@_io, self, @_root)
    }
    self
  end
  class Block < Kaitai::Struct::Struct
    def initialize(_io, _parent = nil, _root = nil)
      super(_io, _parent, _root)
      _read
    end

    def _read
      @magic = @_io.read_bytes(4)
      raise Kaitai::Struct::ValidationNotEqualError.new([85, 70, 50, 10].pack('C*'), @magic, @_io, "/types/block/seq/0") if not @magic == [85, 70, 50, 10].pack('C*')
      @second_magic = @_io.read_bytes(4)
      raise Kaitai::Struct::ValidationNotEqualError.new([87, 81, 93, 158].pack('C*'), @second_magic, @_io, "/types/block/seq/1") if not @second_magic == [87, 81, 93, 158].pack('C*')
      @flags = Flags.new(@_io, self, @_root)
      @target_address = @_io.read_u4le
      _ = @target_address
      raise Kaitai::Struct::ValidationExprError.new(@target_address, @_io, "/types/block/seq/3") if not _ % 4 == 0
      @len_payload = @_io.read_u4le
      _ = @len_payload
      raise Kaitai::Struct::ValidationExprError.new(@len_payload, @_io, "/types/block/seq/4") if not _ % 4 == 0
      @block_number = @_io.read_u4le
      @num_blocks_raw = @_io.read_u4le
      raise Kaitai::Struct::ValidationLessThanError.new(block_number + 1, @num_blocks_raw, @_io, "/types/block/seq/6") if not @num_blocks_raw >= block_number + 1
      if !(flags.has_family_id)
        @file_size = @_io.read_u4le
      end
      if flags.has_family_id
        @family_id = Kaitai::Struct::Stream::resolve_enum(Uf2::FAMILY_ID, @_io.read_u4le)
      end
      _io_data = @_io.substream(476)
      @data = BlockData.new(_io_data, self, @_root)
      @final_magic = @_io.read_bytes(4)
      raise Kaitai::Struct::ValidationNotEqualError.new([48, 111, 177, 10].pack('C*'), @final_magic, @_io, "/types/block/seq/10") if not @final_magic == [48, 111, 177, 10].pack('C*')
      self
    end

    ##
    # Determines whether this is a block that `picotool` prepends to RP2350
    # flash images as a workaround for erratum RP2350-E10 (i.e. a hardware
    # bug in the A2 version of the RP2350 boot ROM).
    # 
    # Such a block is always written on its own, but its `num_blocks_raw` is
    # set to 2. If we trusted this value, we would attempt to read one block
    # too many (which would most likely fail). Therefore, we must correct it
    # to 1 before using it.
    # 
    # The conditions for detecting this block come from the
    # [`check_abs_block()`](https://github.com/raspberrypi/picotool/blob/6f6458d792b93685a11423b244a585eaa99eafcf/elf2uf2/elf2uf2.cpp#L147)
    # function in `picotool`. However, there are some differences:
    # 
    # 1. In Kaitai Struct, we cannot easily check whether all 256 payload
    #    bytes are set to `0xef`, so we only check the first and last bytes.
    # 2. There are .uf2 files in the wild where `flags.has_extension_tags`
    #    is true, but there are actually no extension tags (the first and
    #    only tag has a size of 0, which is just a terminator), so our
    #    condition allows for this case. You can download an example of such
    #    a .uf2 file here:
    #    <https://github.com/neednotapply/DC32-cfw/releases/tag/1.69.13.37>
    # 
    # It's worth noting that we cannot require the presence of the
    # `extension_tag_type::rp2_ignore_block`
    # (`UF2_EXTENSION_RP2_IGNORE_BLOCK`) tag because the UF2 files generated
    # by `picotool` prior to
    # <https://github.com/raspberrypi/picotool/commit/78c9bd121b09399823b67ee7ea89003ca0d3315f>
    # don't have it. Therefore, we check whether this tag is present only if
    # `flags.has_extension_tags` is set.
    # 
    # Test .uf2 files with this special block can be downloaded from
    # <https://micropython.org/download/RPI_PICO2/>. Note that all the .uf2
    # files there are actually two UF2 (sub)files concatenated, and this
    # Kaitai Struct implementation parses only one at a time (so in order to
    # parse both, you need something like the helper spec `uf2_files.ksy`
    # from
    # <https://github.com/kaitai-io/kaitai_struct_formats/pull/542#discussion_r3906386820>).
    # v1.24.x releases predate the `extension_tag_type::rp2_ignore_block`
    # tag, while releases v1.25.0 and later include it.
    # @see https://github.com/raspberrypi/picotool/blob/6f6458d792b93685a11423b244a585eaa99eafcf/elf2uf2/elf2uf2.cpp#L147 Git tag "2.3.0"
    # @see https://github.com/raspberrypi/picotool/commit/78c9bd121b09399823b67ee7ea89003ca0d3315f Source
    def is_rp2350_e10_block
      return @is_rp2350_e10_block unless @is_rp2350_e10_block.nil?
      @is_rp2350_e10_block =  (( ((flags.value == 8192) || (flags.value == 40960)) ) && (family_id == :family_id_rp2xxx_absolute) && (num_blocks_raw == 2) && (block_number == 0) && (len_payload == 256) && (data.payload[0].ord == 239) && (data.payload[-1].ord == 239) && ( ((!(flags.has_extension_tags)) || (data.extension_tags[0].len_tag == 0) || ( ((data.extension_tags[0].len_tag == 4) && (data.extension_tags[0].tag_type == :extension_tag_type_rp2_ignore_block)) )) )) 
      @is_rp2350_e10_block
    end
    def num_blocks
      return @num_blocks unless @num_blocks.nil?
      @num_blocks = (is_rp2350_e10_block ? 1 : num_blocks_raw)
      @num_blocks
    end
    attr_reader :magic
    attr_reader :second_magic
    attr_reader :flags

    ##
    # Address in flash where `data.payload` should be written, or an offset
    # in the file specified by `data.file_name` if `flags.is_file_container`
    # is set.
    # 
    # The [official
    # spec](https://github.com/microsoft/uf2/blob/90e9741f217f5a40c98ba74d663e408041037578/README.md#payload-sizes)
    # says:
    # 
    # > In any event, payload size and target address should always be
    # > 4-byte aligned.
    attr_reader :target_address

    ##
    # The [official
    # spec](https://github.com/microsoft/uf2/blob/90e9741f217f5a40c98ba74d663e408041037578/README.md#payload-sizes)
    # says:
    # 
    # > In any event, payload size and target address should always be
    # > 4-byte aligned.
    attr_reader :len_payload
    attr_reader :block_number

    ##
    # Number of blocks that make up the UF2 file to which this block
    # belongs. Every block of a file has the same value. It is at least 1
    # and every `block_number` in the file must be less than this value
    # (which is validated by this Kaitai Struct implementation).
    attr_reader :num_blocks_raw

    ##
    # Size of the file this block belongs to, but only if
    # `flags.is_file_container` is true. Otherwise, the official spec allows
    # this field to be set to anything - though in practice, it's always
    # zero.
    attr_reader :file_size
    attr_reader :family_id
    attr_reader :data
    attr_reader :final_magic
    attr_reader :_raw_data
  end
  class BlockData < Kaitai::Struct::Struct
    def initialize(_io, _parent = nil, _root = nil)
      super(_io, _parent, _root)
      _read
    end

    def _read
      @payload = @_io.read_bytes(_parent.len_payload)
      if _parent.flags.is_file_container
        @file_name = (@_io.read_bytes_term(0, false, true, true)).force_encoding("UTF-8")
      end
      if _parent.flags.has_extension_tags
        @extension_tags = []
        i = 0
        begin
          _ = ExtensionTag.new(@_io, self, @_root)
          @extension_tags << _
          i += 1
        end until _.len_tag == 0
      end
      self
    end

    ##
    # Describes a region that doesn't need to be flashed again if the
    # checksum matches.
    # 
    # The [official
    # spec](https://github.com/microsoft/uf2/blob/90e9741f217f5a40c98ba74d663e408041037578/README.md#md5-checksum)
    # says that "This is currently only used on ESP32", but no real-world
    # firmware sample has been found (at least none of the .uf2 files from
    # <https://github.com/adafruit/tinyuf2/releases>,
    # [MicroPython](https://micropython.org/download/) or
    # [CircuitPython](https://circuitpython.org/downloads) contain it). It
    # was found only in some synthetic test files, e.g.
    # <https://github.com/umi-eng/uftwo/blob/35bccf75b4f81c43f088696a8c4a9912f1f4104e/uftwo/tests/checksum_256.uf2>.
    def md5_checksum
      return @md5_checksum unless @md5_checksum.nil?
      if _parent.flags.has_md5_checksum
        _pos = @_io.pos
        @_io.seek(_io.size - 24)
        @md5_checksum = Md5Checksum.new(@_io, self, @_root)
        @_io.seek(_pos)
      end
      @md5_checksum
    end

    ##
    # The bytes to be written to `_parent.target_address`, which is either
    # an address in flash, or an offset in the file specified by `file_name`
    # if `_parent.flags.is_file_container` is set.
    attr_reader :payload
    attr_reader :file_name
    attr_reader :extension_tags
  end

  ##
  # @see https://github.com/microsoft/uf2/blob/90e9741f217f5a40c98ba74d663e408041037578/README.md#extension-tags Source
  class ExtensionTag < Kaitai::Struct::Struct
    def initialize(_io, _parent = nil, _root = nil)
      super(_io, _parent, _root)
      _read
    end

    def _read
      @len_tag = @_io.read_u1
      _ = @len_tag
      raise Kaitai::Struct::ValidationExprError.new(@len_tag, @_io, "/types/extension_tag/seq/0") if not  ((_ == 0) || (_ >= min_len_tag)) 
      @tag_type = Kaitai::Struct::Stream::resolve_enum(Uf2::EXTENSION_TAG_TYPE, @_io.read_bits_int_le(24))
      @_io.align_to_byte
      if len_tag != 0
        @value = @_io.read_bytes(len_value)
      end
      @padding = @_io.read_bytes(-(len_tag) % 4)
      self
    end
    def len_value
      return @len_value unless @len_value.nil?
      @len_value = (len_tag >= min_len_tag ? len_tag - min_len_tag : 0)
      @len_value
    end
    def min_len_tag
      return @min_len_tag unless @min_len_tag.nil?
      @min_len_tag = 1 + 3
      @min_len_tag
    end

    ##
    # Total size of the tag in bytes, including this byte and `tag_type`, so
    # at least 4. The exception is the last tag which terminates the list -
    # it specifies a total size of 0.
    attr_reader :len_tag
    attr_reader :tag_type
    attr_reader :value

    ##
    # Tags are 4-byte aligned, so a tag whose size is not a multiple
    # of 4 is followed by padding.
    attr_reader :padding
  end

  ##
  # @see https://github.com/microsoft/uf2/blob/90e9741f217f5a40c98ba74d663e408041037578/uf2.h#L43-L47 Source
  # @see https://github.com/raspberrypi/pico-sdk/blob/98a542c1a62fb549ffb5d66a3e5892b06276b670/src/common/boot_uf2_headers/include/boot/uf2.h#L23-L27 Git tag "2.3.0"
  class Flags < Kaitai::Struct::Struct
    def initialize(_io, _parent = nil, _root = nil)
      super(_io, _parent, _root)
      _read
    end

    def _read
      @value = @_io.read_u4le
      _ = @value
      raise Kaitai::Struct::ValidationExprError.new(@value, @_io, "/types/flags/seq/0") if not  ((_ & ~61441 == 0) && (!( ((is_file_container) && (has_extension_tags)) ))) 
      self
    end

    ##
    # Indicates whether extension tags are present after the payload.
    def has_extension_tags
      return @has_extension_tags unless @has_extension_tags.nil?
      @has_extension_tags = value & 32768 != 0
      @has_extension_tags
    end

    ##
    # The field at offset 28 in the block is `family_id` instead of
    # `file_size`.
    def has_family_id
      return @has_family_id unless @has_family_id.nil?
      @has_family_id = value & 8192 != 0
      @has_family_id
    end

    ##
    # Indicates whether `md5_checksum` is present at the end of `data`.
    def has_md5_checksum
      return @has_md5_checksum unless @has_md5_checksum.nil?
      @has_md5_checksum = value & 16384 != 0
      @has_md5_checksum
    end

    ##
    # When set, the UF2 format is used as a container for regular files
    # (akin to a TAR file, or ZIP archive without compression).
    # 
    # `target_address` is the offset in the file where the payload is to be
    # written, and `file_size` is the size of that file. The name of the
    # destination file is stored in `data.file_name`.
    def is_file_container
      return @is_file_container unless @is_file_container.nil?
      @is_file_container = value & 4096 != 0
      @is_file_container
    end

    ##
    # Indicates that this block should be skipped when writing the device
    # flash. It can be used to store data that does not fit on the device,
    # typically embedded source code or debug info.
    def not_main_flash
      return @not_main_flash unless @not_main_flash.nil?
      @not_main_flash = value & 1 != 0
      @not_main_flash
    end

    ##
    # Only the five bits that we cover in value instances below are defined,
    # and no other bit may be set. The [official
    # spec](https://github.com/microsoft/uf2/blob/90e9741f217f5a40c98ba74d663e408041037578/README.md#flags)
    # says "Currently, there are five flags defined". If any other flags are
    # added in the future, this .ksy spec will need to be updated.
    # 
    # The `is_file_container` and `has_extension_tags` flags disagree about
    # what follows the payload in `data` (a file name or a list of extension
    # tags), so this Kaitai Struct implementation treats them as mutually
    # exclusive and will reject a block that sets both. The official spec
    # does not specify how this should be handled, but logically there's a
    # conflict, so we've decided to strictly treat it as an error.
    attr_reader :value
  end

  ##
  # @see https://github.com/microsoft/uf2/blob/90e9741f217f5a40c98ba74d663e408041037578/README.md#md5-checksum Source
  class Md5Checksum < Kaitai::Struct::Struct
    def initialize(_io, _parent = nil, _root = nil)
      super(_io, _parent, _root)
      _read
    end

    def _read
      @start_address = @_io.read_u4le
      @len_region = @_io.read_u4le
      @md5 = @_io.read_bytes(16)
      self
    end
    attr_reader :start_address
    attr_reader :len_region
    attr_reader :md5
  end
  attr_reader :first_block
  attr_reader :blocks
end