pack.cr
Version, currently main branch1 version
- main branchlatestFeb 7, 2021
github.com/HertzDevil/pack.cr
Compile-time (un)pack macros from Perl / Ruby
16 stars
0 dependents
License: MIT
Installation
# Add this to your shard.yml
dependencies:
pack.cr:
github: HertzDevil/pack.cr
branch: mainmain is a branch, not a release, so this tracks it rather than pinning a version.
Then run:
shards installshard.yml
- Crystal
0.36.0- License
- MIT
- Author
- Quinton Miller <nicetas.c@gmail.com>
Dependencies
This version declares no dependencies.
README
# pack.cr





This Crystal library provides macros that transform simple values to and from
byte sequences according to compile-time format strings, based on Perl and
Ruby's `pack` and `unpack` functions. Packing enforces full type safety and
unpacking directly returns extracted values in their specified types.
The library is still under early development.
## Usage
### `Pack.pack`
```crystal
require "pack"
# `Pack.pack` returns a new writable `Bytes`
Pack.pack "csl>", 42_i8, -1000_i16, 1 << 31 # => Bytes[42, 24, 252, 128, 0, 0, 0]
# `Pack.pack_to` writes to an `IO` instance
File.open("my.bin", "rb") do |f|
version = 1_u8
total_songs = 5_u8
first_song = 1_u8
Pack.pack_to f, "U4CCCCS*",
{'N', 'E', 'S', 'M'}, 0x1A, version, # 0x1A allowed due to auto-casting
total_songs, first_song, [0x8000, 0xE000, 0xE003] of UInt16
end
```
### `Pack.unpack`
```crystal
require "pack"
# The following:
Pack.unpack buf, "c2S!>a*"
# roughly expands to:
def unpack(buf : Bytes)
byte_offset = 0
sz = sizeof(Int8)
value1 = StaticArray(Int8, 2).new do |i|
IO::ByteFormat::SystemEndian.decode(Int8, buf[byte_offset + sz * i, sz])
end
byte_offset += sz * 2
sz = sizeof(UInt16)
value2 = IO::ByteFormat::BigEndian.decode(UInt16, buf[byte_offset, sz])
byte_offset += sz
elem_count = buf.size - byte_offset
value3 = buf[byte_offset, elem_count]
byte_offset += elem_count
Tuple.new(value1, value2, value3)
end
# `Pack.unpack` returns a tuple of extracted values
x1, x2, x3 = Pack.unpack Bytes[0x01, 0xC8, 0x03, 0x04], "cCs"
x1 # => 1_i8
x2 # => 200_u8
x3 # => 1027_i16
# No need for further casts
typeof(x1) # => Int8
typeof(x2) # => UInt8
typeof(x3) # => Int16
# Repeat counts and globs become `StaticArray`s and `Array`s
x1, x2 = Pack.unpack Bytes[1, 0, 2, 0, 3, 0], "c2s>*"
x1 # => StaticArray[1_i8, 0_i8]
x2 # => [512_i16, 768_i16]
# Binaries become `Bytes`, UTF-8 values become `Char`s and `String`s
x1, x2, x3 = Pack.unpack Bytes[0x41, 0x42, 0x43, 0x31, 0x32, 0x33, 0x34], "a2U2U*"
x1 # => Bytes[65, 66]
x2 # => "C1"
x3 # => "234"
```
### Current features
* [ ] Packing
* [x] Fixed-size integers (`c` `C` `s` `S` `l` `L` `q` `Q` `n` `N` `v` `V`)
* [x] Native integers (`i` `I` `j` `J`)
* [x] Native size modifiers (`_` `!`)
* [x] Endianness modifiers (`<` `>`)
* [x] Floating-point values (`d` `f` `F` `e` `E` `g` `G`)
* [x] BER-compressed integers (`w`)
* [ ] Binary strings (`a` `A` `Z`)
* [ ] UTF-8 characters / strings (`U` `U*`)
* [x] Bitstrings and hexstrings (`b` `B` `h` `H`)
* [ ] Raw pointers and slices (`p` `P`)
* [ ] UU-encoded strings (`u`)
* [ ] Base64-encoded strings (`m` `M`)
* [ ] String lengths (`/`)
* [ ] Offset directives (`@` `x` `X`)
* [ ] Aligned offsets (`x!` `X!`)
* [x] Repeat counts and globs (`*`)
* [ ] Unpacking
* [x] Fixed-size integers (`c` `C` `s` `S` `l` `L` `q` `Q` `n` `N` `v` `V`)
* [x] Native integers (`i` `I` `j` `J`)
* [x] Native size modifiers (`_` `!`)
* [x] Endianness modifiers (`<` `>`)
* [x] Floating-point values (`d` `f` `F` `e` `E` `g` `G`)
* [x] BER-compressed integers (`w`)
* [x] Binary strings (`a` `A` `Z`)
* [x] UTF-8 characters / strings (`U` `U*`)
* [x] Bitstrings and hexstrings (`b` `B` `h` `H`)
* [x] Raw pointers and slices (`p` `P`)
* [ ] UU-encoded strings (`u`)
* [ ] Base64-encoded strings (`m` `M`)
* [ ] String lengths (`/`)
* [x] Offset directives (`@` `x` `X`)
* [ ] Aligned offsets (`x!` `X!`)
* [x] Repeat counts and globs (`*`)
* [ ] Unpacking directly from readable & rewindable `IO`?
### Non-features (probably)
* Runtime format strings
* Long double (`D`)
* Signed modifier (`!`) for `n` `N` `v` `V`
* Endianness modifiers (`<` `>`) for `d` `f` `F`
* Checksums (`%`)
* Command groups (`(` `)` `.`)
## Installation
1. Add the dependency to your `shard.yml`:
```yaml
dependencies:
pack.cr:
github: HertzDevil/pack.cr
```
2. Run `shards install`
## See also
* [crystal-lang/crystal#276](https://github.com/crystal-lang/crystal/issues/276)
* ["Crystal equivalent of Ruby’s open(url).read[].unpack?"](https://forum.crystal-lang.org/t/crystal-equivalent-of-rubys-open-url-read-unpack/2667)
* [Prior attempt](https://github.com/Fusion/crystal-pack)
* [perlpacktut](https://perldoc.perl.org/perlpacktut)
* [Ruby's String#unpack](https://ruby-doc.org/core-3.0.0/String.html#method-i-unpack)
* [Ruby's Array#pack](https://ruby-doc.org/core-3.0.0/Array.html#method-i-pack)
## Contributing
1. Fork it (<https://github.com/HertzDevil/pack.cr/fork>)
2. Create your feature branch (`git checkout -b my-new-feature`)
3. Commit your changes (`git commit -am 'Add some feature'`)
4. Push to the branch (`git push origin my-new-feature`)
5. Create a new Pull Request
## Contributors
* [Quinton Miller](https://github.com/HertzDevil) - creator and maintainer
Documentation
Built from the current release. The first visit to a release nobody has asked for starts its build.
Links
This branch
- Branch
main- Seen
- Feb 7, 2021
- Crystal
0.36.0- Indexed
- yes
Dependents
No indexed shard depends on this one yet.
Repository
github.com/HertzDevil/pack.cr
Metadata
- Created
- Aug 12, 2026
- Updated
- Aug 13, 2026
- Synced
- Aug 13, 2026
- Versions
- 1