Bài 18.828 phút đọc
Tự thiết kế định dạng tệp
Sau bài này bạn sẽ làm được
- Thiết kế phần đầu tệp có số nhận dạng và số phiên bản
- Đóng gói từng trường tường minh thay vì ghi cả struct
- Chọn thứ tự byte cố định và viết hàm đọc ghi số nguyên
- Thêm mã kiểm tra để phát hiện tệp hỏng
Tệp dữ liệu của bạn sẽ sống lâu hơn phiên bản chương trình đã tạo ra nó. Bài này chỉ ra cách thiết kế một định dạng mà ba năm sau vẫn đọc được, trên một máy khác, bằng một chương trình đã đổi cấu trúc dữ liệu.
#Vì sao không ghi thẳng struct
fwrite(&sv, sizeof sv, 1, f);
/* Mot dong. Nhanh. Va no gan lien tep cua ban voi:
- trinh bien dich nao
- phien ban trinh bien dich nao
- kien truc 32 hay 64 bit
- thu tu byte cua may
- co bat -fpack-struct khong
- THU TU KHAI BAO cac truong trong struct
Doi bat ky dieu nao trong sau dieu tren la tep cu thanh rac. */| Cách | Dòng mã | Sống được bao lâu | Dùng khi |
|---|---|---|---|
| fwrite thẳng struct | 1 | Tới lần sửa struct đầu tiên | Tệp tạm, xóa ngay sau khi chạy xong |
| Có phần đầu ghi kích thước bản ghi | khoảng 20 | Phát hiện được lệch, nhưng không đọc được tệp cũ | Dự án cá nhân, dữ liệu tạo lại được |
| Đóng gói từng trường tường minh | khoảng 60 | Nhiều năm, qua nhiều phiên bản | Bất cứ khi nào người dùng thật sẽ giữ tệp |
| Dùng thư viện sẵn có | 0, nhưng thêm phụ thuộc | Rất lâu | Dự án lớn, và chấp nhận thêm thư viện |
#Phần đầu tệp
Số nhận dạng
Vài byte cố định ở đầu tệp, dùng để nhận ra loại tệp mà không cần nhìn phần mở rộng. Tệp PNG bắt đầu bằng
89 50 4E 47, tệp ZIP bằng 50 4B 03 04, tệp ELF bằng 7F 45 4C 46.dinh-dang.h
#ifndef DINH_DANG_H
#define DINH_DANG_H
#include <stdint.h>
#include <stdio.h>
#define SVDB_MAGIC "SVDB"
#define SVDB_PHIEN_BAN 1u
typedef struct {
char magic[4]; /* "SVDB", khong co ky tu ket thuc */
uint32_t phien_ban;
uint32_t so_ban_ghi;
uint32_t ma_kiem; /* tinh tren toan bo vung du lieu */
} PhanDau;
#define SVDB_CO_PHAN_DAU 16u /* 4 + 4 + 4 + 4, tinh tay chu KHONG dung sizeof */
int svdb_ghi_phan_dau(FILE *f, const PhanDau *pd);
int svdb_doc_phan_dau(FILE *f, PhanDau *pd);
#endifdinh-dang.c
#include <string.h>
#include "dinh-dang.h"
static int ghi_u32(FILE *f, uint32_t v) {
unsigned char b[4] = {
(unsigned char)(v >> 24), (unsigned char)(v >> 16),
(unsigned char)(v >> 8), (unsigned char)(v ),
};
return fwrite(b, 1, 4, f) == 4 ? 0 : -1;
}
static int doc_u32(FILE *f, uint32_t *ra) {
unsigned char b[4];
if (fread(b, 1, 4, f) != 4) return -1;
*ra = ((uint32_t)b[0] << 24) | ((uint32_t)b[1] << 16)
| ((uint32_t)b[2] << 8) | (uint32_t)b[3];
return 0;
}
int svdb_ghi_phan_dau(FILE *f, const PhanDau *pd) {
if (fwrite(SVDB_MAGIC, 1, 4, f) != 4) return -1;
if (ghi_u32(f, pd->phien_ban) != 0) return -1;
if (ghi_u32(f, pd->so_ban_ghi) != 0) return -1;
if (ghi_u32(f, pd->ma_kiem) != 0) return -1;
return 0;
}
int svdb_doc_phan_dau(FILE *f, PhanDau *pd) {
if (fread(pd->magic, 1, 4, f) != 4) return -1;
if (memcmp(pd->magic, SVDB_MAGIC, 4) != 0) {
fprintf(stderr, "Khong phai tep SVDB\n");
return -1;
}
if (doc_u32(f, &pd->phien_ban) != 0) return -1;
if (pd->phien_ban > SVDB_PHIEN_BAN) {
fprintf(stderr, "Tep phien ban %u, chuong trinh chi doc toi %u.\n"
"Hay cap nhat chuong trinh.\n",
pd->phien_ban, SVDB_PHIEN_BAN);
return -1;
}
if (doc_u32(f, &pd->so_ban_ghi) != 0) return -1;
if (doc_u32(f, &pd->ma_kiem) != 0) return -1;
return 0;
}#Đóng gói từng trường
Ghi và đọc một bản ghi
typedef struct {
char mssv[16];
char ho_ten[64];
uint32_t nam_sinh;
uint32_t diem_x100; /* 8.25 luu la 825, xem Bai 50.3 ve so nguyen */
} SinhVien;
#define SVDB_CO_BAN_GHI 88u /* 16 + 64 + 4 + 4, tinh tay */
int svdb_ghi_ban_ghi(FILE *f, const SinhVien *sv) {
if (fwrite(sv->mssv, 1, 16, f) != 16) return -1;
if (fwrite(sv->ho_ten, 1, 64, f) != 64) return -1;
if (ghi_u32(f, sv->nam_sinh) != 0) return -1;
if (ghi_u32(f, sv->diem_x100) != 0) return -1;
return 0;
}
int svdb_doc_ban_ghi(FILE *f, SinhVien *sv) {
if (fread(sv->mssv, 1, 16, f) != 16) return -1;
if (fread(sv->ho_ten, 1, 64, f) != 64) return -1;
if (doc_u32(f, &sv->nam_sinh) != 0) return -1;
if (doc_u32(f, &sv->diem_x100) != 0) return -1;
/* Ep ket thuc chuoi, phong khi tep bi sua tay hoac hong */
sv->mssv[15] = '\0';
sv->ho_ten[63] = '\0';
return 0;
}terminal
./thu-dong-goi
ghi/doc u32 khop nhau: dung het 2003 ghi kieu lon truoc: 00 00 07 D3
# Xem tệp bằng công cụ hex
xxd -g 1 sv.svdb | head -8
00000000: 53 56 44 42 00 00 00 01 00 00 00 03 e3 f0 e7 7e SVDB...........~ 00000010: 32 30 32 31 30 30 31 00 00 00 00 00 00 00 00 00 2021001......... 00000020: 4e 67 75 79 65 6e 20 56 61 6e 20 41 00 00 00 00 Nguyen Van A....
#Mã kiểm tra tính toàn vẹn
ma-kiem.c
#include <stdint.h>
/* FNV-1a 32 bit. Ngan, nhanh, du tot de phat hien hong ngau nhien. */
#define FNV_DAU 2166136261u
#define FNV_NHAN 16777619u
uint32_t ma_kiem(const void *p, size_t n, uint32_t h) {
const unsigned char *b = p;
for (size_t i = 0; i < n; ++i) {
h ^= b[i];
h *= FNV_NHAN;
}
return h;
}terminal
./thu-ma-kiem
FNV-1a("") = 2166136261
FNV-1a("a") = 3826002220
FNV-1a("SVDB") = 3824215934 (0xE3F0E77E)
FNV-1a("hello") = 1335831723 (0x4F9F2CAB)Ghép vào quy trình lưu và tải
/* Khi luu */
int luu(const char *ten, const SinhVien *ds, uint32_t n) {
FILE *f = fopen(ten, "wb");
if (!f) return -1;
PhanDau pd = { { 'S','V','D','B' }, SVDB_PHIEN_BAN, n, 0u };
/* Chua biet ma kiem, ghi tam 0 roi quay lai sua */
if (svdb_ghi_phan_dau(f, &pd) != 0) { fclose(f); return -1; }
uint32_t h = FNV_DAU;
for (uint32_t i = 0; i < n; ++i) {
if (svdb_ghi_ban_ghi(f, &ds[i]) != 0) { fclose(f); return -1; }
h = ma_kiem(&ds[i], sizeof ds[i], h);
}
/* Quay lai ghi de truong ma kiem, o vi tri 12 */
if (fseek(f, 12L, SEEK_SET) != 0) { fclose(f); return -1; }
if (ghi_u32(f, h) != 0) { fclose(f); return -1; }
return fclose(f) == 0 ? 0 : -1;
}
/* Khi tai */
int tai(const char *ten, SinhVien **ra_ds, uint32_t *ra_n) {
FILE *f = fopen(ten, "rb");
if (!f) return -1;
PhanDau pd;
if (svdb_doc_phan_dau(f, &pd) != 0) { fclose(f); return -1; }
SinhVien *ds = malloc(pd.so_ban_ghi * sizeof *ds);
if (!ds) { fclose(f); return -1; }
uint32_t h = FNV_DAU;
for (uint32_t i = 0; i < pd.so_ban_ghi; ++i) {
if (svdb_doc_ban_ghi(f, &ds[i]) != 0) {
fprintf(stderr, "%s: tep bi cat cut o ban ghi %u trong %u\n",
ten, i, pd.so_ban_ghi);
free(ds); fclose(f);
return -1;
}
h = ma_kiem(&ds[i], sizeof ds[i], h);
}
fclose(f);
if (h != pd.ma_kiem) {
fprintf(stderr, "%s: ma kiem khong khop, tep da hong\n", ten);
free(ds);
return -1;
}
*ra_ds = ds;
*ra_n = pd.so_ban_ghi;
return 0;
}#Đổi định dạng mà không mất tệp cũ
int svdb_doc_ban_ghi_theo_phien_ban(FILE *f, uint32_t pb, SinhVien *sv) {
/* Truong co tu phien ban 1 */
if (fread(sv->mssv, 1, 16, f) != 16) return -1;
if (fread(sv->ho_ten, 1, 64, f) != 64) return -1;
if (doc_u32(f, &sv->nam_sinh) != 0) return -1;
if (doc_u32(f, &sv->diem_x100) != 0) return -1;
/* Truong them tu phien ban 2 */
if (pb >= 2u) {
if (fread(sv->lop, 1, 16, f) != 16) return -1;
} else {
strcpy(sv->lop, "khong ro"); /* gia tri mac dinh hop ly */
}
/* Truong them tu phien ban 3 */
if (pb >= 3u) {
if (doc_u32(f, &sv->so_tin_chi) != 0) return -1;
} else {
sv->so_tin_chi = 0u;
}
sv->mssv[15] = '\0';
sv->ho_ten[63] = '\0';
sv->lop[15] = '\0';
return 0;
}Ba thay đổi phá vỡ tệp cũ
/* Doi kieu cua mot truong da co */
uint32_t nam_sinh; -> uint16_t nam_sinh;
/* Doi thu tu cac truong */
mssv, ho_ten, nam -> ho_ten, mssv, nam
/* Bo mot truong di */
/* Ca ba deu pha vo tep cu ma KHONG the doc lai duoc,
tru khi ban viet ham doc rieng cho tung phien ban. */Chỉ thêm trường ở cuối
/* Chi THEM truong, va chi them O CUOI ban ghi */
/* Muon bo mot truong: van doc no, roi bo qua gia tri.
Muon doi kieu: them truong MOI ben canh, giu truong cu. */
/* Voi truong khong con dung: */
uint32_t nam_sinh; /* KHONG DUNG NUA tu phien ban 4,
van doc de giu bo cuc, luon ghi 0 */Tự làm thử
- Cài
ghi_u32vàdoc_u32rồi kiểm tra chúng khớp nhau với tám giá trị biên. - Ghi một tệp có phần đầu đầy đủ, mở bằng
xxdvà tìm ra từng trường trong bản kết xuất hex. - Sửa một byte trong vùng dữ liệu bằng trình soạn hex và xác nhận chương trình báo mã kiểm tra không khớp.
- Đổi số phiên bản trong tệp lên 99 và xác nhận chương trình từ chối với thông báo rõ ràng.
- Thêm một trường mới ở cuối bản ghi, tăng phiên bản lên 2, và xác nhận chương trình mới vẫn đọc được tệp phiên bản 1.
Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.
Tóm tắt
- Ghi thẳng struct gắn tệp của bạn với trình biên dịch, kiến trúc và thứ tự khai báo trường.
- Phần đầu nên có số nhận dạng, số phiên bản, số bản ghi và mã kiểm tra. Kích thước phần đầu là hằng số tường minh, không phải
sizeof. - Đóng gói từng trường bằng hàm ghi và đọc riêng, và luôn ép ký tự kết thúc cho chuỗi đọc từ tệp.
- Mã kiểm tra phát hiện tệp hỏng do đĩa hay do truyền dở, nhưng không chống được người sửa cố ý.
- Chỉ thêm trường ở cuối bản ghi, và luôn tăng số phiên bản khi bố cục đổi.