123 lines
3.6 KiB
C
123 lines
3.6 KiB
C
/* reduce.c - 把 fuzz 找到的失败输入最小化到最短复现样例
|
|
*
|
|
* 判定 (oracle):同一段输入, "一次性喂" 与 "逐字节喂" 的渲染结果不一致.
|
|
* 用法: build/reduce build/md_fail.bin
|
|
*/
|
|
#include "markdown.h"
|
|
#include "util.h"
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
/* 注意:md_feed 只接受 NUL 结尾的字符串, 而最小化过程中的缓冲区
|
|
* 是"长度受限"的, 所以必须先做一份带结尾符的副本. */
|
|
static char *render(const char *src, size_t len, size_t chunk)
|
|
{
|
|
char *text = xmalloc(len + 1);
|
|
memcpy(text, src, len);
|
|
text[len] = '\0';
|
|
|
|
char *buf = NULL;
|
|
size_t size = 0;
|
|
FILE *f = open_memstream(&buf, &size);
|
|
if (!f) { free(text); return NULL; }
|
|
MdRenderer *r = md_new(1);
|
|
md_set_out(r, f);
|
|
if (chunk == 0) {
|
|
md_feed(r, text);
|
|
} else {
|
|
for (size_t i = 0; i < len; i += chunk) {
|
|
size_t n = len - i < chunk ? len - i : chunk;
|
|
char *t = xstrndup(text + i, n);
|
|
md_feed(r, t);
|
|
free(t);
|
|
}
|
|
}
|
|
md_finish(r);
|
|
md_free(r);
|
|
fclose(f);
|
|
free(text);
|
|
return buf;
|
|
}
|
|
|
|
/* 只要"一次性"与"逐字节"不一致就算失败 */
|
|
static int fails(const char *src, size_t len)
|
|
{
|
|
if (len == 0) return 0;
|
|
/* md_feed 以 NUL 结尾, 输入里不能有 NUL */
|
|
for (size_t i = 0; i < len; i++) if (src[i] == '\0') return 0;
|
|
char *a = render(src, len, 0);
|
|
char *b = render(src, len, 1);
|
|
int bad = (a && b && strcmp(a, b) != 0);
|
|
if (bad) {
|
|
/* 确认真的不是"空输出 vs 空输出"这种假阳性 */
|
|
if (strcmp(a, b) == 0) bad = 0;
|
|
}
|
|
free(a); free(b);
|
|
return bad;
|
|
}
|
|
|
|
static void show(const char *label, const char *buf, size_t len)
|
|
{
|
|
printf("%s (%zu 字节): ", label, len);
|
|
for (size_t i = 0; i < len; i++) {
|
|
unsigned char c = (unsigned char)buf[i];
|
|
if (c == '\n') printf("\\n");
|
|
else if (c == '\t') printf("\\t");
|
|
else if (c == 0x1b) printf("<ESC>");
|
|
else if (c >= 0x20 && c < 0x7f) putchar(c);
|
|
else printf("\\x%02x", c);
|
|
}
|
|
putchar('\n');
|
|
}
|
|
|
|
int main(int argc, char **argv)
|
|
{
|
|
if (argc < 2) { fputs("用法: reduce <失败输入文件>\n", stderr); return 2; }
|
|
size_t len = 0;
|
|
char *buf = read_entire_file(argv[1], &len);
|
|
if (!buf) { perror("读取失败"); return 2; }
|
|
if (!fails(buf, len)) { puts("该输入当前不复现失败"); free(buf); return 1; }
|
|
|
|
puts("== 原始失败输入 ==");
|
|
show(" ", buf, len);
|
|
{
|
|
char *a = render(buf, len, 0), *b = render(buf, len, 1);
|
|
show(" 一次性", a, strlen(a));
|
|
show(" 逐字节", b, strlen(b));
|
|
free(a); free(b);
|
|
}
|
|
|
|
/* 贪心块删除 */
|
|
size_t gran = len / 2;
|
|
while (gran >= 1) {
|
|
size_t i = 0;
|
|
while (i + gran <= len) {
|
|
char *save = xmalloc(gran);
|
|
memcpy(save, buf + i, gran);
|
|
memmove(buf + i, buf + i + gran, len - i - gran);
|
|
if (fails(buf, len - gran)) {
|
|
len -= gran;
|
|
} else {
|
|
memmove(buf + i + gran, buf + i, len - i - gran);
|
|
memcpy(buf + i, save, gran);
|
|
i += gran;
|
|
}
|
|
free(save);
|
|
}
|
|
if (gran == 1) break;
|
|
gran /= 2;
|
|
}
|
|
|
|
printf("\n== 最小复现样例 (%zu 字节) ==\n", len);
|
|
show(" ", buf, len);
|
|
{
|
|
char *a = render(buf, len, 0), *b = render(buf, len, 1);
|
|
show(" 一次性", a, strlen(a));
|
|
show(" 逐字节", b, strlen(b));
|
|
free(a); free(b);
|
|
}
|
|
free(buf);
|
|
return 0;
|
|
}
|