cs50 pset4 恢复 - 为什么将整个文件写入内存无法通过 check50?

cs50 pset4 Recover - why does writing entire file to memory fail check50?

我正在研究 Recover,想知道我的方法是否存在根本性缺陷。该演练建议使用 fread() 以 512 字节块的形式遍历文件,查找 JPEG headers,并写入一个新文件 - 00X.jpg - 每次找到一个文件。我尝试的是使用 malloc() 创建一个任意大的临时缓冲区,使用 fread() 的 return 值来确定文件的大小,并将整个文件写入一个结构数组,其中包含两个数据类型; .B 表示 BYTE,用于存储文件,.header 表示 bool,表示每个 JPEG header 的开始位置。

我 运行 遇到两个问题。一是恢复的图像没有通过 check50,二是尝试从我的数组中一次写入多个字节会导致垃圾字节。这是我正在做的事情:

typedef uint8_t BYTE;
typedef struct
{
    BYTE B;
    bool header;
}
images;

这定义了数据类型 BYTE 和我的使用字节和布尔值的结构。

BYTE *tmp_buffer = malloc(4000000 * sizeof(BYTE));
int counter = fread(tmp_buffer, sizeof(BYTE), 4000000, file);
images buffer[counter];

这使用 malloc() 创建任意大的缓冲区,使用它和 fread 的 return 值来确定文件的字节大小,然后在内存中创建一个缓冲区以供使用。

for (int copy = 0; copy < counter; copy++)
{
    buffer[copy].header = false;
    buffer[copy].B = tmp_buffer[copy];
}
free(tmp_buffer);
fclose(file);
for (int check = 0; check < counter; check++)
{
    if (buffer[check].B == 0xff && buffer[check + 1].B == 0xd8 && buffer[check + 2].B == 0xff)
    {
        buffer[check].header = true;
    }
}

这会将每个字节从 'temporary' 缓冲区复制到永久缓冲区,将所有 header 设置为 false,然后关闭 file/frees 内存。之后,它找到 JPEG headers 并将它们设置为 true。从这里开始,我正在尝试看看什么有效:

int headers_counter = 1;
for (int header_location = 0; header_location < counter; header_location+= 512)
{
    if (buffer[header_location].header == true)
    {
        printf("%i. %i\n", headers_counter, header_location);
        headers_counter++;
    }
}

这会打印原始文件中每个 header 的编号和数组(不是字节)位置,并且它似乎可以工作。我说 'appears' 是因为以下代码确实恢复了图像:

int file_number = 0;
char file_name[8];
sprintf(file_name, "%03i.jpg", file_number);
FILE *img = fopen(file_name, "w");
for (int i = 1024; i < 115200; i++)
{
    fwrite(&buffer[i].B, sizeof(BYTE), 1, img);
}

这并不是为了解决整个问题,即恢复所有 50 张图像。它仅旨在通过从 000.jpg 的 header 的第一个字节开始并在 001.jpg 的 header 之前的最后一个字节结束来恢复 000.jpg(编辑:这是一个 hard-coded 示例,使用打印到上面终端的 header 位置,也是一个示例)。它似乎是这样做的,但它没有通过 check50 错误“恢复的图像不匹配。”

我的女朋友也在参加 class,她按照演练建议的方式实现了她的代码。我们以十六进制输出打开 000.jpg 文件并进行比较。我们没有遍历每个字节,但前几十行和最后几十行看起来是相同的,松弛 space 和所有。

我提到的另一件事是一次写入多个字节时的垃圾字符。如果我将最终循环更改为:

for (int i = 1024; i < 115200; i+= 512)
{
    fwrite(&buffer[i].B, sizeof(BYTE), 512, img);
}

然后效果更差,000.jpg 说这是一种无效或不受支持的图像格式。我查看了十六进制输出,这是我在比较原始循环的第一行和上面增加 512 的行时看到的结果:

ff d8 ff e0 00 10 4a 46 49 46 00 01 01 00 00 01
ff 01 d8 00 ff 00 e0 00 00 00 10 00 4a 00 46 00

每隔一个位置多一个字节!我在这里不知所措。在这一点上,更多的是了解这些行为。我敢肯定两者都有合乎逻辑的解释,但这让我发疯!我尝试做一个字节数组而不是添加了 bool 的结构,它做了同样的事情。

正如上面评论中所写,通过尝试使用结构并尝试存储每个 jpg 一次写出——你让事情变得比需要的更难。由于说明讨论了 FAT 文件系统(它位于从中获取图像的卡上),因此将每个文件的块存储在 512 字节的扇区中。要扫描卡,您只需要一个 512 字节的缓冲区来处理对其输出文件的读取和立即写入。不需要结构,也不需要动态分配内存。

接近读取的方法是从文件中读取每 512 个数据块。然后您需要检查块的前 4 个字节是否包含 jpg header。一个简短的测试函数可以写成:

#include <stdio.h>
#include <stdlib.h>

#define FNLEN 128       /* if you need a constant, #define one (or more) */
#define BLKSZ 512

/* check if first 4-bytes in buf match jpg header */
int chkjpgheader (const unsigned char *buf)
{
    return  buf[0] == 0xff && 
            buf[1] == 0xd8 && 
            buf[2] == 0xff && 
            buf[3] >> 4 == 0xe;
}

(您只需测试每个条件是否是 true return 条件的结果)

思考如何处理 jpg headers 扫描和读取文件,您可以在一个循环中完成所有操作,从输入中读取 512 个字节,并保持 jpg headers found -- 您也可以将其用作标志来指示 header 已找到。您将读取数据块,测试它是否是 header,如果是,如果不是第一个 header,关闭最后写入的 jpg 文件的输出文件,创建一个新文件名,打开文件(验证每个步骤),然后在循环检查每个 512 字节块的开头以查找 header 签名时写出数据。重复直到 运行 文件外。

您可以实现类似于:

/* find each jpg header and write contents to separate file_000x.jpg files.
 * returns the number of jpg files successfully recovered.
 */
int recoverjpgs (FILE *ifp)
{
    char jpgname[FNLEN] = "";       /* jpg output filename */
    unsigned char buf[BLKSZ];       /* read buffer */
    int jpgcnt = 0;                 /* found jpg header count*/
    size_t nbytes;                  /* no. of bytes read/written */
    FILE *fp = NULL;                /* FILE* pointer for jpg output */
    
    /* read until jpg header found */
    while ((nbytes = fread (buf, 1, BLKSZ, ifp)) > 0) {
        /* check if jpg header found */
        if (nbytes >= 4 && chkjpgheader(buf)) {
            /* if not 1st header, close current file */
            if (jpgcnt) {
                if (fclose (fp) == EOF) {   /* validate every close-after-write */
                    perror ("recoverjpg()-fclose");
                    return jpgcnt - 1;
                }
            }
            /* create output filename (e.g. file_0001.jpg) */
            sprintf (jpgname, "file_%04d.jpg", jpgcnt + 1);
            /* open next file/validate file open for writing */
            if ((fp = fopen (jpgname, "wb")) == NULL) {
                perror ("fopen-outfile");
                return jpgcnt;
            }
            jpgcnt += 1;    /* increment recovered jpg count */
        }
        /* if header found - write block in buf to output file */
        if (jpgcnt && fwrite (buf, 1, nbytes, fp) != nbytes) {
            perror ("recoverjpg()-fwrite");
            return jpgcnt - 1;
        }
    }
    /* if file opened, close final file */
    if (jpgcnt && fclose (fp) == EOF) {     /* validate every close-after-write */
        perror ("recoverjpg()-fclose");
        return jpgcnt - 1;
    }
    
    return jpgcnt;  /* return number of jpg files recovered */
}

(注意: jpgcnt 既用作计数器 用于控制第一个 fclose() 在 jpg 文件上发生并控制第一次写入第一个文件的时间。)

看看 returns。理解为什么 jpgcntjpgcnt - 1 在函数的不同位置被 returned。也明白为什么你总是检查 fclose() after-a-write 的 return 是否发生了。当最终数据刷新到文件并关闭文件时,可能会发生许多错误——最后一次检查最后一次写入不会捕获到这些错误。所以规则——总是验证 close-after-write。关闭输入文件时不需要检查。

这就是您所需要的。在 main() 中,您将打开输入文件并将打开的文件流简单地传递给保存 return 的 recoverjpgs() 函数,以了解成功恢复了多少 jpg 文件。它可以很简单:

int main (int argc, char **argv) {
    
    FILE *fp = NULL;            /* input file stream pointer */
    int jpgcnt = 0;             /* count of jpg files recovered */
    
    if (argc < 2 ) {    /* validate 1 argument given for filename */
        fprintf (stderr, "error: insufficient input,\n"
                         "usage: %s filename\n", argv[0]);
        return 1;
    }
    
    /* open file/validate file open for reading */
    if ((fp = fopen (argv[1], "rb")) == NULL) {
        perror ("fopen-argv[1]");
        return 1;
    }
    
    if ((jpgcnt = recoverjpgs(fp)))
        printf ("recovered %d .jpg files.\n", jpgcnt);
    else
        puts ("no jpg files recovered.");
        
    fclose (fp);
}

这是完整的程序,只需 copy/paste 将 3 个部分放在一起试试看。

例子Use/Output

$ ./bin/recover ~/doc/c/cs50/recover/card.raw
recovered 50 .jpg files.

(会在当前目录下创建file_0001.jpgfile_0050.jpg这50个文件--你可以欣赏到jgp文件中的气球、花朵、女孩等... .)

检查一下,如果您还有其他问题,请告诉我。


编辑Per-Comment关于分配存储每个文件写一次

即使您想在写入一次之前完全缓冲每个文件,使用具有单个 uint8_t(字节)和 bool 的结构来标记该结构是否为 header 字节意义不大。为什么?它使写例程变得一团糟。这将必须检查分配块中的每个结构,该块大到足以在写入时容纳整个 card.raw 文件以捕获 4-struct 序列,其中每个结构的 bool 标志设置为真 - 基本上复制所有在读取期间完成的测试以找到 header 字节并设置你的 bool 结构成员 true 开始。

如前所述,如果有数以亿计的文件,您可能希望扫描来自 card.raw 的输入流并将每个 jpg 的字节保存在缓冲区中,以便它们可以一次写入文件当进程继续时(你甚至可以 fork 写入一个单独的进程,这样如果你真的想调整东西,读取可以继续而不等待写入。

无论如何,方法都是一样的。如果你动态分配buf,你可以用每个jpg文件填充它,当找到下一个header时——将buf的当前内容写入下一个header 到你的文件,(将下一个 header 读到 buf 的开头)并重复直到你 运行 没有输入要检查。

您将在整个过程中重复使用为 buf 分配的存储空间,并且仅在当前文件需要的存储空间多于当前分配的存储空间时才进行扩展。 (所以 buf 的大小最终可以容纳一天结束时找到的最大的 jpg)。这最大限度地减少了分配,并且意味着在所有 50 个文件中唯一需要的 realloc 是遇到更大文件时所需的 realloc。如果接下来的 20 个文件都适合当前分配的缓冲区 - 不需要调整并且您不断填充 buf 不同的 jpg 文件内容,因为它们是从“取证图像”中恢复的(听起来很重要)

只增加了一个bufsz变量能够跟踪 buf 的当前分配大小和一个 total 变量来跟踪每个 jpg 文件中读取的总字节数。除此之外,您只是重新安排文件的写入位置,以便您等到一个完整的 jpg 被读入 buf,然后再打开这些字节并将这些字节写入文件,然后在文件写入后立即关闭文件(编写了一个简短的函数来处理这个问题——因为编写一个 generic-reusable 函数以将给定数量的字节从缓冲区写入给定名称的文件是有意义的。

完整的文件可以这样写

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>

#define FNLEN 128       /* if you need a constant, #define one (or more) */
#define BLKSZ 512
#define JPGSZ 1<<15     /* 32K initial allocation size */

/* write 'nbytes' from 'buf' to 'fname'. returns number of bytes
 * written on success, zero otherwise.
 */ 
size_t writebuf2file (const char *fname, void *buf, size_t nbytes)
{
    FILE *fp = NULL;    /* FILE* pointer for jpg output */
    
    /* open file/validate file open for writing */
    if ((fp = fopen (fname, "wb")) == NULL) {
        perror ("writebuf2file-fopen");
        return 0;
    }
    /* write buffer to file/validate bytes written */
    if (fwrite (buf, 1, nbytes, fp) != nbytes) {
        perror ("writebuf2file()-fwrite");
        return 0;
    }
    /* close file/validate every close-after-write */
    if (fclose (fp) == EOF) {
        perror ("writebuf2file-fclose");
        return 0;
    }
    
    return nbytes;
}

/* check if first 4-bytes in buf match jpg header */
int chkjpgheader (const unsigned char *buf)
{
    return  buf[0] == 0xff && 
            buf[1] == 0xd8 && 
            buf[2] == 0xff && 
            buf[3] >> 4 == 0xe;
}

/* find each jpg header and write contents to separate file_000x.jpg files.
 * returns the number of jpg files successfully recovered.
 */
int recoverjpgs (FILE *ifp)
{
    char jpgname[FNLEN] = "";                   /* jpg output filename */
    int jpgcnt = 0;                             /* found jpg header count*/
    size_t  nbytes,                             /* no. of bytes read/written */
            bufsz = JPGSZ,                      /* tracks current allocation of buf */
            total = 0;                          /* tracks total bytes in jpg file */
    uint8_t *buf = malloc (JPGSZ);              /* read buffer */
    
    if (!buf) { /* validate every allocation/reallocation */
        perror ("malloc-buf");
        return 0;
    }
    
    /* read until jpg header found */
    while ((nbytes = fread (buf + total, 1, BLKSZ, ifp)) > 0) {
        /* check if jpg header found */
        if (nbytes >= 4 && chkjpgheader(buf + total)) {
            /* if not 1st header, write buffer to file, reset for next file */
            if (jpgcnt) {
                /* create output filename (e.g. file_0001.jpg) */
                sprintf (jpgname, "file_%04d.jpg", jpgcnt);
                /* write current buf to file */
                if (!writebuf2file (jpgname, buf, total))
                    return jpgcnt - 1;
                /* move header block to start of buf */
                memmove (buf, buf + total, BLKSZ);
                total = 0;                  /* reset total for next file */
            }
            jpgcnt += 1;    /* increment recovered jpg count */
        }
        /* if header found - began accumulating blocks in buf */
        if (jpgcnt)
            total += nbytes;
        /* check if reallocation required before next read */
        if (total + BLKSZ > bufsz) {
            /* add a fixed 32K each time reallocaiton required
             * always realloc to a temporary pointer to prevent memory leak
             * on realloc failure.
             */
            void *tmp = realloc (buf, bufsz + (1 << 15));
            if (!tmp) {                     /* validate every reallocations */
                perror ("realloc-buf");
                return jpgcnt - 1;
            }
            buf = tmp;              /* assign reallocated block to buf */
            bufsz += 1 << 15;       /* update bufsz with new allocation size */
        }
    }
    /* write final buffer to file */
    if (jpgcnt) {
        /* create output filename (e.g. file_0001.jpg) */
        sprintf (jpgname, "file_%04d.jpg", jpgcnt);
        /* write current buf to file */
        if (!writebuf2file (jpgname, buf, total))
            return jpgcnt - 1;
    }
    
    free (buf);     /* free allocated memory */
    
    return jpgcnt;  /* return number of jpg files recovered */
}

int main (int argc, char **argv) {
    
    FILE *fp = NULL;            /* input file stream pointer */
    int jpgcnt = 0;             /* count of jpg files recovered */
    
    if (argc < 2 ) {    /* validate 1 argument given for filename */
        fprintf (stderr, "error: insufficient input,\n"
                         "usage: %s filename\n", argv[0]);
        return 1;
    }
    
    /* open file/validate file open for reading */
    if ((fp = fopen (argv[1], "rb")) == NULL) {
        perror ("fopen-argv[1]");
        return 1;
    }
    
    if ((jpgcnt = recoverjpgs(fp)))
        printf ("recovered %d .jpg files.\n", jpgcnt);
    else
        puts ("no jpg files recovered.");
        
    fclose (fp);
}

在您编写的任何动态分配内存的代码中,您对分配的任何内存块负有 2 责任:(1) 始终保留指向内存块的起始地址 因此,(2) 当不再需要它时可以释放

您必须使用内存错误检查程序来确保您不会尝试访问内存或写入 beyond/outside 您分配的块的边界,尝试读取或基于未初始化的条件跳转值,最后,确认您释放了所有已分配的内存。

对于Linux valgrind是正常的选择。每个平台都有类似的内存检查器。它们都很简单易用,只需运行你的程序通过它。

始终确认您已释放所有分配的内存并且没有内存错误。

慢慢来,仔细阅读代码。如果您还有其他问题,请告诉我。