/* * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0.If a copy of the MPL was not distributed with this * file, You can obtain one at http ://mozilla.org/MPL/2.0/. */ #undef NDEBUG #include #include #include #include #include #include #include #include #include "zlib.h" #include "ESPReader.h" // // === CONSTANTS === // #define STDOUT_FILENO 1 // timestamp field access const uint16_t day_mask = 0x1F; const uint16_t month_mask = 0xF; const uint16_t year_mask = 0x7F; const int month_offset = 5; const int year_offset = 9; // // === FORWARD DECLARATIONS === // void asserts(void); // Tree walkers void walk_concat(SizedBuf *tree, struct walker_callbacks cb, void *from_parent); void walk_group(SizedBuf *tree, struct walker_callbacks cb, void *from_parent); void walk_record(SizedBuf *tree, struct walker_callbacks cb, void *from_parent); // Header printers void print_group_header(Group *header); void print_record_header(Record *header); // Printer helpers void litcopy(struct str_buf *sb, struct str_lit lit); void num_str(struct str_buf *sb, unsigned long num, int radix); void type_str(struct str_buf *sb, Type4 type); void timestamp_str(struct str_buf *sb, uint16_t timestamp); void group_label_str(struct str_buf *sb, Group *header); void record_flags_str(struct str_buf *sb, Record *header); void sb_write(int fp, struct str_buf sb_pre, struct str_buf sb_post); // Utilities Timestamp convert_ts(uint16_t ts); // Callbacks void print_cb( Node n, void *data, void **carry_out, void *from_parent, void **to_children ); void stats_cb( Node n, void *data, void **carry_out, void *from_parent, void **to_children ); void decompress_pre( Node n, void *decom_ptr, void **carry_out, void *from_parent, void **to_children ); void decompress_post( Node n, void *data, void *carry_in ); void create_tree_cb( Node n, void *data, void **carry_out, void *from_parent, void **to_children ); void serialize_cb(MetaNode *m, void *data); // // === FUNCTIONS === // void asserts(void) { // binary overlay size checks assert(sizeof(Record) == 24); // Record struct incorrect size assert(sizeof(Group) == 24); // Group struct incorrect size assert(sizeof((Group) { 0 }.label) == 4); // Label union incorrect size assert(sizeof(Field) == 6); // Field struct incorrect size assert(sizeof(MetaNode) == 64); // 1 cache line // zlib compatability assert(sizeof(uLongf) == sizeof(uint32_t)); assert(sizeof(Bytef) == sizeof(char)); } void espr_walk( SizedBuf esp, struct walker_callbacks cb, void *from_parent ) { // check assertions that cannot be checked at compile time asserts(); // check that we are at the start of the file { Type4 *type = NULL; int err = sb_peek(esp, sizeof(Type4), &type); assert(err == 0); assert(type->uint == rt[TES4]); } walk_concat(&esp, cb, from_parent); assert(sb_empty(esp)); } /* Unknown data will be some concatenation of groups and records. * * `walk_concat` will call the appropriate walking function * for each segment of unknown data in this concatenation. */ void walk_concat(SizedBuf *tree, struct walker_callbacks cb, void *from_parent) { while (!sb_empty(*tree)) { const Type4 *type = NULL; int err = sb_peek(*tree, sizeof(Type4), &type); assert(err == 0); // check valid type assert(rt[rt_hash(type->uint)] == type->uint); // only need to distinguish between groups and records if (type->uint == rt[GRUP]) walk_group(tree, cb, from_parent); else walk_record(tree, cb, from_parent); } } /* Walk a group record. Group records are containers for any other type of * record, including other group records. * * This function will also call `cb` with the node constructed from this group * record. */ void walk_group(SizedBuf *tree, struct walker_callbacks cb, void *from_parent) { Group *header = NULL; { int err = sb_peek(*tree, sizeof(Group), &header); assert(err == 0); } SizedBuf group = { .size = header->size }; { int err = sb_recast(tree, group.size, &group.data); assert(err == 0); err = sb_recast(&group, sizeof(Group), &header); assert(err == 0); } Node n = { .header.group = header, .data = group.data, .type = NT_GROUP }; void *carry = NULL; void *to_children = NULL; // Pre-walk callback if (cb.pre) cb.pre(n, cb.data, &carry, from_parent, &to_children); // Walk through the concatenation of data inside the group. walk_concat(&group, cb, to_children); assert(sb_empty(group)); // Post-walk callback if (cb.post) cb.post(n, cb.data, carry); } void walk_record(SizedBuf *tree, struct walker_callbacks cb, void *from_parent) { Record *header = NULL; char *data = NULL; { int err = sb_recast(tree, sizeof(Record), &header); assert(err == 0); err = sb_recast(tree, header->size, &data); assert(err == 0); } assert(header->type.uint != rt[GRUP]); Node n = { .header.record = header, .data = data, .type = NT_RECORD }; void *carry = NULL; void *to_children = NULL; /* Pre and post walk callbacks make less sense for record walking as * records are leaf-ish, will still call both here for now as field * walking may be added in the future. */ // Pre-walk callback if (cb.pre) cb.pre(n, cb.data, &carry, from_parent, &to_children); // Post-walk callback if (cb.post) cb.post(n, cb.data, carry); } void espr_print(SizedBuf esp) { struct walker_callbacks cb = { .pre = print_cb }; espr_walk(esp, cb, NULL); } void print_cb( Node n, void *data, void **carry_out, void *from_parent, void **to_children ) { (void)data; (void)carry_out; (void)from_parent; (void)to_children; switch (n.type) { case NT_GROUP: print_group_header(n.header.group); break; case NT_RECORD: print_record_header(n.header.record); break; default: assert(false); // invalid node type } } ESPStats espr_stats(SizedBuf esp) { ESPStats stats = { 0 }; struct walker_callbacks cb = { .pre = stats_cb, .data = &stats }; espr_walk(esp, cb, NULL); return stats; } /* Tallies up the group and record count. Calculates uncompressed size; groups * only need their header size tallied as their data size will be handled by * further walking of the tree. */ void stats_cb( Node n, void *data, void **carry_out, void *from_parent, void **to_children ) { (void)carry_out; (void)from_parent; (void)to_children; struct esp_stats *stats = data; switch (n.type) { case NT_GROUP: stats->group_count++; stats->decompressed_size += sizeof(Group); break; case NT_RECORD: stats->record_count++; stats->decompressed_size += sizeof(Record); if (n.header.record->flags & COMPRESSED_FLAG) { // uncompressed size is stored in the first 4 bytes of // data stats->decompressed_size += *((uint32_t *)n.data); } else { stats->decompressed_size += n.header.record->size; } break; default: assert(false); // invalid node type } } void espr_decompress(SizedBuf esp, SizedBuf decom) { struct walker_callbacks cb = { .pre = decompress_pre, .post = decompress_post, .data = &decom }; espr_walk(esp, cb, NULL); } /* Handles the copying of groups and records, and the decompression of * compressed record data. * * For groups it copies only the header as group data will be handled by further * walking. The destination prior to copying will also be saved to carry_out for * groups so that decompress_post can correctly update the size of the copied * group. * * For uncompressed records it simply copies the entirety of the record to the * destination. For compressed records in copies the header first and then * directly decompresses the compressed record into the destination. */ void decompress_pre( Node n, void *out_buf, void **carry_out, void *from_parent, void **to_children ) { (void)from_parent; (void)to_children; SizedBuf *d = out_buf; switch (n.type) { case NT_RECORD: // compressed record if (n.header.record->flags & COMPRESSED_FLAG) { Record *copied_header = NULL; { // Acces for copied header int err = sb_peek(*d, sizeof(Record), &copied_header); assert(err == 0); // copy header err = sb_copyin(d, (char *)n.header.record, sizeof(Record)); assert(err == 0); } // decompress directly into buffer // first 4 bytes are the decompressed size const uint32_t dc_size = *((uint32_t *)n.data); uint32_t to_copy = dc_size; uint32_t cur_size = n.header.record->size - sizeof(uint32_t); char *data_start = n.data + sizeof(uint32_t); // check that we have enough space in the buffer sb_check(*d, dc_size); int ret = uncompress( (Bytef *)d->data, (uLongf *)&to_copy, (Bytef *)data_start, (uLong)cur_size ); assert(ret == Z_OK); assert(to_copy == dc_size); // update the buffer sb_update(d, dc_size); // update header data size copied_header->size = dc_size; // unset compressed flag copied_header->flags &= ~COMPRESSED_FLAG; } else { // copy record size_t record_size = sizeof(Record) + n.header.record->size; sb_copyin(d, (char *)n.header.record, record_size); } break; case NT_GROUP: // save copied header location for post-walk group size recalc *carry_out = (void *)d->data; // copy header, contents will be copied while walking sb_copyin(d, (char *)n.header.group, sizeof(Group)); break; default: assert(false); // invalid node type } } /* Handles recalculating group size after decompression. The location of the * the group's copied header will be passed in in carry_in and can be used both * to access the copied group header and calculate the new size of the group * based on the difference between the current destination pointer and the * group header pointer. */ void decompress_post(Node n, void *out, void *carry_in) { SizedBuf *d = out; // only need to handle group resize if (n.type == NT_GROUP) { Group *g = (Group *)carry_in; uint32_t new_size = (uint32_t)((char *)d->data - (char *)g); g->size = new_size; } } void print_group_header(Group *header) { assert(header->type < GTS_SIZE); // Guess at enough with significant margin char buf[1024] = { 0 }; const struct str_buf sb_pre = { .buf = buf, .size = sizeof(buf) }; struct str_buf sb = sb_pre; // literals struct str_lit l1 = STR_LIT("--- HEADER: GROUP ---"), l2 = STR_LIT("\nType: "), l3 = STR_LIT("\nSize: "), l4 = STR_LIT("\nLabel: "), l5 = STR_LIT("\nGroup type: "), l6 = STR_LIT("\nTimestamp: "), l7 = STR_LIT("\nVersion Control Info: "), l8 = STR_LIT("\nUnknown: "), l9 = STR_LIT("\n"); struct str_lit gt = group_type_strings[header->type]; // construct output litcopy(&sb, l1); litcopy(&sb, l2); type_str(&sb, header->grup); litcopy(&sb, l3); num_str(&sb, header->size, 10); litcopy(&sb, l4); group_label_str(&sb, header); litcopy(&sb, l5); litcopy(&sb, gt); litcopy(&sb, l6); timestamp_str(&sb, header->timestamp); litcopy(&sb, l7); num_str(&sb, header->vcinfo, 16); litcopy(&sb, l8); num_str(&sb, header->unknown, 16); litcopy(&sb, l9); sb_write(STDOUT_FILENO, sb_pre, sb); } void litcopy(struct str_buf *sb, struct str_lit lit) { assert(sb->size >= lit.size); memcpy(sb->buf, lit.lit, lit.size); sb->size -= lit.size; sb->buf += lit.size; } void num_str(struct str_buf *sb, unsigned long num, int radix) { errno_t ret = _ultoa_s(num, sb->buf, sb->size, radix); assert(ret == 0); int len = (int)strlen(sb->buf); sb->size -= len; sb->buf += len; } void type_str(struct str_buf *sb, Type4 type) { assert(sb->size >= 4); for (size_t i = 0; i != 4; i++) sb->buf[i] = type.bytes[i]; sb->buf += 4; sb->size -= 4; } void group_label_str(struct str_buf *sb, Group *header) { switch (header->type) { case GT_TOP: type_str(sb, header->label.type); break; case GT_INTERIOR_CELL_BLOCK: case GT_INTERIOR_CELL_SUBBLOCK: num_str(sb, header->label.number, 10); break; case GT_EXTERIOR_CELL_BLOCK: case GT_EXTERIOR_CELL_SUBBLOCK: uint16_t x = header->label.coord[1], y = header->label.coord[0]; litcopy(sb, LIT("X: ")); num_str(sb, x, 10); litcopy(sb, LIT("Y: ")); num_str(sb, y, 10); break; case GT_WORLD_CHILDREN: case GT_CELL_CHILDREN: case GT_TOPIC_CHILDREN: case GT_CELL_PERSISTENT_CHILDREN: case GT_CELL_TEMPORARY_CHILDREN: litcopy(sb, LIT("FormID[")); num_str(sb, header->label.formid, 16); litcopy(sb, LIT("]")); break; default: assert(false); // invalid group type } } void timestamp_str(struct str_buf *sb, uint16_t timestamp) { Timestamp ts = convert_ts(timestamp); litcopy(sb, LIT("20x")); num_str(sb, ts.year, 10); litcopy(sb, LIT("-")); num_str(sb, ts.month, 10); litcopy(sb, LIT("-")); num_str(sb, ts.day, 10); } void sb_write(int fp, struct str_buf sb_pre, struct str_buf sb_post) { int size = sb_pre.size - sb_post.size; assert(size >= 0); int ret = _write(fp, sb_pre.buf, size); assert(ret == size); } void print_record_header(Record *header) { char buf[1024] = { 0 }; const struct str_buf sb_pre = { .buf = buf, .size = sizeof(buf) }; struct str_buf sb = sb_pre; const struct str_lit l1 = LIT("--- HEADER: RECORD ---"), l2 = LIT("\nType: "), l3 = LIT("\nFlags: "), l4 = LIT("\nFormID: "), l5 = LIT("\nTimestamp: "), l6 = LIT("\nVersion Control Info: "), l7 = LIT("\nVersion: "), l8 = LIT("\nUnknown: "), l9 = LIT("\n"); litcopy(&sb, l1); litcopy(&sb, l2); type_str(&sb, header->type); litcopy(&sb, l3); record_flags_str(&sb, header); litcopy(&sb, l4); num_str(&sb, header->formid, 16); litcopy(&sb, l5); timestamp_str(&sb, header->timestamp); litcopy(&sb, l6); num_str(&sb, header->vcinfo, 16); litcopy(&sb, l7); num_str(&sb, header->version, 10); litcopy(&sb, l8); num_str(&sb, header->unknown, 16); litcopy(&sb, l9); sb_write(STDOUT_FILENO, sb_pre, sb); } void record_flags_str(struct str_buf *sb, Record *header) { uint32_t flags = header->flags; const uint32_t type = header->type.uint; // print flags if (type == rt[REFR]) { // TODO // REFR requires FormID lookup flags = 0; } else { rfs_inner *const flag_lut = rfs[rt_hash(type)]; if (flag_lut) { while (flags != 0) { // get next flag, from lowest bit to highest // will always be >= 0 as flags is not 0 int lowest = _tzcnt_u32(flags); assert(lowest < 32); // get flag string const struct str_lit lit = (*flag_lut)[lowest]; // not a valid flag if (!lit.lit) break; // copy flag string litcopy(sb, LIT("\n - ")); litcopy(sb, lit); // remove flag from to be processed flags ^= ((uint32_t)1) << lowest; } } } // slow path if (flags != 0) { printf("\n\nOriginal flags: %08x\n", header->flags); printf("Unhandled flags: %08x\n", flags); assert(false); // unhandled flags } } /* Converts the bit-packed/encoded timestamp used in esp/esm files into day, * month and year. See UESP for further explanation. * * This currently handles the timestamp format used in Skyrim.esm, but newer * files apparently use a different format. This will need to be handled later. */ Timestamp convert_ts(uint16_t ts) { /* const uint8_t day = (uint8_t)(ts & day_mask); const uint8_t month = (uint8_t)((ts >> month_offset) & month_mask); const uint16_t year = (ts >> year_offset) & year_mask; */ const uint8_t day = ts & 0xff; const uint8_t hb = (ts >> 8) & 0xff; const uint8_t month = ((hb - 1) % 12) + 1; const uint8_t year = ((hb - 1) / 12 + 3) % 10; return (Timestamp) { year, month, day }; } MetaTree espr_create_tree(SizedBuf in, SizedBuf tree) { // create root node MetaNode *root = NULL; { int err = sb_recast(&tree, sizeof(MetaNode), &root); assert(err == 0); } *root = (MetaNode){ 0 }; // walk struct walker_callbacks cb = { .pre = create_tree_cb, .data = &tree }; espr_walk(in, cb, root); return (MetaTree) { .root = root, .size = in.size }; } void create_tree_cb( const Node n, void *data, void **carry_out, void *from_parent, void **to_children ) { (void)carry_out; // add new metanode to tree SizedBuf *tree = data; MetaNode *m = NULL; { int err = sb_recast(tree, sizeof(MetaNode), &m); assert(err == 0); } // parent passes their MetaNode to children MetaNode *p = from_parent; // construct new node *m = (MetaNode){ 0 }; // zero/null unused m->n = n; m->parent = p; m->prev = p->last_child; // the linked list of children may not already exist if (p->last_child) p->last_child->next = m; else p->first_child = m; p->last_child = m; // send self to children *to_children = m; } void espr_meta_walk(MetaTree tree, struct meta_callbacks cb) { espr_meta_node_walk(tree.root, cb); } void espr_meta_node_walk(MetaNode *m, struct meta_callbacks cb) { cb.pre(m, cb.data); MetaNode *child = m->first_child; while (child) { espr_meta_node_walk(child, cb); child = child->next; } } void espr_serialize(MetaTree tree, SizedBuf out) { struct meta_callbacks cb = { .pre = serialize_cb, .data = &out }; espr_meta_walk(tree, cb); } void serialize_cb(MetaNode *m, void *data) { SizedBuf *out = data; // exit on empty node if (!m->n.data) return; switch (m->n.type) { case NT_GROUP: // only serialize the header of groups { int err = sb_copyin(out, (char *)m->n.header.group, sizeof(Group)); assert(err == 0); } break; case NT_RECORD: size_t data_size = m->n.header.record->size; char *header = (char *)m->n.header.record; // serialize header and data separately as they may be // discontiguous { int err = sb_copyin(out, header, sizeof(Record)); assert(err == 0); err = sb_copyin(out, m->n.data, data_size); assert(err == 0); } break; default: assert(false); } }