VexelRay
Nanobuf · code golf

Nanobuf, golfed.

Nanobuf is a serialization format built so that its reader and its writer each fit on one line of a shell. This page asks the obvious next question: how short can they get in other languages? Every entry here passes the same test cases, and fewer bytes wins.

Leaderboard

Bytes for a reader and a writer

Each count is the UTF-8 length of the entry's one line. The lines marked intrabuf are the shell lines that ship in intrabuf-nanobuf. They are written to be used rather than to win: they print an error message, which golf entries may skip. They are tested by intrabuf's own suite, not by the harness on this page.

LanguageReadWriteTotalShare
Perl14188229
JScript .NET229117346
Tcl261129390
C#273158431
shintrabuf271193464
Bashintrabuf294176470
VB.NET343173516
Java338182520
Rust379233612
C484199683
PowerShellintrabuf367326693
readerwriter
Rules

What an entry has to do

An entry is two lines: a reader and a writer, each a function (or the language's closest equivalent) that someone could paste into a program.

  • The reader takes a path, a schema name and a major version. It hands over every row after the header, kind first, by returning them or by calling a function once per row. It refuses a file whose header names another format version, another schema or another major version, and a file with no header. Fields are UTF-8 text, or bytes that are UTF-8.
  • The writer takes a path and a row's fields. It appends one record to the file: the fields joined by 0x1F, then 0x1E and a line feed, in UTF-8 with no byte order mark. It refuses a field that contains 0x1E, 0x1F or NUL, and then writes nothing.
  • Refusing means any failure the caller can catch: an exception, die, a Tcl error, None or Nothing, or a non-zero result. The entry doesn't have to print a message.
  • The count is the UTF-8 length of the line. Imports, usings and #includes that a file of that language normally opens with are listed under each entry and not counted. Everything else is.
  • A missing field reads as empty, so a reader may drop empty fields at the end of a row. Perl's split does that for free.

Every entry runs against these 18 cases. Each language's line goes into a small driver that the harness builds and runs as its own program:

  • Reads what the reference writes. The rows include empty fields, empty trailing fields, tabs, line breaks inside values (CR LF among them), a value that starts with a line break, a lone CR, non-ASCII text, every shell's special characters, a row with only a kind and a row with thirteen fields.
  • Reads the same file without its final line feed; with CR LF, a lone CR, or nothing at all after every record; and after Git's line-ending conversion, which turns every LF into CR LF, inside values too.
  • Reads a header with no rows, and ignores a truncated final record: whatever follows the last 0x1E is not a record.
  • Refuses another schema, a schema that is a prefix of the real one, another major version, a file at major 11 when asked for 1, an empty file, and a file in nanobuf version 2.
  • Writes a file byte for byte identical to the reference's, and refuses 0x1E, 0x1F and NUL without writing anything. C strings can't hold NUL, so C skips that last case and runs 17.

The truncated-record case is the one addition to intrabuf's own cases, which the spec asks for. intrabuf's suite now checks it too, and every shell line passes it.

Entries

The lines

Shortest first. Each line is shown wrapped to fit, but it is one line. Copy takes it exactly.

Perl

229 bytes

One regex with /g picks out every record that ends in 0x1E and eats the line break in front of it, so a truncated tail never matches. The header is checked as a regex on the raw record, where \c_ is 0x1F. The writer checks all the fields at once by testing "@_", the fields joined with spaces.

Reader · 141 bytes
sub nbr{local$/;open F,'<:raw',$_[0];($h,@r)=<F>=~/\r?\n?(.*?)\36/gs;$h=~/^nanobuf\c_1\c_\Q$_[1]\E\c_$_[2](\.|\c_|$)/?map[split/\37/],@r:die}
Writer · 88 bytes
sub nbw{"@_"=~/[\0\36\37]/&&die;open my$f,'>>:raw',shift;print$f join("\37",@_),"\36\n"}
Use
nbr(PATH, SCHEMA, MAJOR) returns the rows as array references; nbw(PATH, FIELD, ...) appends one. Both die to refuse.
Needs
Nothing.
Tested with
Perl 5.42 (Git for Windows)

JScript .NET

346 bytes

ECMAScript 3 with the .NET library behind it: a regex exec loop does what Perl's /g does, and File.AppendAllText already writes UTF-8 without a byte order mark.

Reader · 229 bytes
function nbr(p,s,m){var r=[],t=System.IO.File.ReadAllText(p),x=/\r?\n?([^\x1e]*)\x1e/g,a;while(a=x.exec(t))r.push(a[1].split('\x1f'));var h=r.shift();if(h[0]!='nanobuf'||h[1]!='1'||h[2]!=s||h[3].split('.')[0]!=m)throw 0;return r}
Writer · 117 bytes
function nbw(p,f){if(/[\0\x1e\x1f]/.test(f.join('')))throw 0;System.IO.File.AppendAllText(p,f.join('\x1f')+'\x1e\n')}
Use
nbr(path, schema, major) returns an array of rows; nbw(path, fields) takes the fields as an array. Both throw to refuse.
Needs
Nothing. The lines name System.IO.File in full.
Tested with
jsc 14.0 (.NET Framework 4.8)

Tcl

390 bytes

Opens the file in binary, so fields come back as the file's bytes. regexp -all -inline finds the records, lassign splits off the header, and string match $m.* on the version with a . appended accepts 1 and 1.2 but not 11.

Reader · 261 bytes
proc nbr {p s m} {set f [open $p rb];foreach {- x} [regexp -all -inline {\r?\n?([^\x1e]*)\x1e} [read $f]] {lappend r [split $x \x1f]};close $f;set r [lassign $r h];if {[lrange $h 0 2] ne [list nanobuf 1 $s]||![string match $m.* [lindex $h 3].]} {error n};set r}
Writer · 129 bytes
proc nbw {p args} {if {[regexp {[\0\x1e\x1f]} [join $args]]} {error n};set f [open $p ab];puts $f [join $args \x1f]\x1e;close $f}
Use
nbr path schema major returns a list of rows; nbw path field ... appends one. Both raise an error to refuse.
Needs
Nothing.
Tested with
Tcl 8.6.18

C#

431 bytes

GetRange(1, Count - 2) drops the header and whatever follows the last 0x1E in one call, and throws on a file with no 0x1E at all. throw null is the shortest throw C# has.

Reader · 273 bytes
static List<string[]>nbr(string p,string s,string m){var r=File.ReadAllText(p).Split('\x1e').Select(x=>Regex.Replace(x,"^\r?\n?","").Split('\x1f')).ToList();var h=r[0];if(h[0]!="nanobuf"||h[1]!="1"||h[2]!=s||h[3].Split('.')[0]!=m)throw null;return r.GetRange(1,r.Count-2);}
Writer · 158 bytes
static void nbw(string p,params string[]f){if(Regex.IsMatch(string.Concat(f),"[\0\x1e\x1f]"))throw null;File.AppendAllText(p,string.Join("\x1f",f)+"\x1e\n");}
Use
static List<string[]> nbr(path, schema, major); static void nbw(path, params string[] fields). Both throw to refuse.
Needs
using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions;
Tested with
C# 5 (csc 4.8, .NET Framework)

VB.NET

516 bytes

VB won't put a method's body on the same line as its declaration, so both lines are delegates holding a one-expression lambda. The reader binds its variables with a LINQ query's Let over a one-element array.

Reader · 343 bytes
Dim nbr As Func(Of String,String,String,List(Of String()))=Function(p,s,m)(From t In{File.ReadAllText(p)}Let r=Regex.Matches(t,"\r?\n?([^\x1e]*)\x1e").Cast(Of Match).Select(Function(x)x.Groups(1).Value.Split(ChrW(31))).ToList(),h=r(0)Select If(h(0)="nanobuf"And h(1)="1"And h(2)=s And h(3).Split("."c)(0)=m,r.Skip(1).ToList(),Nothing)).First()
Writer · 173 bytes
Dim nbw As Action(Of String,String())=Sub(p,f)If Regex.IsMatch(Join(f,""),"[\x00\x1e\x1f]")Then Throw New Exception Else File.AppendAllText(p,Join(f,ChrW(31))&ChrW(30)&vbLf)
Use
nbr(path, schema, major) returns a List(Of String()), or Nothing to refuse; nbw(path, fields) takes an array and throws to refuse.
Needs
Imports System.Collections.Generic, System.IO, System.Linq, System.Text.RegularExpressions
Tested with
Visual Basic 2012 (vbc 14.8, .NET Framework)

Java

520 bytes

A compact source file needs no class and no imports. Matcher.results() streams the records, the header is matched as one regex with the schema quoted, and throw null refuses.

Reader · 338 bytes
List<String[]>nbr(String p,String s,String m)throws Exception{var r=new ArrayList<>(Pattern.compile("\r?\n?([^\36]*)\36").matcher(Files.readString(Path.of(p))).results().map(x->x.group(1).split("\37")).toList());if(!String.join("\37",r.remove(0)).matches("(?s)nanobuf\0371\37"+Pattern.quote(s)+"\37"+m+"([.\37].*)?"))throw null;return r;}
Writer · 182 bytes
void nbw(String p,String...f)throws Exception{if(String.join("",f).matches("(?s).*[\0\36\37].*"))throw null;try(var w=new FileWriter(p,true)){w.write(String.join("\37",f)+"\36\n");}}
Use
List<String[]> nbr(String path, String schema, String major); void nbw(String path, String... fields). Both throw to refuse.
Needs
Nothing: they are top-level methods in a compact source file, which imports all of java.base by itself.
Tested with
Java 25 (GraalVM 25.0.3)

Rust

612 bytes

rsplit_once drops what follows the last 0x1E, and its ? refuses a file with none. Slices of String compare directly with an array of &str.

Reader · 379 bytes
fn nbr(p:&str,s:&str,m:&str)->Option<Vec<Vec<String>>>{let t=std::fs::read_to_string(p).ok()?;let mut i=t.rsplit_once('\x1e')?.0.split('\x1e').map(|x|x.strip_prefix("\r\n").or(x.strip_prefix(['\r','\n'])).unwrap_or(x).split('\x1f').map(String::from).collect::<Vec<_>>());let h=i.next()?;(h.len()>3&&h[..3]==["nanobuf","1",s]&&h[3].split('.').next()==Some(m)).then(||i.collect())}
Writer · 233 bytes
fn nbw(p:&str,f:&[&str])->Option<()>{if f.concat().contains(['\0','\x1e','\x1f']){return None}std::io::Write::write_all(&mut std::fs::File::options().append(true).create(true).open(p).ok()?,(f.join("\x1f")+"\x1e\n").as_bytes()).ok()}
Use
fn nbr(&str, &str, &str) -> Option<Vec<Vec<String>>>; fn nbw(&str, &[&str]) -> Option<()>. None refuses.
Needs
Nothing.
Tested with
Rust 1.98.1

C

683 bytes

Reads the whole file, then splits it in place in one pass, writing a NUL over each separator. The fields are pointers into that one buffer, freed when the reading is done.

Reader · 484 bytes
int nbr(char*p,char*s,char*m,void(*g)(int,char**)){FILE*F=fopen(p,"rb");if(!F)return 1;fseek(F,0,2);long n=ftell(F),h=0,c,l=strlen(m);char*b=malloc(n+1),*r=b,*e,**v=malloc(8*n+8);rewind(F);fread(b,1,n,F);fclose(F);for(;h>=0&&(e=memchr(r,30,b+n-r));r=e+1){*e=0;r+=*r==13;r+=*r==10;for(c=1,*v=r;r<e;r++)if(*r==31)*r=0,v[c++]=r+1;if(h)g(c,v);else h=c>3&&!strcmp(*v,"nanobuf")&&!strcmp(v[1],"1")&&!strcmp(v[2],s)&&!strncmp(v[3],m,l)&&strchr(".",v[3][l])?1:-1;}free(b);free(v);return h<1;}
Writer · 199 bytes
int nbw(char*p,int c,char**f){int i=0;for(;i<c;i++)if(strpbrk(f[i],"\36\37"))return 1;FILE*F=fopen(p,"ab");if(!F)return 1;for(i=0;i<c;i++)fprintf(F,"%s%s",f[i],i<c-1?"\37":"\36\n");return fclose(F);}
Use
int nbr(path, schema, major, void (*row)(int n, char **fields)) calls row for each row, like the shell lines; int nbw(path, int n, char **fields). Both return non-zero to refuse.
Needs
#include <stdio.h>, <stdlib.h> and <string.h>
Tested with
MSVC 19.51 (Visual Studio 18)
Try it

Run the harness, or beat a line

The entries, their drivers and the harness are in nanobuf-golf. With Java 25 or later, from that folder:

java Golf.java            # every language
java Golf.java perl tcl   # just these

It prints each entry's byte counts and which cases fail. It runs on Windows for now, because that's where it finds the C, C#, JScript and VB compilers. The languages here are the ones the test machine already had. Python, JavaScript, Ruby, Go, Lua and PHP come next. A shorter line that passes every case replaces the one here.