פורמטים של קבצים - כותרת GIF#

בסוף הפרק הזה תוכלו לזהות קובץ בינארי לפי מספר הקסם שלו, לפענח את כותרת הפריסה הקבועה שלו, ולפענח בתי דגלים ארוזי־ביטים. הדוגמה היא פורמט תמונת GIF, אבל הגישה עוברת ישירות ל־PNG, WAV, BMP, ELF, ZIP, וכל פורמט אחר הבנוי סביב כותרת קבועה.

כותרת ה־GIF ו־logical screen descriptor#

קובץ GIF מתחיל בשני בלוקים בגודל קבוע, שלושה־עשר בתים בסך הכל. ממפרט GIF89a:

כותרת (6 בתים)#

offset

בתים

שדה

0

3

חתימה - "GIF"

3

3

גרסה - "87a" או "89a"

Logical Screen Descriptor (7 בתים)#

offset

בתים

שדה

סדר בתים

6

2

רוחב מסך לוגי

little-endian

8

2

גובה מסך לוגי

little-endian

10

1

דגלים ארוזים

-

11

1

אינדקס צבע רקע

-

12

1

יחס מימדי פיקסל

-

לבית הדגלים הארוז יש חמישה תת־שדות:

Bit:  7  6 5 4  3  2 1 0
     [GCT][CR  ][Sort][GCT size]
  • ביט 7 - דגל טבלת צבעים גלובלית

  • ביטים 6-4 - רזולוציית צבע (פחות 1)

  • ביט 3 - דגל מיון

  • ביטים 2-0 - גודל טבלת צבעים גלובלית (גודל בפועל = 2^(value+1))

כל מפרט פורמט נראה בערך כך: שדות ברוחב קבוע בסדר ידוע, עם בית ביטים־ארוזים מדי פעם.

קריאתו פנימה#

התבנית כותבת את עצמה שדה אחר שדה:

sub parse_gif_header {
    my ($fh) = @_;
    binmode $fh;

    my $header;
    read $fh, $header, 13
        or die "short read: $!";

    my ($sig, $ver, $w, $h, $flags, $bg, $aspect) =
        unpack "A3 A3 v v C C C", $header;

    die "not a GIF" unless $sig eq "GIF";
    die "unknown version: $ver" unless $ver eq "87a" or $ver eq "89a";

    return {
        version => $ver,
        width   => $w,
        height  => $h,
        flags   => $flags,
        bg      => $bg,
        aspect  => $aspect,
    };
}

הוראה אחר הוראה:

  • A3 A3 - שישה בתים של ASCII, מחולקים לחתימה וגרסה. A (לא a או Z) כי המפרט מבטיח בדיוק שלושה תווים ללא ריפוד - A מחזיר אותם ללא שינוי ובעל ההתנהגות הבטוחה יותר של הסרת רווחים ב־unpack.

  • v v - שני מספרים שלמים 16-ביט ללא־סימן little-endian. פורמט ה־GIF הוא little-endian לאורך כולו; v היא ההתאמה המדויקת.

  • C C C - שלושה בתים ללא־סימן.

רוחב כולל: 3 + 3 + 2 + 2 + 1 + 1 + 1 = 13 בתים. אנו קוראים בדיוק את המספר הזה לפני unpack; פחות מכך הוא שגיאה.

בדיקת מספר הקסם#

שתי השורות שלמעלה ממחישות תבנית שתחזרו עליה עבור כל פורמט קובץ:

die "not a GIF" unless $sig eq "GIF";
die "unknown version: $ver" unless $ver eq "87a" or $ver eq "89a";

בדקו את הקסם קודם, פענחו את היתר אחר כך. היפוך הסדר מאפשר לנתוני שטות לעבור לשלבים מאוחרים יותר שבהם הודעות השגיאה פחות ברורות. לפורמטים עם קסם ארוך יותר (PNG הוא 8 בתים, ELF הוא 4, הסנדוויץ׳ ״RIFF…WAVE״ של WAV הוא 12), אותו רעיון חל: בצעו unpack מספיק כדי לזהות את הקובץ, הפסיקו בחוסר התאמה, ואז בצעו unpack על היתר.

פענוח בית הדגלים הארוז#

pack ו־unpack אינם יכולים לפענח שדות ביטים תת־בתיים ישירות (למעט הוראות b / B בצורת מחרוזת, ראו את פרק המחרוזות). הגישה הפרגמטית היא היסטים ומסכות ב־Perl רגיל על הבית ש־unpack החזיר:

sub decode_gif_flags {
    my ($flags) = @_;
    return {
        gct_present => ($flags >> 7) & 0x01,
        color_res   => (($flags >> 4) & 0x07) + 1,
        sort_flag   => ($flags >> 3) & 0x01,
        gct_size    => 2 ** ((($flags) & 0x07) + 1),
    };
}

The pattern - ”unpack the byte, pick the bits apart with shifts“

  • is so common it deserves to be named. Anything smaller than a byte belongs outside the template.

ריצה מפותחת#

open my $fh, "<:raw", "sample.gif" or die $!;
my $hdr = parse_gif_header($fh);
my $gct = decode_gif_flags($hdr->{flags});

printf "GIF%s %dx%d\n", $hdr->{version}, $hdr->{width}, $hdr->{height};
printf "global colour table: %s (%d entries)\n",
       $gct->{gct_present} ? "yes" : "no",
       $gct->{gct_size};

על קובץ GIF89a אמיתי במידות 640×480, זה מדפיס:

GIF89a 640x480
global colour table: yes (256 entries)

כתיבת כותרת#

ההיפוך קצר עוד יותר - כל הוראה שפענחנו, אנו יכולים לארוז. פרט אחד: את בית הדגלים יש להרכיב מחדש מתת־השדות שלו תחילה.

sub encode_gif_flags {
    my (%f) = @_;
    return (($f{gct_present} & 0x01) << 7)
         | (((log2($f{color_res})) & 0x07) << 4)
         | (($f{sort_flag}   & 0x01) << 3)
         | (log2($f{gct_size}) - 1);
}

sub write_gif_header {
    my ($fh, $hdr, $gct) = @_;
    my $flags = encode_gif_flags(%$gct);
    my $bytes = pack "A3 A3 v v C C C",
        "GIF", $hdr->{version},
        $hdr->{width}, $hdr->{height},
        $flags, $hdr->{bg}, $hdr->{aspect};
    print {$fh} $bytes;
}

אותה תבנית בצד pack, אותם 13 בתים החוצה, מתאימים לקורא ה־GIF בצד השני. נסיעה חזור עם וקטור בדיקה היא הדרך המהירה ביותר לקבל ביטחון שהתבנית נכונה: בצעו unpack על קובץ קיים, ארזו מחדש את התוצאה, השוו בית־מול־בית.

התרחבות לפורמטים גדולים יותר#

הגישה היא קומפוזיציונית. לפורמט כמו WAV יש chunks רבים, לכל אחד כותרת משלו (id + size + data). אתם כותבים:

  • פונקציית עזר ברמה נמוכה ״קריאת כותרת chunk אחת״ - שתי קריאות unpack על 8 בתים: FourCC של 4 בתים וגודל little-endian של 4 בתים.

  • מפיץ המסתכל על ה־FourCC וקורא לפענחן לכל chunk.

  • פענחני chunk שמבצעים unpack על גוף ה־chunk לפי פריסת הבתים של אותו chunk עצמו.

כל שכבה היא unpack קטן מול תבנית זעירה. ברגע שרוחב שדה תלוי בערך שזה עתה קראתם, אתם עוצרים, מרכיבים את התבנית החדשה מאותו ערך, וקוראים שוב ל־unpack - אותה תבנית שלבית שהשתמשנו בה לשמות DNS.

מה למדתם#

  • מספר קסם קודם, גוף שני. היכשלו מהר בפורמט שגוי.

  • פורמטים של קבצים little-endian משתמשים ב־v / V, big-endian משתמשים ב־n / N; < / > מטפלים במקרים של עם־סימן או 64-ביט.

  • שדות תת־בתיים הם היסטים ב־Perl רגיל, לא הוראות תבנית.

  • פורמטים גדולים יותר מתפרקים לתבניות קטנות רבות. עבודת המתכנת היא לשמור על כל תבנית קטנה וברורה.

עם דפי העיון כחיפוש שלכם ושבעת הפרקים האלה תחת חגורתכם, הפורמט הבינארי הבא שיחצה את שולחנכם אמור להיקרא כבעיה פתורה. דפי pack ו־unpack מכילים את טבלת ההוראות המלאה כאשר תצטרכו לאשר רוחב או מתאם.