Skip to content

Instantly share code, notes, and snippets.

@gingerBill
Created March 11, 2021 18:56
Show Gist options
  • Save gingerBill/64400b872584d9875d8347dbead5ae1e to your computer and use it in GitHub Desktop.
Save gingerBill/64400b872584d9875d8347dbead5ae1e to your computer and use it in GitHub Desktop.
package main
import "core:os"
import "core:fmt"
import "core:mem"
import "core:slice"
import "intrinsics"
Image_Dos_Header :: struct {
e_signature: u16,
e_cblp: u16,
e_cp: u16,
e_crlc: u16,
e_cparhdr: u16,
e_minalloc: u16,
e_maxalloc: u16,
e_ss: u16,
e_sp: u16,
e_csum: u16,
e_ip: u16,
e_cs: u16,
e_lfarlc: u16,
e_ovno: u16,
e_res: [4]u16,
e_oemid: u16,
e_oeminfo: u16,
e_res2: [10]u16,
e_lfanew: i32,
};
Image_File_Header :: struct {
Machine: u16,
NumberOfSections: u16,
TimeDateStamp: u32,
PointerToSymbolTable: u32,
NumberOfSymbols: u32,
SizeOfOptionalHeader: u16,
Characteristics: u16,
};
Image_Data_Directory :: struct {
VirtualAddress: u32,
Size: u32,
};
Image_Optional_Header32 :: struct {
Magic: u16,
MajorLinkerVersion: u8,
MinorLinkerVersion: u8,
SizeOfCode: u32,
SizeOfInitializedData: u32,
SizeOfUninitializedData: u32,
AddressOfEntryPoint: u32,
BaseOfCode: u32,
BaseOfData: u32,
ImageBase: u32,
SectionAlignment: u32,
FileAlignment: u32,
MajorOperatingSystemVersion: u16,
MinorOperatingSystemVersion: u16,
MajorImageVersion: u16,
MinorImageVersion: u16,
MajorSubsystemVersion: u16,
MinorSubsystemVersion: u16,
Win32VersionValue: u32,
SizeOfImage: u32,
SizeOfHeaders: u32,
CheckSum: u32,
Subsystem: u16,
DllCharacteristics: u16,
SizeOfStackReserve: u32,
SizeOfStackCommit: u32,
SizeOfHeapReserve: u32,
SizeOfHeapCommit: u32,
LoaderFlags: u32,
NumberOfRvaAndSizes: u32,
DataDirectory: [16]Image_Data_Directory,
};
Image_Nt_Headers32 :: struct {
Signature: u32,
FileHeader: Image_File_Header,
OptionalHeader: Image_Optional_Header32,
};
Image_Optional_Header32plus :: struct {
Magic: u16,
MajorLinkerVersion: u8,
MinorLinkerVersion: u8,
SizeOfCode: u32,
SizeOfInitializedData: u32,
SizeOfUninitializedData: u32,
AddressOfEntryPoint: u32,
BaseOfCode: u32,
ImageBase: u64,
SectionAlignment: u32,
FileAlignment: u32,
MajorOperatingSystemVersion: u16,
MinorOperatingSystemVersion: u16,
MajorImageVersion: u16,
MinorImageVersion: u16,
MajorSubsystemVersion: u16,
MinorSubsystemVersion: u16,
Win32VersionValue: u32,
SizeOfImage: u32,
SizeOfHeaders: u32,
CheckSum: u32,
Subsystem: u16,
DllCharacteristics: u16,
SizeOfStackReserve: u64,
SizeOfStackCommit: u64,
SizeOfHeapReserve: u64,
SizeOfHeapCommit: u64,
LoaderFlags: u32,
NumberOfRvaAndSizes: u32,
DataDirectory: [16]Image_Data_Directory,
};
Image_Nt_Headers32plus :: struct {
Signature: u32,
FileHeader: Image_File_Header,
OptionalHeader: Image_Optional_Header32plus,
};
Image_Section_Header :: struct {
Name: [8]u8, // IMAGE_SIZEOF_SHORT_NAME
Misc: struct #raw_union {
PhysicalAddress: u32,
VirtualSize: u32,
},
VirtualAddress: u32,
SizeOfRawData: u32,
PointerToRawData: u32,
PointerToRelocations: u32,
PointerToLinenumbers: u32,
NumberOfRelocations: u16,
NumberOfLinenumbers: u16,
Characteristics: u32,
};
Image_Cor20_Header :: struct {
cb: u32,
MajorRuntimeVersion: u16,
MinorRuntimeVersion: u16,
MetaData: Image_Data_Directory,
Flags: u32,
dummyunionname: struct #raw_union {
EntryPointToken: u32,
EntryPointRVA: u32,
},
Resources: Image_Data_Directory,
StrongNameSignature: Image_Data_Directory,
CodeManagerTable: Image_Data_Directory,
VTableFixups: Image_Data_Directory,
ExportAddressTableJumps: Image_Data_Directory,
ManagedNativeHeader: Image_Data_Directory,
};
section_from_rva :: proc(sections: []Image_Section_Header, rva: u32) -> ^Image_Section_Header {
for _, i in sections {
s := &sections[i];
if rva >= s.VirtualAddress && rva < s.VirtualAddress + s.Misc.VirtualSize {
return s;
}
}
return nil;
}
offset_from_rva :: proc(section: Image_Section_Header, rva: u32) -> uintptr {
return uintptr(rva - section.VirtualAddress + section.PointerToRawData);
}
to_string :: proc(s: []byte) -> string {
n := 0;
for c in s {
if c == 0 {
break;
}
n += 1;
}
return string(s[:n]);
}
uncompress_unsigned :: proc(cursor: ^[]byte) -> u32 {
data := uintptr(raw_data(cursor^));
value: u32;
length: u32;
switch x := (^u8)(data)^; {
case x & 0x80 == 0x00:
length = 1;
value = u32(x);
case x & 0xc0 == 0x80:
length = 2;
value = u32(x & 0x3f) << 8;
data += 1;
value |= u32((^u8)(data)^);
case x & 0xe0 == 0xc0:
length = 4;
value = u32(x & 0x1f) << 24;
data += 1;
value |= u32((^u8)(data)^) << 16;
data += 1;
value |= u32((^u8)(data)^) << 8;
data += 1;
value |= u32((^u8)(data)^);
case:
panic("invalid compressed integer in blob");
}
cursor^ = cursor[length:];
return value;
}
uncompress_enum :: proc($T: typeid, cursor: ^[]byte) -> T where intrinsics.type_is_enum(T) {
return T(uncompress_unsigned(cursor));
}
read_data :: proc($T: typeid, cursor: ^[]byte) -> T {
res := (^T)(raw_data(cursor^))^;
cursor^ = cursor[size_of(T):];
return res;
}
read_string :: proc(cursor: ^[]byte) -> string {
length := uncompress_unsigned(cursor);
s := string(cursor[:length]);
cursor^ = cursor[length:];
return s;
}
Database :: struct {
data: []byte,
ptr: uintptr,
strings: []byte,
blobs: []byte,
guids: []byte,
tables: []byte,
module: Table,
type_ref: Table,
type_def: Table,
field: Table,
method_def: Table,
param: Table,
interface_impl: Table,
member_ref: Table,
constant: Table,
custom_attribute: Table,
field_marshal: Table,
decl_security: Table,
class_layout: Table,
field_layout: Table,
stand_alone_sig: Table,
event_map: Table,
event: Table,
property_map: Table,
property: Table,
method_semantics: Table,
method_impl: Table,
module_ref: Table,
type_spec: Table,
impl_map: Table,
field_rva: Table,
assembly: Table,
assembly_processor: Table,
assembly_os: Table,
assembly_ref: Table,
assembly_ref_processor: Table,
assembly_ref_os: Table,
file: Table,
exported_type: Table,
manifest_resource: Table,
nested_class: Table,
generic_param: Table,
method_spec: Table,
generic_param_constraint: Table,
}
Table_Column :: struct {
offset, size: u8,
}
Table :: struct {
ptr: uintptr,
row_count: u32,
row_size: u8,
columns: [6]Table_Column,
}
table_set_columns :: proc(t: ^Table, a: u8, b := u8(0), c := u8(0), d := u8(0), e := u8(0), f := u8(0)) {
assert(a != 0);
assert(a <= 8);
assert(b <= 8);
assert(c <= 8);
assert(d <= 8);
assert(e <= 8);
assert(f <= 8);
assert(t.row_size == 0);
t.row_size = a + b + c + d + e + f;
t.columns[0] = {0, a};
if b != 0 { t.columns[1] = {u8(a), b}; }
if c != 0 { t.columns[2] = {u8(a+b), c}; }
if d != 0 { t.columns[3] = {u8(a+b+c), d}; }
if e != 0 { t.columns[4] = {u8(a+b+c+d), e}; }
if f != 0 { t.columns[5] = {u8(a+b+c+d+e), f}; }
}
table_index_size :: proc(t: Table) -> u8 {
return 2 if t.row_count < 1<<16 else 4;
}
table_get_value :: proc(t: ^Table, $T: typeid, row, column: u32) -> T {
data_size := t.columns[column].size;
assert(data_size == 1 || data_size == 2 || data_size == 4 || data_size == 8);
fmt.assertf(data_size <= size_of(T), "%v <= %v, expected %v", data_size, size_of(T), typeid_of(T));
assert(row <= t.row_count);
ptr := t.ptr + uintptr(row*u32(t.row_size) + u32(t.columns[column].offset));
switch data_size {
case 1: return T((^ u8)(ptr)^);
case 2: return T((^u16)(ptr)^);
case 4: return T((^u32)(ptr)^);
case 8: return T((^u64)(ptr)^);
}
return T((^u64)(ptr)^);
}
Row :: struct {
table: ^Table,
index: u32,
}
bits_needed :: proc(value: u32) -> u8 {
value := value;
value -= 1;
bits := u8(1);
for {
value >>= 1;
if value == 0 {
break;
}
bits += 1;
}
return bits;
}
is_composite_index_size :: proc(row_count: u32, bits: u8) -> bool {
return row_count < (u32(1) << (16-bits));
}
composite_index_size :: proc(tables: ..Table) -> u8 {
assert(len(tables) > 0);
n := bits_needed(u32(len(tables)));
for table in tables {
if !is_composite_index_size(table.row_count, n) {
return 4;
}
}
return 2;
}
table_set_data :: proc(table: ^Table, table_ptr: uintptr) {
table.ptr = table_ptr + uintptr(table.row_count) * uintptr(table.row_size);
}
database_get_blob :: proc(db: ^Database, index: u32) -> []byte {
data := db.blobs[index:];
initial_byte := data[0];
blob_size_bytes: u32;
switch initial_byte>>5 {
case 0, 1, 2, 3:
blob_size_bytes = 1;
initial_byte &= 0x7f;
case 4, 5:
blob_size_bytes = 2;
initial_byte &= 0x3f;
case 6:
blob_size_bytes = 4;
initial_byte &= 0x1f;
case:
panic("invalid blob encoding");
}
blob_size := u32(initial_byte);
for b in data[1:blob_size_bytes-1] {
blob_size = (blob_size << 8) + u32(b);
}
return data[blob_size_bytes:][:blob_size];
}
database_get_string :: proc(db: ^Database, index: u32) -> (string, bool) {
if index >= u32(len(db.strings)) {
return "", false;
}
s := db.strings[index:];
if i, ok := slice.linear_search(s[:], 0); ok {
return string(s[:i]), true;
}
return "", false;
}
table_get_string :: proc(db: ^Database, table: ^Table, row, column: u32) -> (string, bool) {
index := table_get_value(table, u32, row, column);
return database_get_string(db, index);
}
parse_database :: proc(db: ^Database) -> bool {
if len(db.data) < size_of(Image_Dos_Header) {
return false;
}
db.ptr = uintptr(raw_data(db.data));
ptr := db.ptr;
dos := (^Image_Dos_Header)(ptr);
if dos.e_signature != 0x5a4d {
return false;
}
if len(db.data) < int(dos.e_lfanew) + size_of(Image_Nt_Headers32) {
return false;
}
pe := (^Image_Nt_Headers32)(ptr + uintptr(dos.e_lfanew));
// fmt.printf("%#v\n", pe);
if pe.FileHeader.NumberOfSections == 0 || pe.FileHeader.NumberOfSections > 100 {
return false;
}
sections_ptr: ^Image_Section_Header;
com_virtual_address: u32;
switch pe.OptionalHeader.Magic {
case 0x10b: // PE32
com_virtual_address = pe.OptionalHeader.DataDirectory[14].VirtualAddress;
sections_ptr = (^Image_Section_Header)(ptr + uintptr(dos.e_lfanew) + size_of(Image_Nt_Headers32));
case 0x20b: // PE32+
pe_plus := (^Image_Nt_Headers32plus)(ptr + uintptr(dos.e_lfanew));
com_virtual_address = pe_plus.OptionalHeader.DataDirectory[14].VirtualAddress;
sections_ptr = (^Image_Section_Header)(ptr + uintptr(dos.e_lfanew) + size_of(Image_Nt_Headers32plus));
case:
return false;
}
sections := mem.slice_ptr(sections_ptr, int(pe.FileHeader.NumberOfSections));
section := section_from_rva(sections, com_virtual_address);
if section == nil {
return false;
}
offset := offset_from_rva(section^, com_virtual_address);
cli := (^Image_Cor20_Header)(ptr + offset);
if cli.cb != size_of(Image_Cor20_Header) {
return false;
}
section = section_from_rva(sections, cli.MetaData.VirtualAddress);
if section == nil {
return false;
}
offset = offset_from_rva(section^, cli.MetaData.VirtualAddress);
if (^u32)(ptr + offset)^ != 0x424a5342 {
return false;
}
version_length := (^u32)(ptr + offset + 12)^;
stream_count := (^u16)(ptr + offset + uintptr(version_length) + 18)^;
stream_ptr := ptr + offset + uintptr(version_length) + 20;
Stream_Range :: struct {
offset: u32,
size: u32,
};
for i in 0..<stream_count {
stream := (^Stream_Range)(stream_ptr);
name := to_string((^[12]byte)(stream_ptr + 8)[:]);
table_offset := offset+uintptr(stream.offset);
switch name {
case "#Strings":
db.strings = db.data[table_offset:][:stream.size];
case "#Blob":
db.blobs = db.data[table_offset:][:stream.size];
case "#GUID":
db.guids = db.data[table_offset:][:stream.size];
case "#~":
db.tables = db.data[table_offset:][:stream.size];
case "#US":
// ignore
case:
return false;
}
{
n := uintptr(len(name));
padding := 4 - n%4;
if padding == 0 {
padding = 4;
}
stream_ptr += 8 + n + padding;
}
}
heap_sizes := (^bit_set[0..<8; u8])(&db.tables[6])^;
string_index_size: u8 = 4 if (0 in heap_sizes) else 2;
guid_index_size: u8 = 4 if (1 in heap_sizes) else 2;
blob_index_size: u8 = 4 if (2 in heap_sizes) else 2;
valid_bits := (^bit_set[0..<64; u64])(&db.tables[8])^;
table_ptr := uintptr(&db.tables[24]);
for i in 0..<64 {
if i not_in valid_bits {
continue;
}
row_count := (^u32)(table_ptr)^;
table_ptr += 4;
switch i {
case 0x00: db.module.row_count = row_count;
case 0x01: db.type_ref.row_count = row_count;
case 0x02: db.type_def.row_count = row_count;
case 0x04: db.field.row_count = row_count;
case 0x06: db.method_def.row_count = row_count;
case 0x08: db.param.row_count = row_count;
case 0x09: db.interface_impl.row_count = row_count;
case 0x0a: db.member_ref.row_count = row_count;
case 0x0b: db.constant.row_count = row_count;
case 0x0c: db.custom_attribute.row_count = row_count;
case 0x0d: db.field_marshal.row_count = row_count;
case 0x0e: db.decl_security.row_count = row_count;
case 0x0f: db.class_layout.row_count = row_count;
case 0x10: db.field_layout.row_count = row_count;
case 0x11: db.stand_alone_sig.row_count = row_count;
case 0x12: db.event_map.row_count = row_count;
case 0x14: db.event.row_count = row_count;
case 0x15: db.property_map.row_count = row_count;
case 0x17: db.property.row_count = row_count;
case 0x18: db.method_semantics.row_count = row_count;
case 0x19: db.method_impl.row_count = row_count;
case 0x1a: db.module_ref.row_count = row_count;
case 0x1b: db.type_spec.row_count = row_count;
case 0x1c: db.impl_map.row_count = row_count;
case 0x1d: db.field_rva.row_count = row_count;
case 0x20: db.assembly.row_count = row_count;
case 0x21: db.assembly_processor.row_count = row_count;
case 0x22: db.assembly_os.row_count = row_count;
case 0x23: db.assembly_ref.row_count = row_count;
case 0x24: db.assembly_ref_processor.row_count = row_count;
case 0x25: db.assembly_ref_os.row_count = row_count;
case 0x26: db.file.row_count = row_count;
case 0x27: db.exported_type.row_count = row_count;
case 0x28: db.manifest_resource.row_count = row_count;
case 0x29: db.nested_class.row_count = row_count;
case 0x2a: db.generic_param.row_count = row_count;
case 0x2b: db.method_spec.row_count = row_count;
case 0x2c: db.generic_param_constraint.row_count = row_count;
case:
fmt.println("unknown metadata table", i);
return false;
};
}
empty_table := Table{};
type_def_or_ref, has_constant, has_custom_attribute, has_field_marshal, has_decl_security: u8;
member_ref_parent, has_semantics, method_def_or_ref, member_forwarded, implementation: u8;
custom_attribute_type, resolution_scope, type_or_method_def: u8;
{
using db;
type_def_or_ref = composite_index_size(type_def, type_ref, type_spec);
has_constant = composite_index_size(field, param, property);
has_custom_attribute = composite_index_size(method_def, field, type_ref, type_def, param, interface_impl, member_ref, module, property, event, stand_alone_sig, module_ref, type_spec, assembly, assembly_ref, file, exported_type, manifest_resource, generic_param, generic_param_constraint, method_spec);
has_field_marshal = composite_index_size(field, param);
has_decl_security = composite_index_size(type_def, method_def, assembly);
member_ref_parent = composite_index_size(type_def, type_ref, module_ref, method_def, type_spec);
has_semantics = composite_index_size(event, property);
method_def_or_ref = composite_index_size(method_def, member_ref);
member_forwarded = composite_index_size(field, method_def);
implementation = composite_index_size(file, assembly_ref, exported_type);
custom_attribute_type = composite_index_size(method_def, member_ref, empty_table, empty_table, empty_table);
resolution_scope = composite_index_size(module, module_ref, assembly_ref, type_ref);
type_or_method_def = composite_index_size(type_def, method_def);
table_set_columns(&assembly, 4, 8, 4, blob_index_size, string_index_size, string_index_size);
table_set_columns(&assembly_os, 4, 4, 4);
table_set_columns(&assembly_processor, 4);
table_set_columns(&assembly_ref, 8, 4, blob_index_size, string_index_size, string_index_size, blob_index_size);
table_set_columns(&assembly_ref_os, 4, 4, 4, table_index_size(assembly_ref));
table_set_columns(&assembly_ref_processor, 4, table_index_size(assembly_ref));
table_set_columns(&class_layout, 2, 4, table_index_size(type_def));
table_set_columns(&constant, 2, has_constant, blob_index_size);
table_set_columns(&custom_attribute, has_custom_attribute, custom_attribute_type, blob_index_size);
table_set_columns(&decl_security, 2, has_decl_security, blob_index_size);
table_set_columns(&event_map, table_index_size(type_def), table_index_size(event));
table_set_columns(&event, 2, string_index_size, type_def_or_ref);
table_set_columns(&exported_type, 4, 4, string_index_size, string_index_size, implementation);
table_set_columns(&field, 2, string_index_size, blob_index_size);
table_set_columns(&field_layout, 4, table_index_size(field));
table_set_columns(&field_marshal, has_field_marshal, blob_index_size);
table_set_columns(&field_rva, 4, table_index_size(field));
table_set_columns(&file, 4, string_index_size, blob_index_size);
table_set_columns(&generic_param, 2, 2, type_or_method_def, string_index_size);
table_set_columns(&generic_param_constraint, table_index_size(generic_param), type_def_or_ref);
table_set_columns(&impl_map, 2, member_forwarded, string_index_size, table_index_size(module_ref));
table_set_columns(&interface_impl, table_index_size(type_def), type_def_or_ref);
table_set_columns(&manifest_resource, 4, 4, string_index_size, implementation);
table_set_columns(&member_ref, member_ref_parent, string_index_size, blob_index_size);
table_set_columns(&method_def, 4, 2, 2, string_index_size, blob_index_size, table_index_size(param));
table_set_columns(&method_impl, table_index_size(type_def), method_def_or_ref, method_def_or_ref);
table_set_columns(&method_semantics, 2, table_index_size(method_def), has_semantics);
table_set_columns(&method_spec, method_def_or_ref, blob_index_size);
table_set_columns(&module, 2, string_index_size, guid_index_size, guid_index_size, guid_index_size);
table_set_columns(&module_ref, string_index_size);
table_set_columns(&nested_class, table_index_size(type_def), table_index_size(type_def));
table_set_columns(&param, 2, 2, string_index_size);
table_set_columns(&property, 2, string_index_size, blob_index_size);
table_set_columns(&property_map, table_index_size(type_def), table_index_size(property));
table_set_columns(&stand_alone_sig, blob_index_size);
table_set_columns(&type_def, 4, string_index_size, string_index_size, type_def_or_ref, table_index_size(field), table_index_size(method_def));
table_set_columns(&type_ref, resolution_scope, string_index_size, string_index_size);
table_set_columns(&type_spec, blob_index_size);
table_set_data(&assembly, table_ptr);
table_set_data(&assembly_os, table_ptr);
table_set_data(&assembly_processor, table_ptr);
table_set_data(&assembly_ref, table_ptr);
table_set_data(&assembly_ref_os, table_ptr);
table_set_data(&assembly_ref_processor, table_ptr);
table_set_data(&class_layout, table_ptr);
table_set_data(&constant, table_ptr);
table_set_data(&custom_attribute, table_ptr);
table_set_data(&decl_security, table_ptr);
table_set_data(&event_map, table_ptr);
table_set_data(&event, table_ptr);
table_set_data(&exported_type, table_ptr);
table_set_data(&field, table_ptr);
table_set_data(&field_layout, table_ptr);
table_set_data(&field_marshal, table_ptr);
table_set_data(&field_rva, table_ptr);
table_set_data(&file, table_ptr);
table_set_data(&generic_param, table_ptr);
table_set_data(&generic_param_constraint, table_ptr);
table_set_data(&impl_map, table_ptr);
table_set_data(&interface_impl, table_ptr);
table_set_data(&manifest_resource, table_ptr);
table_set_data(&member_ref, table_ptr);
table_set_data(&method_def, table_ptr);
table_set_data(&method_impl, table_ptr);
table_set_data(&method_semantics, table_ptr);
table_set_data(&method_spec, table_ptr);
table_set_data(&module, table_ptr);
table_set_data(&module_ref, table_ptr);
table_set_data(&nested_class, table_ptr);
table_set_data(&param, table_ptr);
table_set_data(&property, table_ptr);
table_set_data(&property_map, table_ptr);
table_set_data(&stand_alone_sig, table_ptr);
table_set_data(&type_def, table_ptr);
table_set_data(&type_ref, table_ptr);
table_set_data(&type_spec, table_ptr);
}
return true;
}
main :: proc() {
path := "misc/tools/win32metadata-master/scripts/BaselineWinmd/10.0.19041.5/Windows.Win32.winmd";
// path := "misc/tools/win32metadata-master/scripts/BaselineWinmd/10.0.21287.1009/Windows.Win32.winmd";
data, data_ok := os.read_entire_file(path);
if !data_ok {
os.exit(1);
}
db := &Database{data=data};
if !parse_database(db) {
os.exit(1);
}
fmt.println("module ", db.module);
fmt.println("type_ref ", db.type_ref);
fmt.println("type_def ", db.type_def);
fmt.println("field ", db.field);
fmt.println("method_def ", db.method_def);
fmt.println("param ", db.param);
fmt.println("interface_impl ", db.interface_impl);
fmt.println("member_ref ", db.member_ref);
fmt.println("constant ", db.constant);
fmt.println("custom_attribute", db.custom_attribute);
fmt.println("class_layout ", db.class_layout);
fmt.println("field_layout ", db.field_layout);
fmt.println("module_ref ", db.module_ref);
fmt.println("impl_map ", db.impl_map);
fmt.println("assembly ", db.assembly);
fmt.println("assembly_ref ", db.assembly_ref);
fmt.println("nested_class ", db.nested_class);
{
t := &db.assembly_ref;
for row_idx in 0..<t.row_count {
if row_idx > 20 {
break;
}
for c, i in t.columns {
if c.size == 0 {
break;
}
if i > 0 {
fmt.print(", ");
}
s, _ := table_get_string(db, t, row_idx, u32(i));
fmt.printf("%q", s);
}
fmt.println();
}
fmt.println();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment