有一天,我有机会研究一下为一台Z80计算机编写的一个古老的辛克莱谱软件。这台机器的软件通常被保存到录音带中,现在人们使用抽头格式存储和交换频谱软件。该格式是原始磁带格式的直译,由一系列块组成(此处呈现为TAPBlock类)。典型的格式是有一个头(在这段代码中是TAPHeader),后面是一个代码块。1984年的巫师巢穴游戏就是一个例子。
这个软件的目的是简单地读取文件并将内容转储成某种结构化的、人类可读的形式。我最感兴趣的是查看十六进制字节字符串(这就是我家里的“人类可读性”!)这就是我在这里使用的格式。一切都按计划进行。下面显示了上述游戏文件的部分示例输出。
{ len = 19, flag = 0, {
00 77 6c 20 20 20 20 20 20 20 20 c7 3e 02 00 c7
3e
}, cksum = 25, calcsum = 25 }
{ type = 0, filename = "wl ", data_len = 16071, param1 = 2, param2 = 16071 }
{ len = 16073, flag = 255, {
00 01 0a 00 ec 31 30 0e 00 00 0a 00 00 0d 00 02
0d 00 fd 34 39 39 39 39 0e 00 00 4f c3 00 0d 00
...
33 36 37 36 0e 00 00 7c 5c 00 2c 31 39 39 0e 00
00 c7 00 00 3a fe 0d
}, cksum = 122, calcsum = 122 }
{ len = 19, flag = 0, {
03 77 6c 7a 20 20 20 20 20 20 20 38 07 50 c3 00
80
}, cksum = 110, calcsum = 110 }
{ type = 3, filename = "wlz ", data_len = 1848, param1 = 50000, param2 = 32768 }
{ len = 1850, flag = 255, {
00 00 00 00 18 3c 7e 7e 7e 5a 7e 3c 24 3c 3c 18
00 03 07 01 06 06 0f 07 e7 f7 ff ff ff 9f 0f 88
...
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
}, cksum = 209, calcsum = 209 }
{ len = 19, flag = 0, {
03 77 6c 62 20 20 20 20 20 20 20 6e 02 60 ea c3
80
}, cksum = 255, calcsum = 255 }
{ type = 3, filename = "wlb ", data_len = 622, param1 = 60000, param2 = 32963 }
{ len = 624, flag = 255, {
c0 38 00 3d 0e 72 0e 71 1c 43 1c 48 2f c0 43 1c
43 0e c0 72 0e 72 08 79 08 77 21 71 21 71 1c c0
...
bf 94 c0 bf 8f b8 8d b0 8f a9 8e c0 ad 8a bf 89
ff 50 35 0a 5a 38 64 5a a8 7a 5a c8 0c ff
}, cksum = 170, calcsum = 170 }
{ len = 19, flag = 0, {
03 77 6c 62 62 20 20 20 20 20 20 77 01 00 fa 80
80
}, cksum = 148, calcsum = 148 }
{ type = 3, filename = "wlbb ", data_len = 375, param1 = 64000, param2 = 32896 }
{ len = 377, flag = 255, {
d9 e1 d9 c9 d9 e5 d9 21 34 eb 7e fe ff 28 f1 fe
c0 20 0b 23 7e 32 c9 fa 23 7e 32 ca fa 23 7e 32
...
44 42 00 00 3c 40 3c 02 42 3c 00 00 fe 10 10 10
10 10 2a b2 5c ed 5b
}, cksum = 176, calcsum = 176 }我不太喜欢我创建TAPHeader构造函数的方式。我考虑过用static_cast<TAPHeader>(data.data())做一些更丑陋的事情,但说得越少越好。有更好的方法吗?关于如何改进代码的任何其他评论也是受欢迎的。我正在为此使用C++17,但是如果C++20提供了一些有吸引力的特性,在这里也是有用的,我也不会反对它。
#include <iostream>
#include <iterator>
#include <iomanip>
#include <fstream>
#include <functional>
#include <numeric>
#include <cstdint>
#include <vector>
#include <optional>
struct TAPHeader {
uint8_t type;
uint8_t filename[11]; // actually 10 bytes, but we add NUL terminator
uint16_t data_len;
uint16_t param1;
uint16_t param2;
explicit TAPHeader(const std::vector<uint8_t> v) {
auto it = v.begin();
type = *it++;
for (int i=0; i<10; ++i) {
filename[i] = *it++;
}
filename[10] = '\0'; // terminate filename string
data_len = *it++;
data_len |= *it++ << 8;
param1 = *it++;
param1 |= *it++ << 8;
param2 = *it++;
param2 |= *it++ << 8;
}
friend std::ostream& operator<<(std::ostream& out, const TAPHeader& hdr) {
return out
<< "{ type = " << (unsigned)hdr.type
<< ", filename = \"" << hdr.filename
<< "\", data_len = " << hdr.data_len
<< ", param1 = " << hdr.param1
<< ", param2 = " << hdr.param2
<< " }";
}
};
class TAPBlock {
uint16_t len; // little-endian
uint8_t flag; // 0 = header, 0xff = body
uint8_t cksum; // simple xor of data, excluding len
std::vector<uint8_t> data;
public:
uint8_t calcsum() const {
return std::accumulate(data.begin(), data.end(), flag, std::bit_xor<uint8_t>());
}
bool is_ok() const { return calcsum() == cksum; }
std::optional<TAPHeader> get_header() const {
if (flag == 0 && data.size() >= 17) {
return TAPHeader{data};
}
return {};
}
bool read(std::istream& in) {
uint8_t lo, hi;
lo = in.get();
hi = in.get();
len = (hi << 8) | lo;
flag = in.get();
data.clear();
data.reserve(len-2);
for (auto count{len-2}; count; --count) {
data.push_back(in.get());
}
cksum = in.get();
return in.good();
}
friend std::ostream& operator<<(std::ostream& out, const TAPBlock blk) {
out << "{ len = " << std::dec << blk.len << ", flag = " << (unsigned)blk.flag << ", {";
if (1 || blk.flag) {
int remaining{0};
for (auto n : blk.data) {
if (remaining == 0) {
remaining = 16;
out << "\n\t";
}
--remaining;
out << std::setw(2) << std::setfill('0') << std::hex << (unsigned)n << ' ';
}
out << "\n}, ";
} else {
}
return out << "cksum = " << std::dec << (unsigned)blk.cksum
<< ", calcsum = " << std::dec << (unsigned)blk.calcsum() << " }";
}
};
int main(int argc, char *argv[])
{
if (argc != 2) {
std::cout << "Usage: dumptap tapfilename\n";
return 1;
}
TAPBlock blk;
std::ifstream in{argv[1]};
while (blk.read(in)) {
std::cout << blk << '\n';
if (auto hdr = blk.get_header()) {
std::cout << *hdr << '\n';
}
}
}发布于 2020-12-18 07:52:00
class buffer {
std::vector<uint8_t> &data;
std::vector<uint8_t>::iterator pos;
public:
buffer(std::vector<uint8_t> &data)
: data(data)
, pos(data.cbegin())
{}
void read(char *dest, size_t len) {
std::copy_n(pos, dest, len);
pos += len;
}
void read(uint16_t &dest) {
dest = *pos++;
dest |= *pos++ << 8;
}
};使用此方法,ctor for TAPHeader的结果如下所示:
explicit TAPHeader(std::vector<uint8_t> const &data, TAPHeader &hdr) {
buffer b(data);
b.read(&type, sizeof(type));
b.read(filename, sizeof(filename)-1);
filename[10] = '\0';
b.read(data_len);
b.read(param1);
b.read(param2);
}我发现这足够简洁和简单,足以证明buffer的代码是正确的。
flag的TAPBlock成员,我可能会这样做:enum Flag : uint8_t { header = 0, body = 0xff } flag;那么get_header可能会出现这样的情况:
std::optional<TAPHeader> get_header() const {
const int min_header_size = 17;
if (flag == header && data.size() >= min_header_size) {
return TAPHeader{data};
}
return {};
}至少对我来说,这更清楚地表达了意图。
发布于 2020-12-17 20:48:29
TAPHeader构造函数和TAPBlock::operator<<的参数可以作为const引用传递,以避免复制传递的对象。
explicit TAPHeader(const std::vector<uint8_t> &v);
// ...
friend std::ostream& operator<<(std::ostream& out, const TAPBlock &blk);在TAPHeader构造函数中,在读取字节时计算两个字节长度值,但在TAPBlock::read中,将两个字节读入局部变量,然后使用这些字节计算长度。为了保持一致性,您应该对两者使用相同的样式。可以将read更改为启动:
len = in.get();
len |= in.get() << 8;那么您就不需要lo或hi变量了。
https://codereview.stackexchange.com/questions/253585
复制相似问题