/*! * Add an entry to a journal. * * If allocation fails, the journal's internal state is fully recoverable. * * \param[in,out] journal Journal * \param[in] origin Origin * \param[in] offset Offset * \param[in] delta Delta * \return Error code */ extern pb_error_t pb_journal_log( pb_journal_t *journal, size_t origin, size_t offset, ptrdiff_t delta) { assert(journal && origin <= offset && delta); if (unlikely_(!pb_journal_valid(journal))) return PB_ERROR_INVALID; /* If no bulk slot is available, grow bulk */ if (!journal->entry.bulk) { pb_journal_entry_t *new_data = pb_allocator_realloc( journal->allocator, journal->entry.data, sizeof(pb_journal_entry_t) * journal->entry.size << 1); if (new_data) { journal->entry.data = new_data; journal->entry.bulk = journal->entry.size; } else { return PB_ERROR_ALLOC; } } /* Create and append a new journal entry */ pb_journal_entry_t entry = { .origin = origin, .offset = offset, .delta = delta }; journal->entry.data[journal->entry.size++] = entry; journal->entry.bulk--; return PB_ERROR_NONE; }
/*! * Adjust the length prefix of the part by the provided delta to reflect the * current length of the part, and return the (possibly) adjusted delta. * * \param[in,out] part Part * \param[in,out] delta Delta * \return Error code */ static pb_error_t adjust_prefix(pb_part_t *part, ptrdiff_t *delta) { assert(part && part->offset.diff.length); assert(pb_part_valid(part) && pb_part_aligned(part)); pb_error_t error = PB_ERROR_NONE; /* Write new length prefix to binary */ uint32_t bytes = pb_part_size(part); pb_binary_buffer_t buffer = pb_binary_buffer_create(); if (!(error = pb_binary_buffer_write_varint32(&buffer, &bytes))) { ptrdiff_t adjust = pb_binary_buffer_size(&buffer) + part->offset.diff.length; if (likely_(!adjust)) { error = pb_binary_write(part->binary, part->offset.start + part->offset.diff.length, part->offset.start, pb_binary_buffer_data(&buffer), pb_binary_buffer_size(&buffer)); /* Length of length prefix changed */ } else { pb_journal_t *journal = pb_binary_journal(part->binary); assert(journal); /* Write new length prefix to binary and perform manual alignment */ pb_error_t error = pb_journal_log(journal, part->offset.start + part->offset.diff.length, part->offset.start, adjust); if (!error) { error = pb_binary_write(part->binary, part->offset.start + part->offset.diff.length, part->offset.start, pb_binary_buffer_data(&buffer), pb_binary_buffer_size(&buffer)); if (unlikely_(error)) { pb_journal_revert(journal); /* LCOV_EXCL_LINE */ /* Update offsets */ } else { /* LCOV_EXCL_LINE */ part->version++; part->offset.start += adjust; part->offset.end += adjust; part->offset.diff.origin -= adjust; part->offset.diff.tag -= adjust; part->offset.diff.length -= adjust; /* Adjust delta for subsequent updates */ *delta += adjust; } } } } pb_binary_buffer_destroy(&buffer); return error; }
/*! * Move a cursor to the next value of a packed field. * * \param[in,out] cursor Cursor * \return Test result */ static int next_packed(pb_cursor_t *cursor) { assert(cursor); pb_offset_t *offset = &(cursor->current.offset), *packed = &(cursor->current.packed); /* Create temporary buffer to read the next value of the packed field */ pb_buffer_t buffer = pb_buffer_create_zero_copy_internal( pb_journal_data_from(pb_cursor_journal(cursor), offset->end), packed->end - offset->end); /* Create stream over temporary buffer */ pb_stream_t stream = pb_stream_create(&buffer); while (pb_stream_left(&stream)) { /* Skip field contents to determine length */ pb_wiretype_t wiretype = pb_field_descriptor_wiretype(cursor->current.descriptor); if (unlikely_((cursor->error = pb_stream_skip(&stream, wiretype)))) break; /* Adjust offsets */ offset->diff.origin -= offset->end - offset->start; offset->start = offset->end; offset->end += pb_stream_offset(&stream); /* Cleanup and return with success */ pb_stream_destroy(&stream); pb_buffer_destroy(&buffer); return 1; } /* Cleanup to move on to next field */ pb_stream_destroy(&stream); pb_buffer_destroy(&buffer); /* Switch back to non-packed context, as end is reached */ *offset = *packed; return packed->end = 0; }
/*! * Align an offset of a specific version according to a journal. * * This is the core of the journaling functionality, as it allows several * different parts to co-exist at the same time, reading or writing values * from or to the underlying binary. However, it's very tricky to get right, * as there are three major scenarios as well as some edge cases: * * -# The change happened before the current part, so the part must be moved by * the given delta. There are two cases which can trigger this scenario: * * -# Update on a preceding part, either located in the current message, in * the preceding message or in a parent message. * * -# Update on the length prefix of the current part, which is done when * altering the contents of a length-prefixed field or message. * * \code{.unparsed} * ... [ T ][ L ][ ... ] ... * ^ (a) * ^^^^^^^^^^^^^^^^ * ^ (b) * \endcode * * -# The change happened within the current part, so the part must either be * resized or cleared completely: * * -# The update happened somewhere inside the current part, which makes it * necessary to resize the current part by the given delta. * * -# The update involves the whole part from start to end and a negative * delta is given which matches its size, so the part was cleared. * * \code{.unparsed} * ... [ T ][ L ][ ... ] ... * ^ (a) * ^^^^^^^^^^^^^^^^^ (b) * \endcode * * -# The change happened outside the current part and the delta is negative, * which means that the current part must be cleared completley. * * \code{.unparsed} * ... [ T ][ L ][ ... ] ... * ^^^^^^^^^^^^^^^^^^^^^ * \endcode * * If a parent message is resized, nothing needs to be done, as the current * part is contained and seemingly not affected by the update, so this is a * case that doesn't need to be explicitly handled. This is repeated until * the provided version matches the current version of the journal. * * \param[in] journal Journal * \param[in,out] version Version * \param[in,out] offset Offset * \return Error code */ extern pb_error_t pb_journal_align( const pb_journal_t *journal, pb_version_t *version, pb_offset_t *offset) { assert(journal && offset); if (unlikely_(!pb_journal_valid(journal))) return PB_ERROR_INVALID; /* Iterate journal entries until we're up-to-date */ uint8_t invalid = 0; while (*version < journal->entry.size) { const pb_journal_entry_t *entry = &journal->entry.data[*version]; /* Change happened before current part: move */ if (entry->origin < offset->start && entry->offset < offset->end) { offset->start += entry->delta; offset->end += entry->delta; /* Apply potential update on relative offsets */ ptrdiff_t *diff[3] = { &(offset->diff.origin), &(offset->diff.tag), &(offset->diff.length) }; for (size_t d = 0; d < 3; d++) if (entry->offset > offset->start + *(diff[d]) - entry->delta) *(diff[d]) -= entry->delta; /* Change happened within current part: resize or clear */ } else if (entry->origin >= offset->start + offset->diff.origin && entry->offset <= offset->end) { /* Change happened inside current part: resize */ if (entry->origin >= offset->start) { offset->end += entry->delta; /* Current part was cleared: clear */ } else if ((offset->start + offset->diff.origin) - (offset->end + entry->delta) == 0) { offset->start += offset->diff.origin; offset->end += entry->delta; offset->diff.origin = 0; offset->diff.tag = 0; offset->diff.length = 0; /* Invalidate after alignment */ invalid = 1; } /* Change happened outside current part: clear */ } else if (entry->origin <= offset->start + offset->diff.origin && entry->origin == entry->offset + entry->delta) { offset->start = entry->origin; offset->end = entry->origin; offset->diff.origin = 0; offset->diff.tag = 0; offset->diff.length = 0; /* Invalidate after alignment */ invalid = 1; } (*version)++; } /* Invalidate part */ if (invalid) *version = SIZE_MAX; return *version == SIZE_MAX ? PB_ERROR_INVALID : PB_ERROR_NONE; }
/*! * Initialize a part. * * The journal must be tested for writability before writing to the binary, as * the binary write is irreversible, while the journal could be reverted. * * \warning Some lines excluded from code coverage are impossible to occur, * but they are necessary to ensure application-wide proper error handling. * Others, especially those concerning the journal and binary, are not * testable within the context of a part. * * \param[in,out] part Part * \param[in] descriptor Field descriptor */ static void init(pb_part_t *part, const pb_field_descriptor_t *descriptor) { assert(part && descriptor); assert(pb_part_aligned(part)); /* Initialize field for writing */ pb_binary_buffer_t buffer = pb_binary_buffer_create(); do { pb_wiretype_t wiretype = pb_field_descriptor_wiretype(descriptor); pb_tag_t tag = pb_field_descriptor_tag(descriptor); /* Write tag to buffer and backup offsets */ uint32_t value = wiretype | (tag << 3); if (pb_binary_buffer_write_varint32(&buffer, &value)) break; /* LCOV_EXCL_LINE */ part->offset.diff.tag = part->offset.start; part->offset.diff.length = part->offset.start + pb_binary_buffer_size(&buffer); /* Write default length of zero for length-prefixed fields */ if (wiretype == PB_WIRETYPE_LENGTH) { uint32_t bytes = 0; if (pb_binary_buffer_write_varint32(&buffer, &bytes)) break; /* LCOV_EXCL_LINE */ } /* New data must be written to part, so obtain journal */ pb_journal_t *journal = pb_binary_journal(part->binary); assert(journal); /* Write buffer to binary and perform manual alignment */ pb_error_t error = pb_journal_log(journal, part->offset.start + part->offset.diff.origin, part->offset.end, pb_binary_buffer_size(&buffer)); if (!error) { size_t offset = pb_binary_buffer_size(&buffer); error = pb_binary_write(part->binary, part->offset.start, part->offset.end, pb_binary_buffer_data(&buffer), pb_binary_buffer_size(&buffer)); if (unlikely_(error)) { pb_journal_revert(journal); /* LCOV_EXCL_LINE */ /* Update offsets */ } else { /* LCOV_EXCL_LINE */ part->version++; part->offset.start += offset; part->offset.end += pb_binary_buffer_size(&buffer); part->offset.diff.origin -= offset; part->offset.diff.tag -= part->offset.start; part->offset.diff.length -= part->offset.start; /* Recursive length prefix update of parent messages */ if (!adjust(part, pb_binary_buffer_size(&buffer))) { pb_binary_buffer_destroy(&buffer); return; } } } /* LCOV_EXCL_LINE */ } while (0); /* LCOV_EXCL_LINE */ /* Yes. You pulled a Pobert */ pb_binary_buffer_destroy(&buffer); /* LCOV_EXCL_LINE */ pb_part_invalidate(part); /* LCOV_EXCL_LINE */ }
/*! * Perform a recursive length prefix update on all messages containing the * given part and possibly the part as such up to its origin. * * This is where all the magic happens. This function takes a binary stream and * reads data from it, until it reaches a length-prefixed field. Messages are * always length-prefixed, so binary parts that are not can be safely skipped. * If the binary stream's current offset matches the tag offset of the part * exactly, and the part is empty, we ran into an initialization update, so we * can directly abort here. * * Otherwise, if the binary part contains the part, and thus does not match * exactly, we must have found a submessage and need to recurse. The length * prefixes must be updated from within, since they are encoded as * variable-sized integers and may also affect the size of parent messages. * * After recursing, it is mandatory to check if the part needs to be * realigned, since there may have been updates on the length prefixes of * submessages that affect the offsets of the part, and perform such a * realignment if necessary. * * Finally, we write the new length prefix and adjust the delta value if the * new length prefix exceeds the length of the current one. That easy. * * \warning The lines excluded from code coverage are impossible to occur, * but they are necessary to ensure application-wide proper error handling. * * \param[in,out] part Part * \param[in,out] stream Binary stream * \param[in,out] delta Delta * \return Error code */ static pb_error_t adjust_recursive( pb_part_t *part, pb_binary_stream_t *stream, ptrdiff_t *delta) { assert(part && stream && delta && *delta); assert(pb_part_aligned(part)); pb_error_t error = PB_ERROR_NONE; /* Read from the binary stream until the end of the part */ while (pb_binary_stream_offset(stream) < part->offset.end) { if (pb_binary_stream_offset(stream) == part->offset.start - *delta && pb_part_empty(part)) break; /* Skip non-length-prefixed fields */ pb_tag_t tag = 0; uint32_t bytes; size_t offset = pb_binary_stream_offset(stream); if ((error = pb_binary_stream_read_varint32(stream, &tag))) break; /* LCOV_EXCL_LINE */ if ((tag & 7) != PB_WIRETYPE_LENGTH) { if (!skip_jump[tag & 7] || (error = skip_jump[tag & 7](stream))) break; /* LCOV_EXCL_LINE */ continue; } /* Create a temporary part, so we can use adjust_prefix() */ pb_part_t temp = { .binary = pb_part_binary(part), .version = pb_part_version(part), .offset = { .start = 0, .end = 0, .diff = { .origin = offset, .tag = offset, .length = pb_binary_stream_offset(stream) } } }; /* Read length from length-prefixed field */ if ((error = pb_binary_stream_read_varint32(stream, &bytes))) break; /* LCOV_EXCL_LINE */ /* Update offsets */ temp.offset.start = pb_binary_stream_offset(stream); temp.offset.end = temp.offset.start + bytes; temp.offset.diff.origin -= temp.offset.start; temp.offset.diff.tag -= temp.offset.start; temp.offset.diff.length -= temp.offset.start; /* Abort, if we're past the origin */ if (temp.offset.start > part->offset.start + part->offset.diff.origin) break; /* The temporary part lies within the part */ if (temp.offset.start <= part->offset.start && temp.offset.end >= part->offset.end - *delta) { temp.offset.end += *delta; /* The parts don't match, so recurse */ if (temp.offset.start != part->offset.start || temp.offset.end != part->offset.end) { pb_binary_stream_t substream = pb_binary_stream_create_at( pb_binary_stream_binary(stream), temp.offset.start); error = adjust_recursive(part, &substream, delta); pb_binary_stream_destroy(&substream); if (unlikely_(error)) break; /* LCOV_EXCL_LINE */ /* Parts may be unaligned due to length prefix update */ if ((!pb_part_aligned(part) && (error = pb_part_align(part))) || (!pb_part_aligned(&temp) && (error = pb_part_align(&temp)))) break; /* LCOV_EXCL_LINE */ } /* Adjust length prefix */ error = adjust_prefix(&temp, delta); break; /* Otherwise just skip binary part */ } else if ((error = pb_binary_stream_skip(stream, bytes))) { break; /* LCOV_EXCL_LINE */ } } /* Invalidate part on error */ if (unlikely_(error)) pb_part_invalidate(part); /* LCOV_EXCL_LINE */ return error; }