恢复

恢复的图片

要解决的问题

为了准备这道题,我们过去几天在校园里拍摄了许多照片,所有这些照片都以 JPEG 格式保存在数码相机的存储卡上。不幸的是,我们不知怎么把它们全删了!谢天谢地,在计算机世界中,”已删除”的意思往往不是”已删除”,而更像是”被遗忘”。尽管相机坚持说存储卡现在是空的,但我们相当确定这不完全是真的。确实,我们希望(呃,期望!)你能编写一个程序来为我们恢复这些照片!

在名为 recover 的文件夹中的 recover.c 文件中,编写一个程序来从存储卡中恢复 JPEG 图片。

Distribution Code

对于这个问题,你将扩展由 CS50 工作人员提供的代码功能。

下载分发代码

登录 cs50.dev,点击你的终端窗口,然后执行 cd 本身。你应该会发现你的终端窗口的提示符类似于下面所示:

$

接下来执行

wget https://cdn.cs50.net/2026/x/psets/4/recover.zip

以便将一个名为 recover.zip 的 ZIP 文件下载到你的 codespace 中。

然后执行

unzip recover.zip

来创建一个名为 recover 的文件夹。你不再需要这个 ZIP 文件,因此你可以执行

rm recover.zip

在提示符处回复”y”然后按回车键,删除你刚下载的 ZIP 文件。

现在输入

cd recover

然后按回车键,进入(即打开)该目录。你的提示符现在应类似于以下内容。

recover/ $

单独执行 ls,你应该会看到两个文件:recover.ccard.raw

背景

尽管 JPEG 比 BMP 复杂得多,但 JPEG 有”签名”——可以将其与其他文件格式区分开来的字节模式。具体来说,JPEG 的前三个字节是

0xff 0xd8 0xff

从左到右,从第一个字节到第三个字节。与此同时,第四个字节可以是 0xe00xe10xe20xe30xe40xe50xe60xe70xe80xe90xea0xeb0xec0xed0xee0xef 中的一个。换句话说,第四个字节的前四个比特是 1110

很有可能,如果你在已知存储照片的介质(例如我的存储卡)上发现这四个字节的模式,它们就标示着 JPEG 的开始。公平地说,你可能纯粹偶然地在某个磁盘上遇到这些模式,因此数据恢复并非一门精确科学。

幸运的是,数码相机倾向于将照片连续存储在存储卡上,即每张照片都紧接在前一张拍摄的照片之后存储。因此,一个 JPEG 的开始通常标志着另一个 JPEG 的结束。然而,数码相机通常使用 FAT 文件系统初始化存储卡,其”块大小”为 512 字节(B)。这意味着这些相机只以 512 B 为单位向这些存储卡写入数据。因此,一张 1 MB(即 1,048,576 B)的照片在存储卡上占用 1048576 ÷ 512 = 2048 个”块”。但一张小一个字节的照片(即 1,048,575 B)也是如此!磁盘上浪费的空间称为”松弛空间”。法医调查人员经常检查松弛空间以寻找可疑数据的残留。

所有这些细节的含义是,你(调查员)可能可以编写一个程序来遍历我的存储卡副本,寻找 JPEG 的签名。每当你找到签名时,你就可以打开一个新文件进行写入,并开始用我的存储卡中的字节填充该文件,只有当你遇到另一个签名时才关闭该文件。此外,你不必一次读取一个字节的存储卡数据,而可以一次读取 512 个字节到缓冲区中以提高效率。由于 FAT 的存在,你可以确信 JPEG 的签名将是”块对齐”的。也就是说,你只需要在一个块的前四个字节中查找这些签名。

当然,要认识到 JPEG 可以跨越连续的块。否则,没有 JPEG 可以大于 512 B。但 JPEG 的最后一个字节可能不会恰好落在块的末尾。回想一下松弛空间的可能性。但不用担心。因为这张存储卡在我开始拍照时是全新的,制造商很可能已经将其”清零”(即用 0 填充),在这种情况下,任何松弛空间都将被 0 填充。那些末尾的 0 出现在你恢复的 JPEG 中没有关系;它们仍然应该是可查看的。

现在,我只有一张存储卡,但你们有很多人!因此,我创建了这张卡的”法医镜像”,将其内容逐字节地存储在一个名为 card.raw 的文件中。为了不让你不必要地浪费时间遍历数百万个 0,我只镜像了存储卡的前几兆字节。但你最终应该会发现该镜像包含 50 个 JPEG。

规格说明

实现一个名为 recover 的程序,从法医镜像中恢复 JPEG 图片。

  • 在名为 recover 的目录中的 recover.c 文件中实现你的程序。
  • 你的程序应接受恰好一个命令行参数,即从中恢复 JPEG 的法医镜像的名称。
  • 如果程序未以恰好一个命令行参数执行,它应提醒用户正确的用法,并且 main 应返回 1
  • 如果法医镜像无法打开以进行读取,你的程序应告知用户这一情况,并且 main 应返回 1
  • 你生成的文件应各自命名为 ###.jpg,其中 ### 是一个三位十进制数字,从第一个图片的 000 开始递增计数。
  • 你的程序如果使用 malloc,则不得泄漏任何内存。

提示

在编写更多代码之前先写一些伪代码

如果不知道如何解决较大的问题,可以将其分解为你可能可以先解决的小问题。例如,这道题实际上只是少数几个问题:

  1. 接受一个命令行参数:存储卡的名称
  2. 打开存储卡
  3. 当存储卡中仍有数据可读时
    1. 从数据中创建 JPEG

让我们写一些伪代码作为注释,提醒你做这些事情:

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

int main(int argc, char *argv[])
{
    // Accept a single 命令行参数

    // Open the memory card

    // While there's still data left to read from the memory card

        // Create JPEGs from the data
}
将伪代码转换为代码

首先,考虑如何接受一个命令行参数。如果用户误用了程序,你应该告诉他们程序的正确用法。

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

int main(int argc, char *argv[])
{
    // Accept a single 命令行参数
    if (argc != 2)
    {
        printf("Usage: ./recover FILE\n");
        return 1;
    }

    // Open the memory card

    // While there's still data left to read from the memory card

        // Create JPEGs from the data
}

现在你已经检查了正确的用法,可以打开存储卡了。请记住,你可以使用 fopen 以编程方式打开 card.raw,如下所示。

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

int main(int argc, char *argv[])
{
    // Accept a single 命令行参数
    if (argc != 2)
    {
        printf("Usage: ./recover FILE\n");
        return 1;
    }

    // Open the memory card
    FILE *card = fopen(argv[1], "r");

    // While there's still data left to read from the memory card

        // Create JPEGs from the data
}

当然,你应该检查文件是否正确打开!如果没有,请告知用户并退出程序:这部分我们留给你来完成。

接下来,你的程序应该从你打开的存储卡中读取数据,直到没有数据可读为止。在此过程中,你的程序应该从 card.raw 中恢复每个 JPEG,将每个 JPEG 作为单独的文件存储在你的当前工作目录中。

首先考虑如何从头到尾读取 card.raw。回想一下,要从文件中读取数据,你需要将数据临时存储在一个”缓冲区”中。再回想一下,card.raw 以 512 字节的块存储数据。因此,你可能需要创建一个 512 字节的缓冲区,以便在顺序读取数据时存储数据块。一种方法是使用 stdint.h 中的 uint8_t 类型,该类型精确存储 8 比特(1 字节)。该类型之所以称为 uint8_t,是因为它存储一个需要 8 比特空间(即一个字节)的无符号/正/非负整数。

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

int main(int argc, char *argv[])
{
    // Accept a single 命令行参数
    if (argc != 2)
    {
        printf("Usage: ./recover FILE\n");
        return 1;
    }

    // Open the memory card
    FILE *card = fopen(argv[1], "r");

    // Create a buffer for a block of data
    uint8_t buffer[512];

    // While there's still data left to read from the memory card

        // Create JPEGs from the data
}

不过,在这里使用 512 作为”魔术数字”可能不是最好的做法。很有可能你可以进一步改进这个设计!

现在,考虑如何从存储卡中读取数据。根据其手册页面fread 返回它已读取的字节数,鉴于 card.raw 包含一定数量的 512 字节块,它应该返回 5120。为了从 card.raw 中读取每个块,在用 fopen 打开后,使用如下循环应该就足够了。

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

int main(int argc, char *argv[])
{
    // Accept a single 命令行参数
    if (argc != 2)
    {
        printf("Usage: ./recover FILE\n");
        return 1;
    }

    // Open the memory card
    FILE *card = fopen(argv[1], "r");

    // Create a buffer for a block of data
    uint8_t buffer[512];

    // While there's still data left to read from the memory card
    while (fread(buffer, 1, 512, card) == 512)
    {
        // Create JPEGs from the data

    }
}

这样,一旦 fread 返回 0(这实际上就是 false),你的循环就会结束。

最后,由你来决定如何在继续从 card.raw 读取数据的同时以编程方式创建 JPEG。为此,你可能会发现下面的讲解视频对你很有用。

请记住,你的程序应该通过将每个文件命名为 ###.jpg 来为其输出的文件编号,其中 ### 是从 000 开始递增的三位十进制数字。请熟悉 sprintf,并注意 sprintf 在内存中的某个位置存储一个格式化字符串。考虑到 JPEG 文件名的规定格式 ###.jpg,你应该为这个字符串分配多少字节?(别忘记 NUL 字符!)

要检查你的程序生成的 JPEG 是否正确,只需双击查看一下!如果每张照片看起来都完整无损,你的操作很可能成功了!

当然,请记得对用 fopen 打开的每个文件调用 fclose

保持工作目录整洁

你的代码初稿生成的 JPEG 很可能是不正确的。(如果你打开它们却看不到任何东西,它们很可能不正确!)执行以下命令来删除当前工作目录中的所有 JPEG。

rm *.jpg

如果你不想被提示确认每次删除,请改为执行下面的命令。

rm -f *.jpg

不过要小心使用 -f 开关,因为它会在不提示你的情况下”强制”删除。

讲解视频

如何测试

运行程序

./recover card.raw

正确性

check50 cs50/problems/2026/x/recover

代码风格

style50 recover.c

如何提交

在终端中执行以下命令提交你的作业,并按提示完成操作。

submit50 cs50/problems/2026/x/recover