From 853261364ed4415d83503d028f2dea1b5a8963df Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Wed, 23 May 2018 22:21:57 -0400 Subject: [PATCH 1/4] Generalised CRC generation and created specific subclasses for the CCITT CRC16 and CRC32. --- NumberTheory/CRC.hpp | 73 ++++++++++++++----- OSBindings/Mac/Clock SignalTests/CRCTests.mm | 28 +++++-- Storage/Disk/Controller/MFMDiskController.cpp | 5 +- Storage/Disk/Controller/MFMDiskController.hpp | 4 +- Storage/Disk/Encodings/MFM/Encoder.cpp | 1 - Storage/Disk/Encodings/MFM/Encoder.hpp | 2 +- Storage/Disk/Encodings/MFM/Shifter.cpp | 4 +- Storage/Disk/Encodings/MFM/Shifter.hpp | 8 +- Storage/Tape/Parsers/Acorn.cpp | 2 +- Storage/Tape/Parsers/Acorn.hpp | 2 +- 10 files changed, 89 insertions(+), 40 deletions(-) diff --git a/NumberTheory/CRC.hpp b/NumberTheory/CRC.hpp index a0fd70729..36e97df08 100644 --- a/NumberTheory/CRC.hpp +++ b/NumberTheory/CRC.hpp @@ -11,45 +11,84 @@ #include -namespace NumberTheory { +namespace CRC { -/*! Provides a class capable of accumulating a CRC16 from source data. */ -class CRC16 { +/*! Provides a class capable of generating a CRC from source data. */ +template class Generator { public: /*! Instantiates a CRC16 that will compute the CRC16 specified by the supplied @c polynomial and @c reset_value. */ - CRC16(uint16_t polynomial, uint16_t reset_value) : - reset_value_(reset_value), value_(reset_value) { + Generator(T polynomial): value_(reset_value) { + const T top_bit = T(~(T(~0) >> 1)); for(int c = 0; c < 256; c++) { - uint16_t shift_value = static_cast(c << 8); + T shift_value = static_cast(c << multibyte_shift); for(int b = 0; b < 8; b++) { - uint16_t exclusive_or = (shift_value&0x8000) ? polynomial : 0x0000; - shift_value = static_cast(shift_value << 1) ^ exclusive_or; + T exclusive_or = (shift_value&top_bit) ? polynomial : 0; + shift_value = static_cast(shift_value << 1) ^ exclusive_or; } - xor_table[c] = static_cast(shift_value); + xor_table[c] = shift_value; } } /// Resets the CRC to the reset value. - inline void reset() { value_ = reset_value_; } + void reset() { value_ = reset_value; } /// Updates the CRC to include @c byte. - inline void add(uint8_t byte) { - value_ = static_cast((value_ << 8) ^ xor_table[(value_ >> 8) ^ byte]); + void add(uint8_t byte) { + if(reflect_input) byte = reverse_byte(byte); + value_ = static_cast((value_ << 8) ^ xor_table[(value_ >> multibyte_shift) ^ byte]); } /// @returns The current value of the CRC. - inline uint16_t get_value() const { return value_; } + inline T get_value() const { + T result = value_^xor_output; + if(reflect_output) { + T reflected_output = 0; + for(std::size_t c = 0; c < sizeof(T); ++c) { + reflected_output = T(reflected_output << 8) | T(reverse_byte(result & 0xff)); + result >>= 8; + } + return reflected_output; + } + return result; + } /// Sets the current value of the CRC. - inline void set_value(uint16_t value) { value_ = value; } + inline void set_value(T value) { value_ = value; } private: - const uint16_t reset_value_; - uint16_t xor_table[256]; - uint16_t value_; + static constexpr int multibyte_shift = (sizeof(T) * 8) - 8; + T xor_table[256]; + T value_; + + constexpr uint8_t reverse_byte(uint8_t byte) const { + return + ((byte & 0x80) ? 0x01 : 0x00) | + ((byte & 0x40) ? 0x02 : 0x00) | + ((byte & 0x20) ? 0x04 : 0x00) | + ((byte & 0x10) ? 0x08 : 0x00) | + ((byte & 0x08) ? 0x10 : 0x00) | + ((byte & 0x04) ? 0x20 : 0x00) | + ((byte & 0x02) ? 0x40 : 0x00) | + ((byte & 0x01) ? 0x80 : 0x00); + } +}; + +/*! + Provides a generator of 16-bit CCITT CRCs, which amongst other uses are + those used by the FM and MFM disk encodings. +*/ +struct CCITT: public Generator { + CCITT() : Generator(0x1021) {} +}; + +/*! + Provides a generator of "standard 32-bit" CRCs. +*/ +struct CRC32: public Generator { + CRC32() : Generator(0x04c11db7) {} }; } diff --git a/OSBindings/Mac/Clock SignalTests/CRCTests.mm b/OSBindings/Mac/Clock SignalTests/CRCTests.mm index e836ea53f..32cbcd6af 100644 --- a/OSBindings/Mac/Clock SignalTests/CRCTests.mm +++ b/OSBindings/Mac/Clock SignalTests/CRCTests.mm @@ -8,18 +8,14 @@ #import #include "CRC.hpp" +#include @interface CRCTests : XCTestCase @end @implementation CRCTests -- (NumberTheory::CRC16)mfmCRCGenerator -{ - return NumberTheory::CRC16(0x1021, 0xffff); -} - -- (uint16_t)crcOfData:(uint8_t *)data length:(size_t)length generator:(NumberTheory::CRC16 &)generator +- (uint16_t)crcOfData:(uint8_t *)data length:(size_t)length generator:(CRC::CCITT &)generator { generator.reset(); for(size_t c = 0; c < length; c++) @@ -34,7 +30,7 @@ 0xa1, 0xa1, 0xa1, 0xfe, 0x00, 0x00, 0x01, 0x01 }; uint16_t crc = 0xfa0c; - NumberTheory::CRC16 crcGenerator = self.mfmCRCGenerator; + CRC::CCITT crcGenerator; uint16_t computedCRC = [self crcOfData:IDMark length:sizeof(IDMark) generator:crcGenerator]; XCTAssert(computedCRC == crc, @"Calculated CRC should have been %04x, was %04x", crc, computedCRC); @@ -63,10 +59,26 @@ 0x20, 0x20, 0x20, 0x20 }; uint16_t crc = 0x4de7; - NumberTheory::CRC16 crcGenerator = self.mfmCRCGenerator; + CRC::CCITT crcGenerator; uint16_t computedCRC = [self crcOfData:sectorData length:sizeof(sectorData) generator:crcGenerator]; XCTAssert(computedCRC == crc, @"Calculated CRC should have been %04x, was %04x", crc, computedCRC); } +- (void)testCCITTCheck { + CRC::CCITT crcGenerator; + for(auto c: std::string("123456789")) { + crcGenerator.add(c); + } + XCTAssertEqual(crcGenerator.get_value(), 0x29b1); +} + +- (void)testCRC32Check { + CRC::CRC32 crcGenerator; + for(auto c: std::string("123456789")) { + crcGenerator.add(c); + } + XCTAssertEqual(crcGenerator.get_value(), 0xcbf43926); +} + @end diff --git a/Storage/Disk/Controller/MFMDiskController.cpp b/Storage/Disk/Controller/MFMDiskController.cpp index e31d87246..aec23893b 100644 --- a/Storage/Disk/Controller/MFMDiskController.cpp +++ b/Storage/Disk/Controller/MFMDiskController.cpp @@ -14,8 +14,7 @@ using namespace Storage::Disk; MFMController::MFMController(Cycles clock_rate) : Storage::Disk::Controller(clock_rate), - shifter_(&crc_generator_), - crc_generator_(0x1021, 0xffff) { + shifter_(&crc_generator_) { } void MFMController::process_index_hole() { @@ -49,7 +48,7 @@ MFMController::Token MFMController::get_latest_token() { return latest_token_; } -NumberTheory::CRC16 &MFMController::get_crc_generator() { +CRC::CCITT &MFMController::get_crc_generator() { return crc_generator_; } diff --git a/Storage/Disk/Controller/MFMDiskController.hpp b/Storage/Disk/Controller/MFMDiskController.hpp index 607454fc9..da8270194 100644 --- a/Storage/Disk/Controller/MFMDiskController.hpp +++ b/Storage/Disk/Controller/MFMDiskController.hpp @@ -72,7 +72,7 @@ class MFMController: public Controller { Token get_latest_token(); /// @returns The controller's CRC generator. This is automatically fed during reading. - NumberTheory::CRC16 &get_crc_generator(); + CRC::CCITT &get_crc_generator(); // Events enum class Event: int { @@ -163,7 +163,7 @@ class MFMController: public Controller { int last_bit_; // CRC generator - NumberTheory::CRC16 crc_generator_; + CRC::CCITT crc_generator_; }; } diff --git a/Storage/Disk/Encodings/MFM/Encoder.cpp b/Storage/Disk/Encodings/MFM/Encoder.cpp index 27c7ebc82..84df0f7f3 100644 --- a/Storage/Disk/Encodings/MFM/Encoder.cpp +++ b/Storage/Disk/Encodings/MFM/Encoder.cpp @@ -187,7 +187,6 @@ template std::shared_ptr } Encoder::Encoder(std::vector &target) : - crc_generator_(0x1021, 0xffff), target_(target) {} void Encoder::output_short(uint16_t value) { diff --git a/Storage/Disk/Encodings/MFM/Encoder.hpp b/Storage/Disk/Encodings/MFM/Encoder.hpp index 4cfe6c6e0..e571e6b53 100644 --- a/Storage/Disk/Encodings/MFM/Encoder.hpp +++ b/Storage/Disk/Encodings/MFM/Encoder.hpp @@ -56,7 +56,7 @@ class Encoder { void add_crc(bool incorrectly); protected: - NumberTheory::CRC16 crc_generator_; + CRC::CCITT crc_generator_; private: std::vector &target_; diff --git a/Storage/Disk/Encodings/MFM/Shifter.cpp b/Storage/Disk/Encodings/MFM/Shifter.cpp index 5ffca5a50..d17098f85 100644 --- a/Storage/Disk/Encodings/MFM/Shifter.cpp +++ b/Storage/Disk/Encodings/MFM/Shifter.cpp @@ -11,8 +11,8 @@ using namespace Storage::Encodings::MFM; -Shifter::Shifter() : owned_crc_generator_(new NumberTheory::CRC16(0x1021, 0xffff)), crc_generator_(owned_crc_generator_.get()) {} -Shifter::Shifter(NumberTheory::CRC16 *crc_generator) : crc_generator_(crc_generator) {} +Shifter::Shifter() : owned_crc_generator_(new CRC::CCITT()), crc_generator_(owned_crc_generator_.get()) {} +Shifter::Shifter(CRC::CCITT *crc_generator) : crc_generator_(crc_generator) {} void Shifter::set_is_double_density(bool is_double_density) { is_double_density_ = is_double_density; diff --git a/Storage/Disk/Encodings/MFM/Shifter.hpp b/Storage/Disk/Encodings/MFM/Shifter.hpp index dbe5a6715..46bd252f6 100644 --- a/Storage/Disk/Encodings/MFM/Shifter.hpp +++ b/Storage/Disk/Encodings/MFM/Shifter.hpp @@ -44,7 +44,7 @@ namespace MFM { class Shifter { public: Shifter(); - Shifter(NumberTheory::CRC16 *crc_generator); + Shifter(CRC::CCITT *crc_generator); void set_is_double_density(bool is_double_density); void set_should_obey_syncs(bool should_obey_syncs); @@ -57,7 +57,7 @@ class Shifter { Token get_token() const { return token_; } - NumberTheory::CRC16 &get_crc_generator() { + CRC::CCITT &get_crc_generator() { return *crc_generator_; } @@ -72,8 +72,8 @@ class Shifter { // input configuration bool is_double_density_ = false; - std::unique_ptr owned_crc_generator_; - NumberTheory::CRC16 *crc_generator_; + std::unique_ptr owned_crc_generator_; + CRC::CCITT *crc_generator_; }; } diff --git a/Storage/Tape/Parsers/Acorn.cpp b/Storage/Tape/Parsers/Acorn.cpp index 92933b758..35a4dc79e 100644 --- a/Storage/Tape/Parsers/Acorn.cpp +++ b/Storage/Tape/Parsers/Acorn.cpp @@ -14,7 +14,7 @@ namespace { const int PLLClockRate = 1920000; } -Parser::Parser() : crc_(0x1021, 0x0000) { +Parser::Parser() { shifter_.set_delegate(this); } diff --git a/Storage/Tape/Parsers/Acorn.hpp b/Storage/Tape/Parsers/Acorn.hpp index 0ac156f1d..b6f870674 100644 --- a/Storage/Tape/Parsers/Acorn.hpp +++ b/Storage/Tape/Parsers/Acorn.hpp @@ -63,7 +63,7 @@ class Parser: public Storage::Tape::Parser, public Shifter::Delegate private: bool did_update_shifter(int new_value, int length); - NumberTheory::CRC16 crc_; + CRC::CCITT crc_; Shifter shifter_; }; From ca4bc92c336432967c66539980680a2b9676dab6 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Wed, 23 May 2018 22:22:17 -0400 Subject: [PATCH 2/4] Adds WOZ CRC checking. --- Storage/Disk/DiskImage/Formats/WOZ.cpp | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/Storage/Disk/DiskImage/Formats/WOZ.cpp b/Storage/Disk/DiskImage/Formats/WOZ.cpp index b5a79b3ec..3100f1a76 100644 --- a/Storage/Disk/DiskImage/Formats/WOZ.cpp +++ b/Storage/Disk/DiskImage/Formats/WOZ.cpp @@ -9,6 +9,7 @@ #include "WOZ.hpp" #include "../../Track/PCMTrack.hpp" +#include "../../../../NumberTheory/CRC.hpp" using namespace Storage::Disk; @@ -21,8 +22,20 @@ WOZ::WOZ(const std::string &file_name) : }; if(!file_.check_signature(signature, 8)) throw Error::InvalidFormat; - // TODO: check CRC32, instead of skipping it. - file_.seek(4, SEEK_CUR); + // Check the file's CRC32, instead of skipping it. + const uint32_t crc = file_.get32le(); + CRC::CRC32 crc_generator; + while(true) { + uint8_t next = file_.get8(); + if(file_.eof()) break; + crc_generator.add(next); + } + if(crc != crc_generator.get_value()) { + throw Error::InvalidFormat; + } + + // Retreat to the first byte after the CRC. + file_.seek(12, SEEK_SET); // Parse all chunks up front. bool has_tmap = false; From 8f78e5039ef89c098a4464b32c9d24d4729b74b0 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Thu, 24 May 2018 18:45:00 -0400 Subject: [PATCH 3/4] Factors out track seeking. --- Storage/Disk/DiskImage/Formats/WOZ.cpp | 27 +++++++++++++++++--------- Storage/Disk/DiskImage/Formats/WOZ.hpp | 9 +++++++++ 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/Storage/Disk/DiskImage/Formats/WOZ.cpp b/Storage/Disk/DiskImage/Formats/WOZ.cpp index 3100f1a76..eae1a84e9 100644 --- a/Storage/Disk/DiskImage/Formats/WOZ.cpp +++ b/Storage/Disk/DiskImage/Formats/WOZ.cpp @@ -90,22 +90,31 @@ int WOZ::get_head_count() { return is_3_5_disk_ ? 2 : 1; } -std::shared_ptr WOZ::get_track_at_position(Track::Address address) { +long WOZ::file_offset(Track::Address address) { // Calculate table position; if this track is defined to be unformatted, return no track. const int table_position = address.head * (is_3_5_disk_ ? 80 : 160) + (is_3_5_disk_ ? address.position.as_int() : address.position.as_quarter()); - if(track_map_[table_position] == 0xff) return nullptr; + if(track_map_[table_position] == 0xff) return NoSuchTrack; // Seek to the real track. - file_.seek(tracks_offset_ + track_map_[table_position] * 6656, SEEK_SET); + return tracks_offset_ + track_map_[table_position] * 6656; +} +std::shared_ptr WOZ::get_track_at_position(Track::Address address) { + long offset = file_offset(address); + if(offset == NoSuchTrack) return nullptr; + + // Seek to the real track. PCMSegment track_contents; - track_contents.data = file_.read(6646); - track_contents.data.resize(file_.get16le()); - track_contents.number_of_bits = file_.get16le(); + { + std::lock_guard lock_guard(file_.get_file_access_mutex()); + file_.seek(offset, SEEK_SET); - const uint16_t splice_point = file_.get16le(); - if(splice_point != 0xffff) { - // TODO: expand track from splice_point? + // In WOZ a track is up to 6646 bytes of data, followed by a two-byte record of the + // number of bytes that actually had data in them, then a two-byte count of the number + // of bits that were used. Other information follows but is not intended for emulation. + track_contents.data = file_.read(6646); + track_contents.data.resize(file_.get16le()); + track_contents.number_of_bits = file_.get16le(); } return std::shared_ptr(new PCMTrack(track_contents)); diff --git a/Storage/Disk/DiskImage/Formats/WOZ.hpp b/Storage/Disk/DiskImage/Formats/WOZ.hpp index 762ee3dce..03f642d35 100644 --- a/Storage/Disk/DiskImage/Formats/WOZ.hpp +++ b/Storage/Disk/DiskImage/Formats/WOZ.hpp @@ -34,6 +34,15 @@ class WOZ: public DiskImage { bool is_3_5_disk_ = false; uint8_t track_map_[160]; long tracks_offset_ = -1; + + /*! + Gets the in-file offset of a track. + + @returns The offset within the file of the track at @c address or @c NoSuchTrack if + the track does not exit. + */ + long file_offset(Track::Address address); + constexpr static long NoSuchTrack = 0; // This is definitely an offset a track can't lie at. }; } From 523ca3264bbd1625c6540211aaed5e1e6b7618f4 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Thu, 24 May 2018 21:44:31 -0400 Subject: [PATCH 4/4] Implements write support for WOZ files. --- NumberTheory/CRC.hpp | 14 ++++++ Storage/Disk/DiskImage/Formats/AppleDSK.cpp | 1 + Storage/Disk/DiskImage/Formats/WOZ.cpp | 51 +++++++++++++++++---- Storage/Disk/DiskImage/Formats/WOZ.hpp | 7 +++ Storage/FileHolder.hpp | 22 +++++++++ 5 files changed, 86 insertions(+), 9 deletions(-) diff --git a/NumberTheory/CRC.hpp b/NumberTheory/CRC.hpp index 36e97df08..b9f960647 100644 --- a/NumberTheory/CRC.hpp +++ b/NumberTheory/CRC.hpp @@ -10,6 +10,7 @@ #define CRC_hpp #include +#include namespace CRC { @@ -58,6 +59,19 @@ template &data) { + reset(); + for(const auto &byte: data) add(byte); + return get_value(); + } + private: static constexpr int multibyte_shift = (sizeof(T) * 8) - 8; T xor_table[256]; diff --git a/Storage/Disk/DiskImage/Formats/AppleDSK.cpp b/Storage/Disk/DiskImage/Formats/AppleDSK.cpp index 4933b74fc..50140f8c4 100644 --- a/Storage/Disk/DiskImage/Formats/AppleDSK.cpp +++ b/Storage/Disk/DiskImage/Formats/AppleDSK.cpp @@ -111,6 +111,7 @@ void AppleDSK::set_tracks(const std::map> tracks_by_address[pair.first] = std::move(track_contents); } + // Grab the file lock and write out the new tracks. std::lock_guard lock_guard(file_.get_file_access_mutex()); for(const auto &pair: tracks_by_address) { file_.seek(file_offset(pair.first), SEEK_SET); diff --git a/Storage/Disk/DiskImage/Formats/WOZ.cpp b/Storage/Disk/DiskImage/Formats/WOZ.cpp index eae1a84e9..d7401dc08 100644 --- a/Storage/Disk/DiskImage/Formats/WOZ.cpp +++ b/Storage/Disk/DiskImage/Formats/WOZ.cpp @@ -9,7 +9,7 @@ #include "WOZ.hpp" #include "../../Track/PCMTrack.hpp" -#include "../../../../NumberTheory/CRC.hpp" +#include "../../Track/TrackSerialiser.hpp" using namespace Storage::Disk; @@ -22,15 +22,15 @@ WOZ::WOZ(const std::string &file_name) : }; if(!file_.check_signature(signature, 8)) throw Error::InvalidFormat; - // Check the file's CRC32, instead of skipping it. + // Get the file's CRC32. const uint32_t crc = file_.get32le(); - CRC::CRC32 crc_generator; - while(true) { - uint8_t next = file_.get8(); - if(file_.eof()) break; - crc_generator.add(next); - } - if(crc != crc_generator.get_value()) { + + // Get the collection of all data that contributes to the CRC. + post_crc_contents_ = file_.read(static_cast(file_.stats().st_size - 12)); + + // Test the CRC. + const uint32_t computed_crc = crc_generator.compute_crc(post_crc_contents_); + if(crc != computed_crc) { throw Error::InvalidFormat; } @@ -119,3 +119,36 @@ std::shared_ptr WOZ::get_track_at_position(Track::Address address) { return std::shared_ptr(new PCMTrack(track_contents)); } + +void WOZ::set_tracks(const std::map> &tracks) { + for(const auto &pair: tracks) { + // Decode the track and store, patching into the post_crc_contents_. + auto segment = Storage::Disk::track_serialisation(*pair.second, Storage::Time(1, 50000)); + + auto offset = static_cast(file_offset(pair.first) - 12); + memcpy(&post_crc_contents_[offset - 12], segment.data.data(), segment.number_of_bits >> 3); + + // Write number of bytes and number of bits. + post_crc_contents_[offset + 6646] = static_cast(segment.number_of_bits >> 3); + post_crc_contents_[offset + 6647] = static_cast(segment.number_of_bits >> 11); + post_crc_contents_[offset + 6648] = static_cast(segment.number_of_bits); + post_crc_contents_[offset + 6649] = static_cast(segment.number_of_bits >> 8); + + // Set no splice information now provided, since it's been lost if ever it was known. + post_crc_contents_[offset + 6650] = 0xff; + post_crc_contents_[offset + 6651] = 0xff; + } + + // Calculate the new CRC. + const uint32_t crc = crc_generator.compute_crc(post_crc_contents_); + + // Grab the file lock, then write the CRC, then just dump the entire file buffer. + std::lock_guard lock_guard(file_.get_file_access_mutex()); + file_.seek(8, SEEK_SET); + file_.put_le(crc); + file_.write(post_crc_contents_); +} + +bool WOZ::get_is_read_only() { + return file_.get_is_known_read_only(); +} diff --git a/Storage/Disk/DiskImage/Formats/WOZ.hpp b/Storage/Disk/DiskImage/Formats/WOZ.hpp index 03f642d35..b6904e1af 100644 --- a/Storage/Disk/DiskImage/Formats/WOZ.hpp +++ b/Storage/Disk/DiskImage/Formats/WOZ.hpp @@ -11,6 +11,7 @@ #include "../DiskImage.hpp" #include "../../../FileHolder.hpp" +#include "../../../../NumberTheory/CRC.hpp" #include @@ -24,9 +25,12 @@ class WOZ: public DiskImage { public: WOZ(const std::string &file_name); + // Implemented to satisfy @c Disk. HeadPosition get_maximum_head_position() override; int get_head_count() override; std::shared_ptr get_track_at_position(Track::Address address) override; + void set_tracks(const std::map> &tracks) override; + bool get_is_read_only() override; private: Storage::FileHolder file_; @@ -35,6 +39,9 @@ class WOZ: public DiskImage { uint8_t track_map_[160]; long tracks_offset_ = -1; + std::vector post_crc_contents_; + CRC::CRC32 crc_generator; + /*! Gets the in-file offset of a track. diff --git a/Storage/FileHolder.hpp b/Storage/FileHolder.hpp index b096e4686..f09f8f4bc 100644 --- a/Storage/FileHolder.hpp +++ b/Storage/FileHolder.hpp @@ -52,6 +52,28 @@ class FileHolder final { */ uint32_t get32le(); + /*! + Writes @c value using successive @c put8s, in little endian order. + */ + template void put_le(T value) { + auto bytes = sizeof(T); + while(bytes--) { + put8(value&0xff); + value >>= 8; + } + } + + /*! + Writes @c value using successive @c put8s, in big endian order. + */ + template void put_be(T value) { + auto shift = sizeof(T) * 8; + while(shift) { + shift -= 8; + put8((value >> shift)&0xff); + } + } + /*! Performs @c get8 four times on @c file, casting each result to a @c uint32_t and returning the four assembled in big endian order.