From ab77f943483a91d6db2b974d930f5d31650a9548 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josef=20S=C3=B6ntgen?= Date: Tue, 1 May 2018 15:29:31 +0200 Subject: [PATCH] gems: add tool to write a GPT to a Block device This component creates a GPT on a Block device. It supports the common actions, as in adding, deleting and modifying entries in the GPT, while considering alignment constraints. If needed it will round the length of a partition down to meet those constraints. The component will not perform layout checking, i.e., it does not care about overlapping partitions. Only when apping a partition it will make sure that the partition will fit. Please read _repos/gems/src/app/gpt_write/README_ for more detailed information on how to use the component and feel free to check out _repos/gems/run/gpt_write.run_. Fixes #2814. --- repos/gems/run/gpt_write.run | 73 +++ repos/gems/src/app/gpt_write/README | 99 ++++ repos/gems/src/app/gpt_write/gpt.h | 453 +++++++++++++++ repos/gems/src/app/gpt_write/main.cc | 765 +++++++++++++++++++++++++ repos/gems/src/app/gpt_write/pmbr.h | 48 ++ repos/gems/src/app/gpt_write/target.mk | 4 + repos/gems/src/app/gpt_write/util.cc | 227 ++++++++ repos/gems/src/app/gpt_write/util.h | 169 ++++++ 8 files changed, 1838 insertions(+) create mode 100644 repos/gems/run/gpt_write.run create mode 100644 repos/gems/src/app/gpt_write/README create mode 100644 repos/gems/src/app/gpt_write/gpt.h create mode 100644 repos/gems/src/app/gpt_write/main.cc create mode 100644 repos/gems/src/app/gpt_write/pmbr.h create mode 100644 repos/gems/src/app/gpt_write/target.mk create mode 100644 repos/gems/src/app/gpt_write/util.cc create mode 100644 repos/gems/src/app/gpt_write/util.h diff --git a/repos/gems/run/gpt_write.run b/repos/gems/run/gpt_write.run new file mode 100644 index 0000000000..1ace87a057 --- /dev/null +++ b/repos/gems/run/gpt_write.run @@ -0,0 +1,73 @@ +# +# Test runs only on Linux +# +assert_spec linux + +# +# Build +# +build { core init drivers/timer server/lx_block app/gpt_write } + +create_boot_directory + +# +# Generate config +# +install_config { + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +} + +# +# Create test file +# +catch { exec dd if=/dev/zero of=bin/gpt.img bs=1M count=256 } + +# +# Boot modules +# +build_boot_image { + core ld.lib.so init timer lx_block gpt_write + gpt.img +} + +run_genode_until {child "gpt_write" exited with exit value 0.*\n} 10 + +exec rm -f bin/gpt.img diff --git a/repos/gems/src/app/gpt_write/README b/repos/gems/src/app/gpt_write/README new file mode 100644 index 0000000000..c4dcdb3c93 --- /dev/null +++ b/repos/gems/src/app/gpt_write/README @@ -0,0 +1,99 @@ +Brief +===== + +This component creates a GPT on a Block device. It supports the common +actions, as in adding, deleting and modifying entries in the GPT, while +considering alignment constraints. If needed it will round the length of +a partition down to meet those constraints. The component will not perform +layout checking, i.e., it does not care about overlapping partitions. Only +when apping a partition it will make sure that the partition will fit. + +As a temporary limitation it does not handle Block devices with a logical +block size of 4096 or larger well. + + +Configuration +============= + +The component is instructed to manage the GPT by specifying actions. +The actions are: + + * 'add' for adding a new partition. The used entry is set by + the 'entry' attribute, while the 'start' attributes denotes the + starting LBA of the partition. If the 'entry' attribute is missing, + the component operates in append-mode and the next free entry will + be used, while the component selects a proper start LBA and ignores + the 'start' attribute. The mandatory 'label', 'size' and 'type' + attributes must be used to set the corresponding property. + + * 'delete' for removing partitions by clearing the corresponding entry. + The entry can by selected by using either the 'entry' or 'label' + attribute. + + * 'modify' for changing properties of an existing entry. The + entry can be selected by using either the 'entry' or 'label' + attribute. The 'new_label', 'new_size' and 'new_type' attributes + may be set to change the corresponding property. + +The size-related attributes, 'size' and 'new_size', can take as value +a suffixed number (K, M, G) to interpret the value to the base of 2^10, +2^20 or 2^30. As special treatment it is also possible to use 'max' as +value. In this case the component will try to use all free space between +this partition and the next partition or, for that matter, the end of the +addressable space within the GPT. + +The type-related attributes, 'type' and 'new_type', take a identifier, +which selects the proper GUID. Valid identifiers are: + + * 'EFI' for the EFI system partition type + * 'BIOS' for a BIOS boot partition type (for GRUB to store its core.img) + * 'BDP' for basic data partition type (Windows/DOS file systems) + * 'Linux' for a Linux file system data type (Linux file systems) + +The label-related attributes, 'label' and 'new_label', take a ASCII string, +i.e., [A-Za-z- *]. + +There are config node attributes, in addition to the actions, that instruct +the component to perform additional tasks. + +To remove the GPT headers as well as the protective MBR the 'wipe' +attribute can be specified: + +! + +In this case 'gpt_write' will only perform the wiping and than exit, +even if other attributes are set or actions specified. + +To update the GPT information, in case the underlying Block device has +changed (for example a generated disk image is copied to a larger USB +stick), the 'update_geometry' attribute may be used to increase the +useable space of the GPT: + +! + +Setting the 'initialize' attribute will instruct the component to clear +any existing GPT/PMBR. + +! + +The alignment of partitions can by specified by setting the 'align' +attribute. It defaults to '4096' if not set. + +These attributes can be freely mixed, considering the constraint of +using 'wipe'. + +The following snippet shows a examplary configuration: + +! +! +! +! +! +! +! +! +! +! +! +! + diff --git a/repos/gems/src/app/gpt_write/gpt.h b/repos/gems/src/app/gpt_write/gpt.h new file mode 100644 index 0000000000..e06c083f9a --- /dev/null +++ b/repos/gems/src/app/gpt_write/gpt.h @@ -0,0 +1,453 @@ +/* + * \brief GUID Partition table definitions + * \author Josef Soentgen + * \date 2014-09-19 + */ + +/* + * Copyright (C) 2014-2017 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU Affero General Public License version 3. + */ + +#ifndef _GPT_H_ +#define _GPT_H_ + +/* Genode includes */ +#include + +/* local includes */ +#include + +namespace Gpt { + + using namespace Genode; + + /*************** + ** Datatypes ** + ***************/ + + using Type = Genode::String<32>; + using Label = Genode::String<32>; + + /** + * DCE uuid struct + */ + struct Uuid + { + enum { UUID_NODE_LEN = 6 }; + + uint32_t time_low; + uint16_t time_mid; + uint16_t time_hi_and_version; + uint8_t clock_seq_hi_and_reserved; + uint8_t clock_seq_low; + uint8_t node[UUID_NODE_LEN]; + + bool valid() const { + return time_low != 0 && time_hi_and_version != 0; + } + } __attribute__((packed)); + + + /** + * GUID parition table header + */ + struct Header + { + char signature[8]; /* identifies GUID Partition Table */ + uint32_t revision; /* GPT specification revision */ + uint32_t size; /* size of GPT header */ + uint32_t crc; /* CRC32 of GPT header */ + uint32_t reserved; /* must be zero */ + uint64_t lba; /* LBA that contains this header */ + uint64_t backup_lba; /* LBA of backup GPT header */ + uint64_t part_lba_start; /* first LBA usable for partitions */ + uint64_t part_lba_end; /* last LBA usable for partitions */ + Uuid guid; /* GUID to identify the disk */ + uint64_t gpe_lba; /* first LBA of GPE array */ + uint32_t gpe_num; /* number of entries in GPE array */ + uint32_t gpe_size; /* size of each GPE */ + uint32_t gpe_crc; /* CRC32 of GPE array */ + + /* the remainder of the struct must be zero */ + + bool valid(bool primary = true) + { + /* check sig */ + if (strcmp(signature, "EFI PART", 8) != 0) { + return false; + } + + /* check header crc */ + uint32_t const save_crc = crc; + crc = 0; + if (Util::crc32(this, size) != save_crc) { + error("wrong header checksum"); + return false; + } + + crc = save_crc; + + /* check header lba */ + if ((primary ? lba : backup_lba) != 1) { + return false; + } + + return true; + } + + bool entries_valid(void const *entries) const + { + size_t const length = gpe_num * gpe_size; + return Util::crc32(entries, length) == gpe_crc ? true : false; + } + } __attribute__((packed)); + + + /** + * GUID partition entry format + */ + struct Entry + { + enum { NAME_LEN = 36 }; + Uuid type; /* partition type GUID */ + Uuid guid; /* unique partition GUID */ + uint64_t lba_start; /* start of partition */ + uint64_t lba_end; /* end of partition */ + uint64_t attributes; /* partition attributes */ + uint16_t name[NAME_LEN]; /* partition name in UTF-16LE */ + + bool valid() const { return type.valid(); } + + /** + * Extract all valid ASCII characters in the name entry + */ + bool read_name(char *dest, size_t dest_len) const + { + return !!Util::extract_ascii(dest, dest_len, name, NAME_LEN); + } + + /** + * Write ASCII to name field + */ + bool write_name(Label const &label) + { + return !!Util::convert_ascii(name, NAME_LEN, + (uint8_t*)label.string(), + label.length() - 1); + } + + uint64_t length() const { return lba_end - lba_start + 1; } + } __attribute__((packed)); + + + /***************** + ** Definitions ** + *****************/ + + enum { REVISION = 0x00010000u, }; + + enum { + MIN_ENTRIES = 128u, + MAX_ENTRIES = MIN_ENTRIES, + ENTRIES_SIZE = sizeof(Entry) * MAX_ENTRIES, + PGPT_LBA = 1, + }; + + + /*************** + ** Functions ** + ***************/ + + /** + * Convert type string to UUID + * + * \param type type string + * + * \return UUID in case the type is known, otherwise exception is thrown + * + * \throw Invalid_type + */ + Uuid type_to_uuid(Type const &type) + { + struct { + char const *type; + Uuid uuid; + } _gpt_types[] = { + /* EFI System Partition */ + { "EFI", 0xC12A7328, 0xF81F, 0x11D2, 0xBA, 0x4B, 0x00, 0xA0, 0xC9, 0x3E, 0xC9, 0x3B }, + /* BIOS Boot Partition (GRUB) */ + { "BIOS", 0x21686148, 0x6449, 0x6E6F, 0x74, 0x4E, 0x65, 0x65, 0x64, 0x45, 0x46, 0x49 }, + /* Basic Data Partition (FAT32, exFAT, NTFS, ...) */ + { "BDP", 0xEBD0A0A2, 0xB9E5, 0x4433, 0x87, 0xC0, 0x68, 0xB6, 0xB7, 0x26, 0x99, 0xC7 }, + /* Linux Filesystem Data (for now used by Genode for Ext2 rootfs) */ + { "Linux", 0x0FC63DAF, 0x8483, 0x4772, 0x8E, 0x79, 0x3D, 0x69, 0xD8, 0x47, 0x7D, 0xE4 }, + }; + + size_t const num = sizeof(_gpt_types) / sizeof(_gpt_types[0]); + for (size_t i = 0; i < num; i++) { + if (type == _gpt_types[i].type) { + return _gpt_types[i].uuid; + } + } + + struct Invalid_type : Genode::Exception { }; + throw Invalid_type(); + } + + + /** + * Generate random UUID (RFC 4122 4.4) + * + * \return random UUID + */ + Uuid generate_uuid() + { + uint8_t buf[sizeof(Uuid)]; + Util::get_random(buf, sizeof(buf)); + + Uuid uuid; + Genode::memcpy(&uuid, buf, sizeof(buf)); + + uuid.time_hi_and_version = + (uuid.time_hi_and_version & 0x0fff) | 0x4000; + uuid.clock_seq_hi_and_reserved = + (uuid.clock_seq_hi_and_reserved & 0x3f) | 0x80; + + return uuid; + } + + + /** + * Get block gap to next logical entry + * + * \param header pointer to GPT header + * \param entry pointer to current entry + * \param entries pointer to entries + * + * \return the number of free blocks to the next logical entry + */ + Genode::uint64_t gap_length(Gpt::Header const &header, + Gpt::Entry const *entries, + Gpt::Entry const *entry) + { + using namespace Genode; + + /* add one block => end == start */ + uint64_t const end_lba = entry ? entry->lba_end + 1 : ~0ull; + + enum { INVALID_START = ~0ull, }; + uint64_t next_start_lba = INVALID_START; + + for (uint32_t i = 0; i < header.gpe_num; i++) { + Entry const *e = (entries + i); + + if (!e->valid() || e == entry) { continue; } + + /* + * Check if the entry starts afterwards and save the + * entry with the smallest distance. + */ + if (e->lba_start >= end_lba) { + next_start_lba = min(next_start_lba, e->lba_start); + } + } + + /* + * Use stored next start LBA or paritions end LBA from header, + * if there is no other entry or we are the only one. + */ + return (next_start_lba == INVALID_START ? header.part_lba_end + : next_start_lba) - end_lba; + } + + + /** + * Find free GPT entry + * + * \param header reference to GPT header + * \param entries pointer to memory containing GPT entries + * + * \return if a free entry is found a pointer to its memory + * is returned, otherwise a null pointer is returned + */ + Entry *find_free(Header const &header, Entry *entries) + { + if (!entries) { return nullptr; } + + Entry *result = nullptr; + for (size_t i = 0; i < header.gpe_num; i++) { + Entry *e = (entries + i); + + if (e->valid()) { continue; } + + result = e; + break; + } + + return result; + } + + + /** + * Get last valid entry + * + * \param header reference to GPT header + * \param entries pointer to memory containing GPT entries + * + * + * \return if a free entry is found a pointer to its memory + * is returned, otherwise a null pointer is returned + */ + Entry const *find_last_valid(Header const &header, Entry const *entries) + { + if (!entries) { return nullptr; } + + Entry const *result = nullptr; + for (size_t i = 0; i < header.gpe_num; i++) { + Entry const * const e = (entries + i); + + if (e->valid()) { result = e; } + } + + return result; + } + + + /** + * Get next free entry + */ + Entry *find_next_free(Header const &header, Entry *entries, + Entry const *entry) + { + if (!entries || !entry) { return nullptr; } + + size_t const num = (entry - entries + 1); + if (num >= header.gpe_num) { return nullptr; } + + Entry *result = nullptr; + for (size_t i = num; i < header.gpe_num; i++) { + Entry * const e = (entries + i); + + if (e->valid()) { continue; } + + result = e; + break; + } + + return result; + } + + + /** + * Lookup GPT entry + * + * \param entries pointer to memory containing GPT entries + * \param num number of GPT entries + * \param label name of the partition in the entry + * + * \return if entry is found a pointer to its memory location + * is returned, otherwise a null pointer is returned + */ + Entry *lookup_entry(Entry *entries, size_t num, Label const &label) + { + if (!entries) { return nullptr; } + + Entry *result = nullptr; + + char tmp[48]; + + for (size_t i = 0; i < num; i++) { + Entry *e = (entries + i); + + if (!e->valid()) { continue; } + + if (!e->read_name(tmp, sizeof(tmp))) { continue; } + + if (Genode::strcmp(label.string(), tmp) == 0) { + result = e; + break; + } + } + + return result; + } + + + /** + * Get number of entry + * + * \param entries pointer to memory containing GPT entries + * \param entry pointer to memory containing GPT entry + * + * \return number of entry + */ + uint32_t entry_num(Entry const *entries, Entry const *e) + { + return e - entries + 1; + } + + + /** + * Get used blocks + * + * \param header reference to GPT header + * \param entries pointer to memory containing GPT entries + * + * \return if a free entry is found a pointer to its memory + * is returned, otherwise a null pointer if returned + */ + uint64_t get_used_blocks(Header const &header, Entry const *entries) + { + if (!entries) { return ~0ull; } + + uint64_t result = 0; + + for (size_t i = 0; i < header.gpe_num; i++) { + Entry const * const e = (entries + i); + + if (!e->valid()) { continue; } + + uint64_t const blocks = e->lba_end - e->lba_start; + result += blocks; + } + + return result; + } + + + /** + * Check if given GPT header and entries are valid + * + * \param header reference to header + * \param entries pointer to memory containing GPT entries + * \param primary if true, header is assumed to be the primary + * and otherwise to be the backup header + * + * \return true if header and entries are valid, otherwise false + */ + bool valid(Header &header, Entry const *e, bool primary) + { + return header.valid(primary) + && header.entries_valid(e) ? true : false; + } + + /** + * Update CRC32 fields + * + * \param header reference to header + * \param entries pointer to memory containing GPT entries + */ + void update_crc32(Header &header, void const *entries) + { + size_t const len = header.gpe_size * header.gpe_num; + header.gpe_crc = Util::crc32(entries, len); + header.crc = 0; + header.crc = Util::crc32(&header, header.size); + } + +} /* namespace Gpt */ + +#endif /* _GPT_H_ */ diff --git a/repos/gems/src/app/gpt_write/main.cc b/repos/gems/src/app/gpt_write/main.cc new file mode 100644 index 0000000000..0f853e75ff --- /dev/null +++ b/repos/gems/src/app/gpt_write/main.cc @@ -0,0 +1,765 @@ +/* + * \brief GPT partitioning tool + * \author Josef Soentgen + * \date 2018-05-01 + */ + +/* + * Copyright (C) 2018 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU Affero General Public License version 3. + */ + +/* Genode includes */ +#include +#include +#include +#include + +/* local includes */ +#include +#include + + +namespace Gpt { + + struct Writer; +} + +struct Gpt::Writer +{ + struct Io_error : Genode::Exception { }; + struct Gpt_invalid : Genode::Exception { }; + + using sector_t = Block::sector_t; + + Block::Connection &_block; + Block::Session::Operations _block_ops { }; + Block::sector_t _block_count { 0 }; + size_t _block_size { 0 }; + + /* + * Blocks available is a crude approximation that _does not_ take + * alignment or unusable blocks because of the layout into account! + */ + uint64_t _blocks_avail { 0 }; + + /* track actions */ + bool _new_gpt { false }; + bool _new_pmbr { false }; + bool _new_geometry { false }; + + /* flags */ + bool _verbose { false }; + bool _update_geometry { false }; + bool _initialize { false }; + bool _wipe { false }; + bool _force_alignment { false }; + + Util::Number_of_bytes _entry_alignment { 4096u }; + + Genode::Xml_node *_config { nullptr }; + + void _handle_config(Genode::Xml_node config) + { + _verbose = config.attribute_value("verbose", false); + _initialize = config.attribute_value("initialize", false); + _wipe = config.attribute_value("wipe", false); + _force_alignment = config.attribute_value("force_align", false); + _update_geometry = config.attribute_value("update_geometry", false); + + { + Util::Size_string align = + config.attribute_value("align", Util::Size_string(4096u)); + ascii_to(align.string(), _entry_alignment); + } + + bool const commands = config.has_sub_node("commands"); + + if (_wipe && (_initialize || commands)) { + Genode::warning("will exit after wiping"); + } + + _config = &config; + } + + /************ + ** Tables ** + ************/ + + Protective_mbr::Header _pmbr { }; + + Gpt::Header _pgpt { }; + Gpt::Entry _pgpt_entries[MAX_ENTRIES] { }; + + Gpt::Header _bgpt { }; + Gpt::Entry _bgpt_entries[MAX_ENTRIES] { }; + + Block::sector_t _old_backup_hdr_lba { 0 }; + + /** + * Fill in-memory GPT header/entries and valid + * + * \param primary if set to true the primary GPT is checked, if false + * the backup header is checked + * + * \throw Io_error + */ + void _fill_and_check_header(bool primary) + { + using namespace Genode; + + Gpt::Header *hdr = primary ? &_pgpt : &_bgpt; + Gpt::Entry *entries = primary ? _pgpt_entries : _bgpt_entries; + + memset(hdr, 0, sizeof(Gpt::Header)); + memset(entries, 0, ENTRIES_SIZE); + + try { + /* header */ + { + Block::sector_t const lba = primary + ? 1 : _pgpt.backup_lba; + Util::Block_io io(_block, _block_size, lba, 1); + memcpy(hdr, io.addr(), sizeof(Gpt::Header)); + + if (!hdr->valid(primary)) { + error(primary ? "primary" : "backup", + " GPT header not valid"); + throw Gpt_invalid(); + } + } + + /* entries */ + { + uint32_t const max_entries = hdr->gpe_num > (uint32_t)MAX_ENTRIES + ? (uint32_t)MAX_ENTRIES : hdr->gpe_num; + Block::sector_t const lba = hdr->gpe_lba; + size_t const count = max_entries * hdr->gpe_size / _block_size; + + Util::Block_io io(_block, _block_size, lba, count); + size_t const len = count * _block_size; + memcpy(entries, io.addr(), len); + } + } catch (Util::Block_io::Io_error) { + error("could not read GPT header/entries"); + throw Io_error(); + } + + if (!Gpt::valid(*hdr, entries, primary)) { + error("GPT header and entries not valid"); + throw Gpt_invalid(); + } + } + + /** + * Wipe old backup GPT header from Block device + */ + bool _wipe_old_backup_header() + { + enum { BLOCK_SIZE = 4096u, }; + if (_block_size > BLOCK_SIZE) { + Genode::error("block size of ", _block_size, "not supported"); + return false; + } + + char zeros[BLOCK_SIZE] { }; + + size_t const blocks = 1 + (ENTRIES_SIZE / _block_size); + Block::sector_t const lba = _old_backup_hdr_lba - blocks; + + using namespace Util; + + try { + Block_io clear(_block, _block_size, lba, blocks, true, zeros, _block_size); + } catch (Block_io::Io_error) { return false; } + + return true; + } + + /** + * Wipe all tables from Block device + * + * Note: calling this method actually destroys old data! + */ + bool _wipe_tables() + { + enum { BLOCK_SIZE = 4096u, }; + if (_block_size > BLOCK_SIZE) { + Genode::error("block size of ", _block_size, "not supported"); + return false; + } + + char zeros[BLOCK_SIZE] { }; + + using namespace Util; + + try { + /* PMBR */ + Block_io clear_mbr(_block, _block_size, 0, 1, true, zeros, _block_size); + size_t const blocks = 1 + (Gpt::ENTRIES_SIZE / _block_size); + + /* PGPT */ + for (size_t i = 0; i < blocks; i++) { + Block_io clear_lba(_block, _block_size, 1 + i, 1, + true, zeros, _block_size); + } + + /* BGPT */ + for (size_t i = 0; i < blocks; i++) { + Block_io clear_lba(_block, _block_size, (_block_count - 1) - i, 1, + true, zeros, _block_size); + } + + return true; + } catch (Block_io::Io_error) { return false; } + } + + /** + * Setup protective MBR + * + * The first protective partition covers the whole Block device from the + * second block up to the 32bit boundary. + */ + void _setup_pmbr() + { + _pmbr.partitions[0].type = Protective_mbr::TYPE_PROTECTIVE; + _pmbr.partitions[0].lba = 1; + _pmbr.partitions[0].sectors = (uint32_t)(_block_count - 1); + + _new_pmbr = true; + } + + /** + * Initialize tables + * + * All tables, primary GPT and backup GPT as well as the protective MBR + * will be cleared in memory, a new disk GUID will be generated and the + * default values will be set. + */ + void _initialize_tables() + { + _setup_pmbr(); + + /* wipe PGPT and BGPT */ + Genode::memset(&_pgpt, 0, sizeof(_pgpt)); + Genode::memset(_pgpt_entries, 0, sizeof(_pgpt_entries)); + Genode::memset(&_bgpt, 0, sizeof(_bgpt)); + Genode::memset(_bgpt_entries, 0, sizeof(_bgpt_entries)); + + size_t const blocks = (size_t)ENTRIES_SIZE / _block_size; + + /* setup PGPT, BGPT will be synced later */ + Genode::memcpy(_pgpt.signature, "EFI PART", 8); + _pgpt.revision = Gpt::REVISION; + _pgpt.size = sizeof(Gpt::Header); + _pgpt.lba = Gpt::PGPT_LBA; + _pgpt.backup_lba = _block_count - 1; + _pgpt.part_lba_start = 2 + blocks; + _pgpt.part_lba_end = _block_count - (blocks + 2); + _pgpt.guid = Gpt::generate_uuid(); + _pgpt.gpe_lba = 2; + _pgpt.gpe_num = Gpt::MAX_ENTRIES; + _pgpt.gpe_size = sizeof(Gpt::Entry); + + _blocks_avail = _pgpt.part_lba_end - _pgpt.part_lba_start; + + _new_gpt = true; + } + + /** + * Synchronize backup header with changes in the primary header + */ + void _sync_backup_header() + { + size_t const len = _pgpt.gpe_num * _pgpt.gpe_size; + Genode::memcpy(_bgpt_entries, _pgpt_entries, len); + Genode::memcpy(&_bgpt, &_pgpt, sizeof(Gpt::Header)); + + _bgpt.lba = _pgpt.backup_lba; + _bgpt.backup_lba = _pgpt.lba; + _bgpt.gpe_lba = _pgpt.part_lba_end + 1; + } + + /** + * Write given header to Block device + * + * \param hdr reference to header + * \param entries pointer to GPT entry array + * \param primary flag to indicate which header to write, primary if true + * and backup when false + * + * \return true when successful, otherwise false + */ + bool _write_header(Gpt::Header const &hdr, Gpt::Entry const *entries, bool primary) + { + using namespace Util; + + try { + Block::sector_t const hdr_lba = primary + ? 1 : _pgpt.backup_lba; + Block_io hdr_io(_block, _block_size, hdr_lba, 1, true, + &hdr, sizeof(Gpt::Header)); + + size_t const len = hdr.gpe_num * hdr.gpe_size; + size_t const blocks = len / _block_size; + Block::sector_t const entries_lba = primary + ? hdr_lba + 1 + : _block_count - (blocks + 1); + Block_io entries_io(_block, _block_size, entries_lba, blocks, true, + entries, len); + } catch (Block_io::Io_error) { return false; } + + return true; + } + + + /** + * Write protective MBR to Block device + * + * \return true when successful, otherwise false + */ + bool _write_pmbr() + { + using namespace Util; + try { + Block_io pmbr(_block, _block_size, 0, 1, true, &_pmbr, sizeof(_pmbr)); + } catch (Block_io::Io_error) { + return false; + } + + return true; + } + + /** + * Commit in-memory changes to Block device + * + * \return true if successful, otherwise false + */ + bool _commit_changes() + { + /* only if in-memory structures changed we want to write */ + if (!_new_gpt && !_new_geometry) { return true; } + + /* remove stale backup header */ + if (_new_geometry) { _wipe_old_backup_header(); } + + _sync_backup_header(); + + Gpt::update_crc32(_pgpt, _pgpt_entries); + Gpt::update_crc32(_bgpt, _bgpt_entries); + + return _write_header(_pgpt, _pgpt_entries, true) + && _write_header(_bgpt, _bgpt_entries, false) + && (_new_pmbr ? _write_pmbr() : true) ? true : false; + } + + /** + * Update geometry information, i.e., fill whole Block device + */ + void _update_geometry_information() + { + if (_pgpt.backup_lba == _block_count - 1) { return; } + + _setup_pmbr(); + + _old_backup_hdr_lba = _pgpt.backup_lba; + + size_t const blocks = (size_t)ENTRIES_SIZE / _block_size; + + _pgpt.backup_lba = _block_count - 1; + _pgpt.part_lba_end = _block_count - (blocks + 2); + + _new_geometry = true; + } + + /************* + ** Actions ** + *************/ + + enum { + INVALID_ENTRY = ~0u, + INVALID_START = 0, + INVALID_SIZE = ~0ull, + }; + + /** + * Check if given entry number is in range + */ + uint32_t _check_range(Gpt::Header const &hdr, uint32_t const entry) + { + return (entry != (uint32_t)INVALID_ENTRY + && (entry == 0 || entry > hdr.gpe_num)) + ? (uint32_t)INVALID_ENTRY : entry; + } + + /** + * Lookup entry by number or label + * + * \return pointer to entry if found, otherwise a nullptr is returned + */ + Gpt::Entry *_lookup_entry(Genode::Xml_node node) + { + Gpt::Label const label = node.attribute_value("label", Gpt::Label()); + uint32_t const entry = + _check_range(_pgpt, + node.attribute_value("entry", (uint32_t)INVALID_ENTRY)); + + if (entry == INVALID_ENTRY && !label.valid()) { + Genode::error("cannot lookup entry, invalid arguments"); + return nullptr; + } + + if (entry != INVALID_ENTRY && label.valid()) { + Genode::warning("entry and label given, entry number will be used"); + } + + Gpt::Entry *e = nullptr; + + if (entry != INVALID_ENTRY) { + /* we start at 0 */ + e = &_pgpt_entries[entry - 1]; + } + + if (e == nullptr && label.valid()) { + e = Gpt::lookup_entry(_pgpt_entries, _pgpt.gpe_num, label); + } + + return e; + } + + /** + * Add new GPT entry + * + * \param node action node that contains the arguments + * + * \return true if entry was successfully added, otherwise false + */ + bool _do_add(Genode::Xml_node node) + { + bool const add = node.has_attribute("entry"); + Gpt::Label const label = node.attribute_value("label", Gpt::Label()); + Gpt::Type const type = node.attribute_value("type", Gpt::Type()); + uint64_t const size = Util::convert(node.attribute_value("size", + Util::Size_string())); + + if (_verbose) { + Genode::log(add ? "Add" : "Append", " entry '", + label.valid() ? label : "", "' size: ", size); + } + + if (!size) { + Genode::error("invalid size"); + return false; + } + + /* check if partition will fit */ + Block::sector_t length = Util::size_to_lba(_block_size, size); + + Block::sector_t lba = node.attribute_value("start", (Block::sector_t)INVALID_START); + + Gpt::Entry *e = nullptr; + if (add) { + uint32_t const entry = _check_range(_pgpt, + node.attribute_value("entry", (uint32_t)INVALID_ENTRY)); + + if ( entry == INVALID_ENTRY + || lba == INVALID_START + || size == INVALID_SIZE) { + Genode::error("cannot add entry, invalid arguments"); + return false; + } + + if (length > _blocks_avail) { + Genode::error("not enough sectors left (", _blocks_avail, + ") to satisfy request"); + return false; + } + + if (_pgpt_entries[entry].valid()) { + Genode::error("cannot add already existing entry ", entry); + return false; + } + + e = &_pgpt_entries[entry-1]; + + if (e->valid()) { + Genode::error("cannot add existing entry ", entry); + return false; + } + + } else { + /* assume append operation */ + Entry const *last = Gpt::find_last_valid(_pgpt, _pgpt_entries); + + e = last ? Gpt::find_next_free(_pgpt, _pgpt_entries, last) + : Gpt::find_free(_pgpt, _pgpt_entries); + + if (!e) { + Genode::error("cannot append partition, no free entry found"); + return false; + } + + if (lba != INVALID_START) { + Genode::warning("will ignore start LBA in append mode"); + } + + lba = last ? last->lba_end + 1 : _pgpt.part_lba_start; + if (lba == INVALID_START) { + Genode::error("cannot find start LBA"); + return false; + } + + /* limit length to available blocks */ + if (length == (~0ull / _block_size)) { + length = Gpt::gap_length(_pgpt, _pgpt_entries, last ? last : nullptr); + } + + /* account for alignment */ + uint64_t const align = _entry_alignment / _block_size; + if (length < align) { + Genode::error("cannot satisfy alignment constraints"); + return false; + } + } + + Gpt::Uuid const type_uuid = Gpt::type_to_uuid(type); + + e->type = type_uuid; + e->guid = Gpt::generate_uuid(); + e->lba_start = Util::align_start(_block_size, + _entry_alignment, lba); + uint64_t const lba_start = e->lba_start; + if (lba_start != lba) { + Genode::warning("start LBA ", lba, " set to ", lba_start, + " due to alignment constraints"); + } + e->lba_end = e->lba_start + (length - 1); + + if (label.valid()) { e->write_name(label); } + + _blocks_avail -= length; + return true; + } + + /** + * Delete existing GPT entry + * + * \param node action node that contains the arguments + * + * \return true if entry was successfully added, otherwise false + */ + bool _do_delete(Genode::Xml_node node) + { + Gpt::Entry *e = _lookup_entry(node); + if (!e) { return false; } + + if (_verbose) { + char tmp[48]; + e->read_name(tmp, sizeof(tmp)); + uint32_t const num = Gpt::entry_num(_pgpt_entries, e); + Genode::log("Delete entry ", num, " '", (char const*)tmp, "'"); + } + + _blocks_avail += e->length(); + + Genode::memset(e, 0, sizeof(Gpt::Entry)); + return true; + } + + /** + * Update existing GPT entry + * + * \param node action node that contains the arguments + * + * \return true if entry was successfully added, otherwise false + */ + bool _do_modify(Genode::Xml_node node) + { + using namespace Genode; + + Gpt::Entry *e = _lookup_entry(node); + + if (!e) { + Genode::error("could not lookup entry"); + return false; + } + + if (_verbose) { + char tmp[48]; + e->read_name(tmp, sizeof(tmp)); + uint32_t const num = Gpt::entry_num(_pgpt_entries, e); + Genode::log("Modify entry ", num, " '", (char const*)tmp, "'"); + } + + uint64_t const new_size = Util::convert(node.attribute_value("new_size", + Util::Size_string())); + if (new_size) { + bool const fill = new_size == ~0ull; + + uint64_t length = fill ? Gpt::gap_length(_pgpt, _pgpt_entries, e) + : Util::size_to_lba(_block_size, new_size); + + if (length == 0) { + error("cannot modify: ", fill ? "no space left" + : "invalid length"); + return false; + } + + uint64_t const old_length = e->length(); + uint64_t const new_length = length + (fill ? old_length : 0); + uint64_t const expand = new_length > old_length ? new_length - old_length : 0; + + if (expand && expand > _blocks_avail) { + Genode::error("cannot modify: new length ", expand, " too large"); + return false; + } + + if (!expand) { _blocks_avail += (old_length - new_length); } + else { _blocks_avail -= (new_length - old_length); } + + /* XXX overlapping check anyone? */ + e->lba_end = e->lba_start + new_length - 1; + } + + Gpt::Label const new_label = node.attribute_value("new_label", Gpt::Label()); + if (new_label.valid()) { e->write_name(new_label); } + + Gpt::Type const new_type = node.attribute_value("new_type", Gpt::Type()); + if (new_type.valid()) { + try { + Gpt::Uuid type_uuid = Gpt::type_to_uuid(new_type); + memcpy(&e->type, &type_uuid, sizeof(Gpt::Uuid)); + } catch (...) { + warning("could not update invalid type"); + } + } + + /* XXX should we generate a new GUID? */ + // e->guid = Gpt::generate_uuid(); + return true; + } + + /** + * Constructor + * + * \param block reference to underlying Block::Connection + * \param config copy of config Xml_node + * + * \throw Io_error + */ + Writer(Block::Connection &block, Genode::Xml_node config) : _block(block) + { + _block.info(&_block_count, &_block_size, &_block_ops); + + if (!_block_ops.supported(Block::Packet_descriptor::WRITE)) { + Genode::error("cannot write to Block session"); + throw Io_error(); + } + + /* initial config read in */ + _handle_config(config); + + /* + * In case of wiping, end here. + */ + if (_wipe) { return; } + + /* + * Read and validate the primary GPT header and its entries first + * and check the backup GPT header afterwards. + */ + if (!_initialize) { + _fill_and_check_header(true); + _fill_and_check_header(false); + + if (_update_geometry) { + Genode::log("Update geometry information"); + _update_geometry_information(); + } + + /* set available blocks */ + uint64_t const total = _pgpt.part_lba_end - _pgpt.part_lba_start; + _blocks_avail = total - Gpt::get_used_blocks(_pgpt, _pgpt_entries); + } + } + + /** + * Execute actions specified in config + * + * \return true if actions were executed successfully, otherwise + * false + */ + bool execute_actions() + { + if (_wipe) { return _wipe_tables(); } + + if (_initialize) { _initialize_tables(); } + + try { + Genode::Xml_node actions = _config->sub_node("actions"); + + actions.for_each_sub_node([&] (Genode::Xml_node node) { + bool result = false; + + if (node.has_type("add")) { + result = _do_add(node); + } else if (node.has_type("delete")) { + result = _do_delete(node); + } else if (node.has_type("modify")) { + result = _do_modify(node); + } else { + Genode::warning("skipping invalid action"); + return; + } + + if (!result) { throw -1; } + + _new_gpt |= result; + }); + } catch (...) { return false; } + + /* finally write changes to disk */ + return _commit_changes(); + } +}; + + +struct Main +{ + Genode::Env &_env; + Genode::Heap _heap { _env.ram(), _env.rm() }; + + Genode::Attached_rom_dataspace _config_rom { _env, "config" }; + + enum { TX_BUF_SIZE = 128u << 10, }; + Genode::Allocator_avl _block_alloc { &_heap }; + Block::Connection _block { _env, &_block_alloc, TX_BUF_SIZE }; + + Genode::Constructible _writer { }; + + Main(Genode::Env &env) : _env(env) + { + if (!_config_rom.valid()) { + Genode::error("invalid config"); + _env.parent().exit(1); + return; + } + + Util::init_random(_heap); + + try { + _writer.construct(_block, _config_rom.xml()); + } catch (...) { + _env.parent().exit(1); + return; + } + + bool const success = _writer->execute_actions(); + _env.parent().exit(success ? 0 : 1); + } +}; + + +void Component::construct(Genode::Env &env) { static Main main(env); } diff --git a/repos/gems/src/app/gpt_write/pmbr.h b/repos/gems/src/app/gpt_write/pmbr.h new file mode 100644 index 0000000000..84981dc67a --- /dev/null +++ b/repos/gems/src/app/gpt_write/pmbr.h @@ -0,0 +1,48 @@ +/* + * \brief Protective MBR partition table definitions + * \author Josef Soentgen + * \date 2018-05-03 + */ + +/* + * Copyright (C) 2018 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU Affero General Public License version 3. + */ + +#ifndef _PMBR_H_ +#define _PMBR_H_ + +/* Genode includes */ +#include + + +namespace Protective_mbr +{ + enum { TYPE_PROTECTIVE = 0xEE, }; + + /** + * Partition table entry format + */ + struct Partition + { + Genode::uint8_t unused[4] { }; + Genode::uint8_t type { }; + Genode::uint8_t unused2[3] { }; + Genode::uint32_t lba { }; + Genode::uint32_t sectors { }; + } __attribute__((packed)); + + /** + * Master boot record header + */ + struct Header + { + Genode::uint8_t unused[446] { }; + Partition partitions[4] { }; + Genode::uint16_t magic { 0xaa55 }; + } __attribute__((packed)); +} + +#endif /* _PMBR_H_ */ diff --git a/repos/gems/src/app/gpt_write/target.mk b/repos/gems/src/app/gpt_write/target.mk new file mode 100644 index 0000000000..21e6aa13df --- /dev/null +++ b/repos/gems/src/app/gpt_write/target.mk @@ -0,0 +1,4 @@ +TARGET := gpt_write +LIBS := base jitterentropy +SRC_CC := main.cc util.cc +INC_DIR := $(PRG_DIR) diff --git a/repos/gems/src/app/gpt_write/util.cc b/repos/gems/src/app/gpt_write/util.cc new file mode 100644 index 0000000000..1ed77d8bd0 --- /dev/null +++ b/repos/gems/src/app/gpt_write/util.cc @@ -0,0 +1,227 @@ +/* + * \brief Random utility + * \author Josef Soentgen + * \date 2018-05-05 + */ + +/* + * Copyright (C) 2018 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU Affero General Public License version 3. + */ + +/* Genode includes */ +#include + +/* local includes */ +#include + +/* library includes */ +#include + + +static struct rand_data *_ec_stir; + + +/** + * Initialize random back end + * + * \param alloc reference to allocator used internally + * + * \throw Init_random_failed + */ +void Util::init_random(Genode::Allocator &alloc) +{ + struct Init_random_failed : Genode::Exception { }; + + /* initialize private allocator backend */ + jitterentropy_init(alloc); + + int err = jent_entropy_init(); + if (err) { + Genode::error("jitterentropy library could not be initialized!"); + throw Init_random_failed(); + } + + /* use the default behaviour as specified in jitterentropy(3) */ + _ec_stir = jent_entropy_collector_alloc(0, 0); + if (!_ec_stir) { + Genode::error("jitterentropy could not allocate entropy collector!"); + throw Init_random_failed(); + } +} + + +/** + * Fill buffer with requested number of random bytes + * + * \param dest pointer to destination buffer + * \param len size of the destination buffer + * + * \throw Could_not_harvest_enough_randomness + */ +void Util::get_random(Genode::uint8_t *dest, Genode::size_t len) +{ + struct Could_not_harvest_enough_randomness : Genode::Exception { }; + + if (jent_read_entropy(_ec_stir, (char*)dest, len) < 0) { + throw Could_not_harvest_enough_randomness(); + } +} + + +/** + * Convert size string + * + * \param size reference to size string + * + * \return converted size in bytes + */ +Genode::uint64_t Util::convert(Util::Size_string const &size) +{ + if (!size.valid()) { return 0; } + + Genode::uint64_t length = 0; + enum { MAX_SIZE = ~0ull, }; + if (size == "max") { + length = MAX_SIZE; + } else { + Util::Number_of_bytes bytes; + Util::ascii_to(size.string(), bytes); + length = bytes; + } + + return length; +} + + +/** + * Align LBA at alignment boundary + * + * \param block_size used to calculate number of LBAs + * \param alignment alignment in number of bytes + * \param lba start LBA + * + * \return returns aligned start LBA + */ +Block::sector_t Util::align_start(Genode::size_t block_size, + Genode::size_t alignment, + Block::sector_t lba) +{ + struct Invalid_alignment : Genode::Exception { }; + if (alignment < block_size || !block_size || !alignment) { + throw Invalid_alignment(); + } + + Block::sector_t const blocks = alignment / block_size; + return Genode::align_addr(lba, Genode::log2(blocks)); +} + + +/** + * Convert size in bytes to number of LBAs + */ +Block::sector_t Util::size_to_lba(Genode::size_t block_size, Genode::uint64_t size) +{ + /* XXX align/round-down etc. */ + return size / block_size; +} + + +/** + * Simple bitwise CRC32 checking + * + * \param buf pointer to buffer containing data + * \param size length of buffer in bytes + * + * \return CRC32 checksum of data + */ +Genode::uint32_t Util::crc32(void const * const buf, Genode::size_t size) +{ + Genode::uint8_t const *p = static_cast(buf); + Genode::uint32_t crc = ~0U; + + while (size--) { + crc ^= *p++; + for (Genode::uint32_t j = 0; j < 8; j++) + crc = (-Genode::int32_t(crc & 1) & 0xedb88320) ^ (crc >> 1); + } + + return crc ^ ~0U; +} + + +/** + * Extract all valid ASCII characters from UTF-16LE buffer + * + * The function operates in a rather crude way and just tries to extract all + * characters < 128, even non-printable ones. + * + * \param dest pointer to destination buffer + * \param dest_len length of the destination buffer in bytes + * \param src pointer to source buffer + * \param dest_len length of the source buffer in 2 bytes + * + * \return length of resulting ASCII string + */ +Genode::size_t Util::extract_ascii(char *dest, size_t dest_len, + uint16_t const *src, size_t src_len) +{ + char *p = dest; + size_t j = 0; + size_t i = 0; + + for (size_t u = 0; u < src_len && src[u] != 0; u++) { + uint32_t utfchar = src[i++]; + + if ((utfchar & 0xf800) == 0xd800) { + unsigned int c = src[i]; + if ((utfchar & 0x400) != 0 || (c & 0xfc00) != 0xdc00) { + utfchar = 0xfffd; + } else { + i++; + } + } + + p[j] = (utfchar < 0x80) ? utfchar : '.'; + /* leave space for NUL */ + if (++j == dest_len - 1) { break; } + } + + p[j] = 0; + return j; +} + + +/** + * Convert printable ASCII characters to UTF-16LE + * + * The function operates in a rather crude way and will + * truncate the input string if it does not fit. + * + * \param dest pointer to destination buffer + * \param dest_len length of the destination buffer in 16bit words + * \param src pointer to source buffer + * \param dest_len length of the source buffer in 8bit words + * + * \return length of resulting UTF-16 string + */ +Genode::size_t Util::convert_ascii(uint16_t *dest, size_t dest_len, + uint8_t const *src, size_t src_len) +{ + Genode::memset(dest, 0, dest_len * sizeof(uint16_t)); + + if (src_len / sizeof(uint16_t) > dest_len) { + Genode::warning("input too long, will be truncated"); + src_len = dest_len; + } + + size_t i = 0; + for (; i < src_len; i++) { + uint16_t const utfchar = src[i]; + dest[i] = utfchar; + } + + return i; +} diff --git a/repos/gems/src/app/gpt_write/util.h b/repos/gems/src/app/gpt_write/util.h new file mode 100644 index 0000000000..480f050e41 --- /dev/null +++ b/repos/gems/src/app/gpt_write/util.h @@ -0,0 +1,169 @@ +/* + * \brief GPT utils + * \author Josef Soentgen + * \date 2018-05-01 + */ + +/* + * Copyright (C) 2018 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU Affero General Public License version 3. + */ + +#ifndef _UTIL_H_ +#define _UTIL_H_ + +/* Genode includes */ +#include +#include +#include +#include + + +namespace Util { + + using namespace Genode; + + void init_random(Genode::Allocator &); + void get_random(uint8_t *dest, size_t len); + + using Label = Genode::String<128>; + + using Size_string = Genode::String<64>; + uint64_t convert(Size_string const &); + + using sector_t = Block::sector_t; + + sector_t align_start(size_t, size_t, sector_t); + sector_t size_to_lba(size_t, uint64_t); + + struct Block_io; + + uint32_t crc32(void const * const, size_t); + size_t extract_ascii(char *, size_t, uint16_t const *, size_t); + size_t convert_ascii(uint16_t *, size_t, uint8_t const *, size_t); + + /* + * Wrapper to get suffixed uint64_t values + */ + class Number_of_bytes + { + uint64_t _n; + + public: + + /** + * Default constructor + */ + Number_of_bytes() : _n(0) { } + + /** + * Constructor, to be used implicitly via assignment operator + */ + Number_of_bytes(Genode::uint64_t n) : _n(n) { } + + /** + * Convert number of bytes to 'size_t' value + */ + operator Genode::uint64_t() const { return _n; } + + void print(Output &output) const + { + using Genode::print; + + enum { KB = 1024UL, MB = KB*1024UL, GB = MB*1024UL }; + + if (_n == 0) print(output, 0); + else if (_n % GB == 0) print(output, _n/GB, "G"); + else if (_n % MB == 0) print(output, _n/MB, "M"); + else if (_n % KB == 0) print(output, _n/KB, "K"); + else print(output, _n); + } + }; + + inline size_t ascii_to(const char *s, Number_of_bytes &result) + { + unsigned long res = 0; + + /* convert numeric part of string */ + int i = ascii_to_unsigned(s, res, 0); + + /* handle suffixes */ + if (i > 0) + switch (s[i]) { + case 'G': res *= 1024; + case 'M': res *= 1024; + case 'K': res *= 1024; i++; + default: break; + } + + result = res; + return i; + } +}; + + +/* + * Block_io wraps a Block::Connection for synchronous operations + */ +struct Util::Block_io +{ + struct Io_error : Genode::Exception { }; + + using Packet_descriptor = Block::Packet_descriptor; + + Block::Connection &_block; + Packet_descriptor _p; + + /** + * Constructor + * + * \param block reference to underlying Block::Connection + * \param block_size logical block size of the Block::Connection + * \param lba LBA to start access from + * \param count number of LBAs to access + * \param write set type of operation, write if true, read + * if false + * + * \throw Io_error + */ + Block_io(Block::Connection &block, size_t block_size, + sector_t lba, size_t count, + bool write = false, void const *data = nullptr, size_t len = 0) + : + _block(block), + _p(_block.tx()->alloc_packet(block_size * count), + write ? Packet_descriptor::WRITE + : Packet_descriptor::READ, lba, count) + { + if (write) { + if (data && len) { + void *p = addr(); + Genode::memcpy(p, data, len); + } else { + Genode::error("invalid data for write"); + throw Io_error(); + } + } + + _block.tx()->submit_packet(_p); + _p = _block.tx()->get_acked_packet(); + if (!_p.succeeded()) { + Genode::error("could not ", write ? "write" : "read", + " block-range [", _p.block_number(), ",", + _p.block_number() + count, ")"); + _block.tx()->release_packet(_p); + throw Io_error(); + } + } + + ~Block_io() { _block.tx()->release_packet(_p); } + + template T addr() + { + return reinterpret_cast(_block.tx()->packet_content(_p)); + } +}; + +#endif /* _UTIL_H_ */