یک API ممکن است چند بایتِ دادهٔ مهندسی را داخل یک رشتهٔ JSON بفرستد. نوشتن «این فیلد Base64 است» هنوز قرارداد کاملی نمی‌سازد: کدام الفبا، چه padding و چند بایت مجاز است؟ در این درس، یک فیلد کوچک با دادهٔ ساختگی را واقعاً می‌آزماییم. هدف، اعتبارسنجیِ یک نمایهٔ محدود است؛ سرور HTTP، فایل حسگر، دادهٔ شخصی یا اعتبارسنج کاملِ OpenAPI اجرا نمی‌شود.

۱. قرارداد فقط یک فیلدِ ازپیش‌خوانده‌شده است

دامنهٔ محدود یک فیلد با مرز نوع متن و محمولهٔ بایتی
تصویر مفهومی: دامنهٔ محدود یک فیلد با مرز نوع متن و محمولهٔ بایتی؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

ورودی تابع باید str دقیق باشد؛ bytes، null پایتون، عدد و bool پذیرفته نیستند. رشتهٔ خالی مجاز است و محمولهٔ خالی می‌دهد. متن باید ASCII باشد، حداکثر هشت نویسه داشته باشد و پس از رمزگشایی از پنج بایت بیشتر نشود. این عددها برای نمونهٔ آموزشی انتخاب شده‌اند؛ سقف عمومیِ اندازهٔ پیام یک سرویس یا ظرفیتِ دستگاه نیستند.

OpenAPI 3.2.1 در بخش دادهٔ دودویی، توصیف محتوای بایتی داخل قالب متنی را از بدنهٔ دودوییِ خام جدا می‌کند. برای توضیحِ این فیلد می‌توان type برابر string، contentEncoding برابر base64 و maxLength برابر هشت نوشت. اما جزئیاتِ نمایش یکتا و سقفِ پنج‌بایتی باید در قرارداد کاربرد و بررسیِ اجرایی نیز روشن باشند؛ یک توضیحِ مبهم، همان تابعِ آزموده‌شده نیست.

تابع، JSON را نمی‌خواند و کلید اضافه، توکن ورود یا نوع فایل را بررسی نمی‌کند. فرض می‌کنیم فراخواننده قبلاً فیلد مورد نظر را از ساختار بیرون آورده است. حتی وقتی بایت‌ها پذیرفته شوند، دربارهٔ معنای آن‌ها چیزی نمی‌دانیم؛ سه بایت می‌توانند نمونهٔ ساختگیِ دلخواه باشند. معتبر بودنِ نمایش، وجود یک تصویر، عددِ حسگرِ درست یا مجوز ارسال را ثابت نمی‌کند.

۲. الفبا و padding را با قرارداد صریح ببندیم

هم‌ترازی گروه‌های بایتی و جایگاه پایانیِ padding
تصویر مفهومی: هم‌ترازی گروه‌های بایتی و جایگاه پایانیِ padding؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

نمایهٔ ما Base64 استاندارد با دو نویسهٔ + و / است؛ گونهٔ URL-safe را قبول نمی‌کند. در RFC 4648، بخش‌های سه و چهار، تفاوت الفبا، گروه‌های بایتی و padding توضیح داده شده است. در این قرارداد، padding لازم باید حاضر باشد؛ فاصله و خط جدید نیز کنار گذاشته نمی‌شوند. این سیاست با خوانندهٔ تساهل‌گرِ متن ایمیل یکی نیست.

در انتهای محموله، یک بایت به دو نویسه و دو علامت مساوی و دو بایت به سه نویسه و یک مساوی تبدیل می‌شوند. سه بایت، چهار نویسهٔ بدون مساوی می‌دهند. برای نمونه، بایتِ حرف f نمایش Zg== دارد؛ دو بایت fo به Zm8= می‌رسند. نبودنِ padding لازم، مجوزِ حدس‌زدن و افزودن خودکار در تابع ما نیست.

بررسیِ چندبرابربودن طول متن از چهار، لازمِ این نمایه است ولی کافی نیست. علامت مساوی در میان داده یا دادهٔ تازه پس از padding می‌تواند همین طول را داشته باشد و باز نامعتبر باشد. بزرگ و کوچک بودن حروف نیز بخشی از داده است؛ lowercase کردنِ ورودی برای «یکسان‌سازی» قرارداد را می‌شکند. شکلِ Zg== و zg== هر دو نمایش یکتا، اما برای بایت‌های متفاوت‌اند.

۳. validate=True به‌تنهایی نمایش یکتا را ثابت نمی‌کند

تمایز رشتهٔ قابل رمزگشایی با نمایش یکتای همان بایت‌ها
تصویر مفهومی: تمایز رشتهٔ قابل رمزگشایی با نمایش یکتای همان بایت‌ها؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

مستندات base64 پایتون می‌گوید validate=True نویسه‌های خارج از الفبا را رد می‌کند. در اجرای ثبت‌شدهٔ ما، Zg== و Zh== هر دو با این گزینه به بایت f رسیدند. تفاوت در بیت‌های تکمیلیِ بدون نقش در محموله است؛ بخش canonical در RFC، صفر بودن این بیت‌ها را برای کدگذارِ سازگار شرح می‌دهد. پس موفقیتِ رمزگشایی با یکتاییِ متن یکسان نیست.

راه بررسیِ این نمایه روشن است: بایت‌های خروجی را دوباره با b64encode استاندارد کد می‌کنیم و باید دقیقاً با ASCII ورودی برابر شوند. خروجیِ بازکدگذاری برای هر دو نمونه Zg== است؛ بنابراین Zh== با خطای noncanonical رد می‌شود. برای دو بایت نیز Zm9= در اجرا به fo رسید، اما بازکدگذاری Zm8= بود و همان خطا رخ داد. این شاهدها واقعاً اجرا شده‌اند، نه رفتار فرضیِ همهٔ زبان‌ها و نسخه‌ها.

تبدیلِ نمایش، رمزنگاری یا احراز هویت نیست. همچنین این تابع، محمولهٔ نامعتبر را برای پذیرشِ پنهانی به شکل دلخواه اصلاح نمی‌کند؛ متن نامنطبق رد می‌شود. اگر کاربرد دیگری گونهٔ بی‌padding یا URL-safe بخواهد، باید قرارداد و آزمون خودش را داشته باشد. کنار هم قرار دادنِ چند سیاست متفاوت زیر یک نام، توضیحِ خطا را دشوار می‌کند.

۴. کد کامل با دو سقف و مرجع مستقلِ بیتی

سقف جداگانهٔ طول متن و تعداد بایتِ رمزگشایی‌شده
تصویر مفهومی: سقف جداگانهٔ طول متن و تعداد بایتِ رمزگشایی‌شده؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

پیش از رمزگشایی، سقف هشت نویسه بررسی می‌شود؛ سپس ASCII، طول گروه، دستور Base64، تعداد بایت و نمایش یکتا کنترل می‌شوند. ترتیبِ خطاها انتخابِ همین تابع است. متن fooba به Zm9vYmE= و foobar به Zm9vYmFy تبدیل می‌شود؛ هر دو هشت نویسه‌اند، ولی اولی پنج و دومی شش بایت است. پس سقف متن به‌تنهایی سقف پنج‌بایتی را ثابت نمی‌کند.

"""One bounded canonical standard-Base64 field, not an API/schema validator."""
import base64
import binascii
import json
import platform
from itertools import product

MAX_DECODED_BYTES = 5
MAX_ENCODED_CHARS = 4 * ((MAX_DECODED_BYTES + 2) // 3)
ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"


def decode_field(value):
    if type(value) is not str:
        raise ValueError("field_type")
    if len(value) > MAX_ENCODED_CHARS:
        raise ValueError("encoded_length")
    try:
        encoded = value.encode("ascii")
    except UnicodeEncodeError as error:
        raise ValueError("ascii") from error
    if len(encoded) % 4:
        raise ValueError("length_quantum")
    try:
        decoded = base64.b64decode(encoded, validate=True)
    except binascii.Error as error:
        raise ValueError("base64_syntax") from error
    if len(decoded) > MAX_DECODED_BYTES:
        raise ValueError("decoded_length")
    if base64.b64encode(decoded) != encoded:
        raise ValueError("noncanonical")
    return decoded


def reference_encode(payload):
    # Independent bit-string encoder; no standard-library Base64 encoder/decoder.
    bits = "".join(f"{byte:08b}" for byte in payload)
    bits += "0" * ((-len(bits)) % 6)
    symbols = "".join(ALPHABET[int(bits[start:start + 6], 2)]
                      for start in range(0, len(bits), 6))
    return symbols + "=" * ((-len(symbols)) % 4)


def error_code(value):
    try:
        decode_field(value)
    except ValueError as error:
        return str(error)
    return None


def run_example():
    checks = {}
    def check(name, condition):
        checks[name] = bool(condition)
        if not checks[name]:
            raise AssertionError(name)

    vectors = [(b"", ""), (b"f", "Zg=="), (b"fo", "Zm8="),
               (b"foo", "Zm9v"), (b"foob", "Zm9vYg=="),
               (b"fooba", "Zm9vYmE="), (b"foobar", "Zm9vYmFy")]
    check("seven_rfc_vectors_encoder_decoder_and_profile_scope", all(reference_encode(raw) == text and base64.b64encode(raw).decode("ascii") == text and base64.b64decode(text, validate=True) == raw and (decode_field(text) == raw if len(raw) <= 5 else error_code(text) == "decoded_length") for raw, text in vectors))
    check("hand_binary_payload_0001ff", decode_field("AAH/") == bytes((0, 1, 255)))
    check("empty_field_explicitly_allowed", decode_field("") == b"")
    check("standard_plus_slash_symbols_accepted", decode_field("+///") == bytes((251, 255, 255)))
    check("missing_padding_and_incomplete_quantum_rejected", all(error_code(x) == "length_quantum" for x in ("Zg", "Zm8", "Z")))
    check("excess_padding_rejected_by_recorded_runtime", error_code("AAAA====") == "base64_syntax" and error_code("Zg======") == "base64_syntax")
    check("interior_leading_or_trailing_payload_after_pad_rejected", all(error_code(x) == "base64_syntax" for x in ("=g==", "Z=g=", "Zg==Zg==")))
    check("whitespace_not_ignored", all(error_code(x) == "base64_syntax" for x in ("Zg= ", "Zg\n=", " Zg=")))
    check("non_ascii_rejected_before_decode", all(error_code(x) == "ascii" for x in ("Zگ==", "Zg==", "Zg=é")))
    check("non_string_types_rejected", all(error_code(x) == "field_type" for x in (b"Zg==", None, 1, True, ["Zg=="])))
    check("urlsafe_alphabet_not_standard_profile", error_code("-___") == "base64_syntax" and error_code("____") == "base64_syntax")
    check("one_byte_nonzero_pad_bits_validate_true_not_enough", base64.b64decode("Zh==", validate=True) == b"f" and error_code("Zh==") == "noncanonical" and decode_field("Zg==") == b"f")
    check("two_byte_nonzero_pad_bits_rejected", base64.b64decode("Zm9=", validate=True) == b"fo" and error_code("Zm9=") == "noncanonical" and decode_field("Zm8=") == b"fo")
    check("decoded_limit_independent_of_equal_encoded_length", len("Zm9vYmE=") == len("Zm9vYmFy") == MAX_ENCODED_CHARS and decode_field("Zm9vYmE=") == b"fooba" and error_code("Zm9vYmFy") == "decoded_length")
    check("encoded_cap_checked_before_decoding", error_code("A" * (MAX_ENCODED_CHARS + 1)) == "encoded_length")
    cases = 0
    for length in range(6):
        for values in product((0, 1, 254, 255), repeat=length):
            raw = bytes(values)
            expected_text = reference_encode(raw)
            if decode_field(expected_text) != raw or base64.b64encode(raw).decode("ascii") != expected_text:
                raise AssertionError("independent bit-string reference")
            cases += 1
    check("independent_bit_reference_1365_payloads_one_group", cases == 1365)
    aliases = 0
    for octet in range(256):
        canonical = reference_encode(bytes((octet,)))
        top_two = ALPHABET.index(canonical[1])
        for pad_bits in range(1, 16):
            alias = canonical[0] + ALPHABET[top_two + pad_bits] + "=="
            if base64.b64decode(alias, validate=True) != bytes((octet,)) or error_code(alias) != "noncanonical":
                raise AssertionError("nonzero pad-bit alias")
            aliases += 1
    check("all_one_byte_nonzero_pad_bit_aliases_3840_one_group", aliases == 3840)
    check("case_is_data_not_normalized", decode_field("Zg==") == b"f" and decode_field("zg==") == bytes((206,)) and decode_field("Zg==") != decode_field("zg=="))
    return {"python_version": platform.python_version(),
            "scope": "one already-parsed exact str field; standard RFC4648 alphabet, canonical required padding, no whitespace or URLsafe variant; not whole JSON/OpenAPI validation, encryption, authentication or network execution",
            "profile": {"max_decoded_bytes": MAX_DECODED_BYTES, "max_encoded_ascii_chars": MAX_ENCODED_CHARS, "empty_allowed": True},
            "hand_payload_hex": decode_field("AAH/").hex(),
            "pad_bits_witness": {"canonical": "Zg==", "noncanonical": "Zh==", "both_builtin_validate_true_hex": "66", "profile_alias_error": error_code("Zh==")},
            "same_encoded_length_boundary": {"encoded_length": 8, "five_byte_text": "Zm9vYmE=", "six_byte_text": "Zm9vYmFy", "six_byte_error": error_code("Zm9vYmFy")},
            "independent_reference_payloads_one_group": cases,
            "noncanonical_one_byte_aliases_one_group": aliases,
            "documented_schema_fragment": {"type": "string", "contentEncoding": "base64", "maxLength": MAX_ENCODED_CHARS, "description": "Application additionally enforces canonical encoding and decoded length<=5 bytes; annotations alone do not execute these checks."},
            "checks": checks}


if __name__ == "__main__":
    print(json.dumps(run_example(), ensure_ascii=False, indent=2))

reference_encode از رشتهٔ بیت‌ها و گروه‌های شش‌بیتی استفاده می‌کند و تابعِ کدگذار یا رمزگشای base64 را فراخوانی نمی‌کند. آن را با ۱۳۶۵ محمولهٔ طول صفر تا پنج از چهار مقدارِ بایت مقایسه کرده‌ایم؛ این مجموعه یک گروه است. هفت شاهدِ بخش ده RFC هم یک گروه‌اند؛ شاهد شش‌بایتیِ foobar از نظر استاندارد صحیح است، اما عمداً از نمایهٔ پنج‌بایتیِ ما رد می‌شود.

{
  "python_version": "3.14.5",
  "scope": "one already-parsed exact str field; standard RFC4648 alphabet, canonical required padding, no whitespace or URLsafe variant; not whole JSON/OpenAPI validation, encryption, authentication or network execution",
  "profile": {
    "max_decoded_bytes": 5,
    "max_encoded_ascii_chars": 8,
    "empty_allowed": true
  },
  "hand_payload_hex": "0001ff",
  "pad_bits_witness": {
    "canonical": "Zg==",
    "noncanonical": "Zh==",
    "both_builtin_validate_true_hex": "66",
    "profile_alias_error": "noncanonical"
  },
  "same_encoded_length_boundary": {
    "encoded_length": 8,
    "five_byte_text": "Zm9vYmE=",
    "six_byte_text": "Zm9vYmFy",
    "six_byte_error": "decoded_length"
  },
  "independent_reference_payloads_one_group": 1365,
  "noncanonical_one_byte_aliases_one_group": 3840,
  "documented_schema_fragment": {
    "type": "string",
    "contentEncoding": "base64",
    "maxLength": 8,
    "description": "Application additionally enforces canonical encoding and decoded length<=5 bytes; annotations alone do not execute these checks."
  },
  "checks": {
    "seven_rfc_vectors_encoder_decoder_and_profile_scope": true,
    "hand_binary_payload_0001ff": true,
    "empty_field_explicitly_allowed": true,
    "standard_plus_slash_symbols_accepted": true,
    "missing_padding_and_incomplete_quantum_rejected": true,
    "excess_padding_rejected_by_recorded_runtime": true,
    "interior_leading_or_trailing_payload_after_pad_rejected": true,
    "whitespace_not_ignored": true,
    "non_ascii_rejected_before_decode": true,
    "non_string_types_rejected": true,
    "urlsafe_alphabet_not_standard_profile": true,
    "one_byte_nonzero_pad_bits_validate_true_not_enough": true,
    "two_byte_nonzero_pad_bits_rejected": true,
    "decoded_limit_independent_of_equal_encoded_length": true,
    "encoded_cap_checked_before_decoding": true,
    "independent_bit_reference_1365_payloads_one_group": true,
    "all_one_byte_nonzero_pad_bit_aliases_3840_one_group": true,
    "case_is_data_not_normalized": true
  }
}

برای تمام ۲۵۶ مقدارِ یک‌بایتی، پانزده حالتِ غیرصفرِ بیت‌های اضافه ساخته شده است؛ ۳۸۴۰ حالت در یک گروه بررسی شدند. خوانندهٔ داخلیِ این اجرا همان بایت را داد و نمایهٔ ما همه را غیرcanonical دانست. هجده گروه همچنین نوع، ASCII، فاصله، الفبای دیگر، padding، مرز طول و پاسخ دستیِ AAH/ برابر بایت‌های 00،01،ff را می‌سنجند؛ اجرای دوباره، گروه تازه‌ای نیست.

متن JSON Schema 2020-12، بخش هشت، contentEncoding را annotation می‌داند؛ رمزگشایی و ارزیابی محتوا به‌طور پیش‌فرض اجرا نمی‌شود. OpenAPIِ خوانده‌شده نیز اعتبارسنجی توسعه‌یافته را جدا توضیح می‌دهد. این کد آن فاصله را برای یک فیلد پر می‌کند و هیچ موتور Schema اجرا نکرده است. تاریخ منابع از اجرا جداست: RFC اکتبر ۲۰۰۶، متن Schema ژوئن ۲۰۲۲، OAS سپتامبر ۲۰۲۶، مستندات Python 3.14.8 و اجرای 3.14.5؛ دسترسی در ۲۵ مهر ۱۴۰۵ است.

قرارداد JSON حسگر ساختار و عدد را می‌سنجد و آزمون CRC بستهٔ حسگر پرسشِ دیگری دربارهٔ مقدار بررسی دارد. Base64 جای هیچ‌کدام نیست. سقفِ این تابع نیز پس از دریافتِ شیء str اعمال می‌شود؛ بودجهٔ کاملِ دریافت HTTP، حافظهٔ فرایند یا زمان پردازشِ سرویس را اندازه نگرفته‌ایم.

۵. چهار پرسش با پاسخ کامل

توضیح قرارداد در کنار اعتبارسنج اجرایی و نمونه‌های مرزی
تصویر مفهومی: توضیح قرارداد در کنار اعتبارسنج اجرایی و نمونه‌های مرزی؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

پرسش اول: چرا Zh== با وجودِ رمزگشایی موفق رد می‌شود؟ پاسخ: همان بایتِ f را می‌دهد، ولی بازکدگذاری استاندارد Zg== است. برابریِ دقیقِ متن برقرار نیست و سیاستِ نمایش یکتای ما noncanonical می‌دهد.

پرسش دوم: maxLength برابر هشت، حداکثر پنج بایت را تضمین می‌کند؟ پاسخ: خیر؛ foobar شش بایت و نمایش آن هم هشت نویسه است. طولِ decoded باید جدا کنترل شود و در این نمونه decoded_length می‌گیریم.

پرسش سوم: Zg را با افزودن دو مساوی قبول کنیم؟ پاسخ: در این قرارداد خیر؛ طولِ گروه ناقص است و خطا می‌گیریم. سیاستِ پذیرشِ بدون padding انتخاب دیگری است و این تابع آن را پیاده نمی‌کند.

پرسش چهارم: وجود contentEncoding در مستندات، این هجده گروه را اجرا می‌کند؟ پاسخ: خیر؛ آن توصیفِ رمزگذاری است و annotation به‌تنهایی بررسیِ محتوای حاضر را اجرا نمی‌کند. باید ابزار و قواعد اجراییِ واقعی را مشخص و نتیجهٔ آن‌ها را مستقل ثبت کرد.

منابع و مطالعهٔ بیشتر

بررسی منابع: ۲۵ مهر ۱۴۰۵. تحلیل‌ها و پیشنهادهای اجرایی، جمع‌بندی نویسنده‌اند.