lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

post.rs (19607B)


      1 use radroots_blossom::{BlobDescriptor, BlobUrl, ByteVerifiedDescriptor, MediaType, Sha256};
      2 use radroots_event::{
      3     media::AuthoredImage,
      4     post::{
      5         AuthoredAsk, AuthoredPhotoUpdate, AuthoredPostError, AuthoredPostImage, AuthoredUpdate,
      6         PostImageDimensions, RADROOTS_ASK_MARKER_TAG_VALUE, RADROOTS_POST_ALT_MAX_BYTES,
      7         RADROOTS_POST_CONTENT_MAX_BYTES, RADROOTS_POST_EVENT_WIRE_MAX_BYTES,
      8         RADROOTS_POST_IMETA_MAX_COUNT, RADROOTS_POST_TAG_ELEMENT_MAX_BYTES,
      9         RADROOTS_POST_TAG_TOTAL_MAX_BYTES, post_image_media_type_is_valid,
     10     },
     11 };
     12 use radroots_event_codec::encode::post::{
     13     authored_ask_to_wire_parts, authored_photo_update_to_wire_parts, authored_update_to_wire_parts,
     14 };
     15 
     16 #[test]
     17 fn authored_update_emits_only_bounded_nonblank_content() {
     18     let update = AuthoredUpdate::new("The first strawberries are ready.").unwrap();
     19     let wire = authored_update_to_wire_parts(&update);
     20 
     21     assert_eq!(wire.kind, 1);
     22     assert_eq!(wire.content, update.content());
     23     assert!(wire.tags.is_empty());
     24     assert_eq!(
     25         AuthoredUpdate::new(" \t").unwrap_err().code(),
     26         "post_content_missing"
     27     );
     28 }
     29 
     30 #[test]
     31 fn authored_update_enforces_the_utf8_byte_limit() {
     32     let maximum = "x".repeat(RADROOTS_POST_CONTENT_MAX_BYTES);
     33     assert!(AuthoredUpdate::new(maximum).is_ok());
     34 
     35     let over = "x".repeat(RADROOTS_POST_CONTENT_MAX_BYTES + 1);
     36     assert_eq!(
     37         AuthoredUpdate::new(over).unwrap_err(),
     38         AuthoredPostError::ContentTooLarge {
     39             max: RADROOTS_POST_CONTENT_MAX_BYTES,
     40             actual: RADROOTS_POST_CONTENT_MAX_BYTES + 1,
     41         }
     42     );
     43 }
     44 
     45 #[test]
     46 fn authored_photo_emits_exact_nip92_order_and_repeatable_fallbacks() {
     47     let image = authored_image(b"strawberries", "image/webp", "webp")
     48         .try_with_fallback(fallback_url(b"strawberries", "cache-one.example", "webp"))
     49         .unwrap()
     50         .try_with_fallback(fallback_url(b"strawberries", "cache-two.example", "webp"))
     51         .unwrap();
     52     let content = format!("Today's harvest {}", image.url());
     53     let photo = AuthoredPhotoUpdate::new(content.clone(), vec![image]).unwrap();
     54     let wire = authored_photo_update_to_wire_parts(&photo);
     55 
     56     assert_eq!(wire.kind, 1);
     57     assert_eq!(wire.content, content);
     58     assert_eq!(wire.tags.len(), 1);
     59     assert_eq!(wire.tags[0].as_slice(), photo.images()[0].imeta_tag());
     60     assert_eq!(
     61         wire.tags[0]
     62             .iter()
     63             .map(|field| field.split_once(' ').map_or(field.as_str(), |part| part.0))
     64             .collect::<Vec<_>>(),
     65         [
     66             "imeta", "url", "x", "m", "dim", "size", "alt", "fallback", "fallback"
     67         ]
     68     );
     69 }
     70 
     71 #[test]
     72 fn authored_ask_precedes_optional_media_with_one_exact_marker() {
     73     let image = authored_image(b"leaf", "image/jpeg", "jpg");
     74     let ask = AuthoredAsk::new(
     75         format!("Is this leaf healthy? {}", image.url()),
     76         vec![image],
     77     )
     78     .unwrap();
     79     let wire = authored_ask_to_wire_parts(&ask);
     80 
     81     assert_eq!(
     82         wire.tags[0],
     83         ["t".to_string(), RADROOTS_ASK_MARKER_TAG_VALUE.to_string()]
     84     );
     85     assert_eq!(wire.tags[1][0], "imeta");
     86 }
     87 
     88 #[test]
     89 fn authored_photo_requires_exact_disjoint_content_url_occurrences() {
     90     let image = authored_image(b"leaf", "image/jpeg", "jpg");
     91     assert_eq!(
     92         AuthoredPhotoUpdate::new("photo", Vec::new()).unwrap_err(),
     93         AuthoredPostError::ImageMissing
     94     );
     95     assert_eq!(
     96         AuthoredPhotoUpdate::new("photo", vec![image.clone()])
     97             .unwrap_err()
     98             .code(),
     99         "imeta_url_occurrence_count"
    100     );
    101     let content = image.url().to_string();
    102     assert_eq!(
    103         AuthoredPhotoUpdate::new(content, vec![image.clone(), image]).unwrap_err(),
    104         AuthoredPostError::DuplicateImageUrl
    105     );
    106 
    107     let repeated = authored_image(b"repeated", "image/jpeg", "jpg");
    108     assert_eq!(
    109         AuthoredPhotoUpdate::new(
    110             format!("{} then {}", repeated.url(), repeated.url()),
    111             vec![repeated],
    112         )
    113         .unwrap_err(),
    114         AuthoredPostError::ImageUrlOccurrenceCount {
    115             expected: 1,
    116             actual: 2,
    117         }
    118     );
    119 
    120     let unicode = authored_image(b"unicode", "image/webp", "webp");
    121     assert!(AuthoredPhotoUpdate::new(format!("収穫 {} 🍓", unicode.url()), vec![unicode]).is_ok());
    122 
    123     let bytes = b"prefix";
    124     let hash = Sha256::digest(bytes);
    125     let short_url = format!("https://media.example/{hash}.webp");
    126     let long_url = format!("{short_url}2");
    127     let short = authored_image_with_url(bytes, "image/webp", &short_url);
    128     let long = authored_image_with_url(bytes, "image/webp", &long_url);
    129     assert_eq!(
    130         AuthoredPhotoUpdate::new(long_url, vec![short, long]).unwrap_err(),
    131         AuthoredPostError::ImageUrlOverlap
    132     );
    133 }
    134 
    135 #[test]
    136 fn authored_photo_and_ask_enforce_the_imeta_count_limit() {
    137     let image = authored_image(b"leaf", "image/jpeg", "jpg");
    138     let images = vec![image.clone(); RADROOTS_POST_IMETA_MAX_COUNT + 1];
    139     let expected = AuthoredPostError::ImageCountExceeded {
    140         max: RADROOTS_POST_IMETA_MAX_COUNT,
    141         actual: RADROOTS_POST_IMETA_MAX_COUNT + 1,
    142     };
    143 
    144     assert_eq!(
    145         AuthoredPhotoUpdate::new(image.url(), images.clone()).unwrap_err(),
    146         expected
    147     );
    148     assert_eq!(AuthoredAsk::new("Question", images).unwrap_err(), expected);
    149 }
    150 
    151 #[test]
    152 fn authored_image_rejects_parameterized_mime_zero_dimensions_and_wrong_fallback_hash() {
    153     let parameterized = AuthoredImage::try_from(verified_descriptor(
    154         b"leaf",
    155         "image/webp; charset=binary",
    156         "webp",
    157     ))
    158     .unwrap();
    159     assert_eq!(
    160         AuthoredPostImage::new(
    161             parameterized,
    162             PostImageDimensions::new(1, 1).unwrap(),
    163             "Leaf",
    164         )
    165         .unwrap_err()
    166         .code(),
    167         "imeta_mime_invalid"
    168     );
    169     assert_eq!(
    170         PostImageDimensions::new(0, 1).unwrap_err().code(),
    171         "imeta_dimensions_invalid"
    172     );
    173 
    174     let image = authored_image(b"leaf", "image/webp", "webp");
    175     assert_eq!(
    176         image
    177             .try_with_fallback(fallback_url(b"other", "cache.example", "webp"))
    178             .unwrap_err()
    179             .code(),
    180         "imeta_fallback_hash_mismatch"
    181     );
    182 }
    183 
    184 #[test]
    185 fn post_image_mime_profile_uses_canonical_parameter_free_media_types() {
    186     assert!(post_image_media_type_is_valid("image/webp"));
    187     assert!(post_image_media_type_is_valid("image/svg+xml"));
    188     assert!(post_image_media_type_is_valid("image/vnd.microsoft.icon"));
    189     assert!(!post_image_media_type_is_valid("IMAGE/WEBP"));
    190     assert!(!post_image_media_type_is_valid("image/webp;quality=90"));
    191     assert!(!post_image_media_type_is_valid("text/plain"));
    192 }
    193 
    194 #[test]
    195 fn authored_image_rejects_zero_size_and_invalid_alt_text() {
    196     let empty = AuthoredImage::try_from(verified_descriptor(b"", "image/webp", "webp")).unwrap();
    197     assert_eq!(
    198         AuthoredPostImage::new(
    199             empty,
    200             PostImageDimensions::new(1, 1).unwrap(),
    201             "Empty image",
    202         )
    203         .unwrap_err(),
    204         AuthoredPostError::ImageSizeInvalid
    205     );
    206 
    207     let blank_alt =
    208         AuthoredImage::try_from(verified_descriptor(b"leaf", "image/webp", "webp")).unwrap();
    209     assert_eq!(
    210         AuthoredPostImage::new(blank_alt, PostImageDimensions::new(1, 1).unwrap(), " \t",)
    211             .unwrap_err(),
    212         AuthoredPostError::ImageAltInvalid
    213     );
    214 
    215     let maximum_alt = "a".repeat(RADROOTS_POST_ALT_MAX_BYTES);
    216     let maximum =
    217         AuthoredImage::try_from(verified_descriptor(b"maximum", "image/webp", "webp")).unwrap();
    218     assert!(
    219         AuthoredPostImage::new(
    220             maximum,
    221             PostImageDimensions::new(1, 1).unwrap(),
    222             maximum_alt,
    223         )
    224         .is_ok()
    225     );
    226 
    227     let oversized_alt = "a".repeat(RADROOTS_POST_ALT_MAX_BYTES + 1);
    228     let oversized =
    229         AuthoredImage::try_from(verified_descriptor(b"oversized", "image/webp", "webp")).unwrap();
    230     assert_eq!(
    231         AuthoredPostImage::new(
    232             oversized,
    233             PostImageDimensions::new(1, 1).unwrap(),
    234             oversized_alt,
    235         )
    236         .unwrap_err(),
    237         AuthoredPostError::ImageAltTooLarge {
    238             max: RADROOTS_POST_ALT_MAX_BYTES,
    239             actual: RADROOTS_POST_ALT_MAX_BYTES + 1,
    240         }
    241     );
    242 }
    243 
    244 #[test]
    245 fn authored_image_enforces_the_exact_tag_element_limit() {
    246     let maximum = AuthoredImage::try_from(verified_descriptor_with_url_element_bytes(
    247         b"maximum-url",
    248         RADROOTS_POST_TAG_ELEMENT_MAX_BYTES,
    249     ))
    250     .unwrap();
    251     let maximum = AuthoredPostImage::new(
    252         maximum,
    253         PostImageDimensions::new(1, 1).unwrap(),
    254         "Maximum URL",
    255     )
    256     .unwrap();
    257     assert_eq!(
    258         maximum.imeta_tag()[1].len(),
    259         RADROOTS_POST_TAG_ELEMENT_MAX_BYTES
    260     );
    261 
    262     let oversized = AuthoredImage::try_from(verified_descriptor_with_url_element_bytes(
    263         b"oversized-url",
    264         RADROOTS_POST_TAG_ELEMENT_MAX_BYTES + 1,
    265     ))
    266     .unwrap();
    267     assert_eq!(
    268         AuthoredPostImage::new(
    269             oversized,
    270             PostImageDimensions::new(1, 1).unwrap(),
    271             "Oversized URL",
    272         )
    273         .unwrap_err(),
    274         AuthoredPostError::TagElementTooLarge {
    275             max: RADROOTS_POST_TAG_ELEMENT_MAX_BYTES,
    276             actual: RADROOTS_POST_TAG_ELEMENT_MAX_BYTES + 1,
    277         }
    278     );
    279 
    280     let maximum_fallback = authored_image(b"maximum-fallback", "image/webp", "webp")
    281         .try_with_fallback(fallback_url_with_element_bytes(
    282             b"maximum-fallback",
    283             RADROOTS_POST_TAG_ELEMENT_MAX_BYTES,
    284         ))
    285         .unwrap();
    286     assert_eq!(
    287         maximum_fallback.imeta_tag().last().unwrap().len(),
    288         RADROOTS_POST_TAG_ELEMENT_MAX_BYTES
    289     );
    290     assert_eq!(
    291         authored_image(b"oversized-fallback", "image/webp", "webp")
    292             .try_with_fallback(fallback_url_with_element_bytes(
    293                 b"oversized-fallback",
    294                 RADROOTS_POST_TAG_ELEMENT_MAX_BYTES + 1,
    295             ))
    296             .unwrap_err(),
    297         AuthoredPostError::TagElementTooLarge {
    298             max: RADROOTS_POST_TAG_ELEMENT_MAX_BYTES,
    299             actual: RADROOTS_POST_TAG_ELEMENT_MAX_BYTES + 1,
    300         }
    301     );
    302 }
    303 
    304 #[test]
    305 fn authored_posts_enforce_the_exact_aggregate_tag_byte_limit() {
    306     let second = authored_image(b"second-image", "image/webp", "webp");
    307     let second_tag_bytes = tag_bytes(second.imeta_tag());
    308     let first_target = RADROOTS_POST_TAG_TOTAL_MAX_BYTES - second_tag_bytes;
    309     let first = fill_image_tag_bytes(
    310         authored_image(b"first-image", "image/webp", "webp"),
    311         b"first-image",
    312         first_target,
    313     );
    314     let content = format!("{} {}", first.url(), second.url());
    315     let photo =
    316         AuthoredPhotoUpdate::new(content.clone(), vec![first.clone(), second.clone()]).unwrap();
    317     let wire = authored_photo_update_to_wire_parts(&photo);
    318     assert_eq!(
    319         wire.tags.iter().map(|tag| tag_bytes(tag)).sum::<usize>(),
    320         RADROOTS_POST_TAG_TOTAL_MAX_BYTES
    321     );
    322 
    323     let first_over = fill_image_tag_bytes(
    324         authored_image(b"first-over", "image/webp", "webp"),
    325         b"first-over",
    326         first_target + 1,
    327     );
    328     let over_content = format!("{} {}", first_over.url(), second.url());
    329     assert_eq!(
    330         AuthoredPhotoUpdate::new(over_content, vec![first_over, second]).unwrap_err(),
    331         AuthoredPostError::TagBytesExceeded {
    332             max: RADROOTS_POST_TAG_TOTAL_MAX_BYTES,
    333             actual: RADROOTS_POST_TAG_TOTAL_MAX_BYTES + 1,
    334         }
    335     );
    336 
    337     let full = fill_image_tag_bytes(
    338         authored_image(b"full-image", "image/webp", "webp"),
    339         b"full-image",
    340         RADROOTS_POST_TAG_TOTAL_MAX_BYTES,
    341     );
    342     assert!(
    343         AuthoredPhotoUpdate::new(full.url(), vec![full.clone()]).is_ok(),
    344         "PhotoUpdate must accept the exact aggregate maximum"
    345     );
    346     assert_eq!(
    347         AuthoredAsk::new(full.url(), vec![full.clone()]).unwrap_err(),
    348         AuthoredPostError::TagBytesExceeded {
    349             max: RADROOTS_POST_TAG_TOTAL_MAX_BYTES,
    350             actual: RADROOTS_POST_TAG_TOTAL_MAX_BYTES
    351                 + "t".len()
    352                 + RADROOTS_ASK_MARKER_TAG_VALUE.len(),
    353         }
    354     );
    355 
    356     let minimum_fallback_bytes = fallback_element_prefix(b"full-image").len() + 1;
    357     assert_eq!(
    358         full.try_with_fallback(fallback_url_with_element_bytes(
    359             b"full-image",
    360             minimum_fallback_bytes,
    361         ))
    362         .unwrap_err(),
    363         AuthoredPostError::TagBytesExceeded {
    364             max: RADROOTS_POST_TAG_TOTAL_MAX_BYTES,
    365             actual: RADROOTS_POST_TAG_TOTAL_MAX_BYTES + minimum_fallback_bytes,
    366         }
    367     );
    368 }
    369 
    370 #[test]
    371 fn authored_posts_enforce_the_exact_canonical_signed_event_budget() {
    372     let image = fill_image_tag_bytes(
    373         authored_image(b"wire-budget", "image/webp", "webp"),
    374         b"wire-budget",
    375         RADROOTS_POST_TAG_TOTAL_MAX_BYTES,
    376     );
    377     let tags = vec![image.imeta_tag().to_vec()];
    378     let minimum_content = image.url();
    379     let minimum_wire_bytes = canonical_signed_post_json_bytes(minimum_content, &tags);
    380     let padding_bytes = RADROOTS_POST_EVENT_WIRE_MAX_BYTES - minimum_wire_bytes;
    381     let maximum_content = format!("{minimum_content}{}", "x".repeat(padding_bytes));
    382 
    383     let maximum = AuthoredPhotoUpdate::new(maximum_content.clone(), vec![image.clone()]).unwrap();
    384     let maximum_wire = authored_photo_update_to_wire_parts(&maximum);
    385     assert_eq!(
    386         canonical_signed_post_json_bytes(&maximum_wire.content, &maximum_wire.tags),
    387         RADROOTS_POST_EVENT_WIRE_MAX_BYTES
    388     );
    389 
    390     let oversized_content = format!("{maximum_content}x");
    391     assert_eq!(
    392         AuthoredPhotoUpdate::new(oversized_content, vec![image.clone()]).unwrap_err(),
    393         AuthoredPostError::EventWireTooLarge {
    394             max: RADROOTS_POST_EVENT_WIRE_MAX_BYTES,
    395             actual: RADROOTS_POST_EVENT_WIRE_MAX_BYTES + 1,
    396         }
    397     );
    398 
    399     let mut escaped_content = maximum_content;
    400     escaped_content.pop();
    401     escaped_content.push('"');
    402     assert_eq!(
    403         AuthoredPhotoUpdate::new(escaped_content, vec![image]).unwrap_err(),
    404         AuthoredPostError::EventWireTooLarge {
    405             max: RADROOTS_POST_EVENT_WIRE_MAX_BYTES,
    406             actual: RADROOTS_POST_EVENT_WIRE_MAX_BYTES + 1,
    407         }
    408     );
    409 }
    410 
    411 #[test]
    412 fn authored_update_counts_json_control_character_expansion() {
    413     let content = format!(
    414         "a{}",
    415         "\u{0001}".repeat(RADROOTS_POST_CONTENT_MAX_BYTES - 1)
    416     );
    417     let actual = canonical_signed_post_json_bytes(&content, &[]);
    418 
    419     assert_eq!(
    420         AuthoredUpdate::new(content).unwrap_err(),
    421         AuthoredPostError::EventWireTooLarge {
    422             max: RADROOTS_POST_EVENT_WIRE_MAX_BYTES,
    423             actual,
    424         }
    425     );
    426     assert!(actual > RADROOTS_POST_EVENT_WIRE_MAX_BYTES);
    427 }
    428 
    429 fn authored_image(bytes: &[u8], media_type: &str, extension: &str) -> AuthoredPostImage {
    430     AuthoredPostImage::new(
    431         AuthoredImage::try_from(verified_descriptor(bytes, media_type, extension)).unwrap(),
    432         PostImageDimensions::new(1200, 900).unwrap(),
    433         "Harvest",
    434     )
    435     .unwrap()
    436 }
    437 
    438 fn authored_image_with_url(bytes: &[u8], media_type: &str, url: &str) -> AuthoredPostImage {
    439     let hash = Sha256::digest(bytes);
    440     let media_type = MediaType::parse(media_type).unwrap();
    441     let descriptor = BlobDescriptor::new(
    442         BlobUrl::parse(url).unwrap(),
    443         hash,
    444         bytes.len() as u64,
    445         media_type.clone(),
    446         1_784_347_200,
    447     )
    448     .unwrap()
    449     .approve_reference()
    450     .unwrap()
    451     .verify_bytes(bytes, &media_type)
    452     .unwrap();
    453     AuthoredPostImage::new(
    454         AuthoredImage::try_from(descriptor).unwrap(),
    455         PostImageDimensions::new(1200, 900).unwrap(),
    456         "Harvest",
    457     )
    458     .unwrap()
    459 }
    460 
    461 fn verified_descriptor(bytes: &[u8], media_type: &str, extension: &str) -> ByteVerifiedDescriptor {
    462     let hash = Sha256::digest(bytes);
    463     let media_type = MediaType::parse(media_type).unwrap();
    464     BlobDescriptor::new(
    465         BlobUrl::parse(&format!("https://media.example/{hash}.{extension}")).unwrap(),
    466         hash,
    467         bytes.len() as u64,
    468         media_type.clone(),
    469         1_784_347_200,
    470     )
    471     .unwrap()
    472     .approve_reference()
    473     .unwrap()
    474     .verify_bytes(bytes, &media_type)
    475     .unwrap()
    476 }
    477 
    478 fn verified_descriptor_with_url_element_bytes(
    479     bytes: &[u8],
    480     element_bytes: usize,
    481 ) -> ByteVerifiedDescriptor {
    482     let hash = Sha256::digest(bytes);
    483     let prefix = format!("https://media.example/{hash}.");
    484     let extension_bytes = element_bytes
    485         .checked_sub("url ".len() + prefix.len())
    486         .expect("requested URL tag element is large enough");
    487     assert!(extension_bytes > 0);
    488     let media_type = MediaType::parse("image/webp").unwrap();
    489     let descriptor = BlobDescriptor::new(
    490         BlobUrl::parse(&format!("{prefix}{}", "x".repeat(extension_bytes))).unwrap(),
    491         hash,
    492         bytes.len() as u64,
    493         media_type.clone(),
    494         1_784_347_200,
    495     )
    496     .unwrap()
    497     .approve_reference()
    498     .unwrap()
    499     .verify_bytes(bytes, &media_type)
    500     .unwrap();
    501     assert_eq!(
    502         "url ".len() + descriptor.url().as_str().len(),
    503         element_bytes
    504     );
    505     descriptor
    506 }
    507 
    508 fn fallback_url(
    509     bytes: &[u8],
    510     host: &str,
    511     extension: &str,
    512 ) -> radroots_blossom::url::ApprovedBlobUrl {
    513     let hash = Sha256::digest(bytes);
    514     BlobUrl::parse(&format!("https://{host}/{hash}.{extension}"))
    515         .unwrap()
    516         .approve()
    517         .unwrap()
    518 }
    519 
    520 fn fallback_url_with_element_bytes(
    521     bytes: &[u8],
    522     element_bytes: usize,
    523 ) -> radroots_blossom::url::ApprovedBlobUrl {
    524     let prefix = fallback_element_prefix(bytes);
    525     let extension_bytes = element_bytes
    526         .checked_sub(prefix.len())
    527         .expect("requested fallback tag element is large enough");
    528     assert!(extension_bytes > 0);
    529     let fallback = BlobUrl::parse(&format!(
    530         "{}{}",
    531         prefix.strip_prefix("fallback ").unwrap(),
    532         "x".repeat(extension_bytes)
    533     ))
    534     .unwrap()
    535     .approve()
    536     .unwrap();
    537     assert_eq!(format!("fallback {fallback}").len(), element_bytes);
    538     fallback
    539 }
    540 
    541 fn fallback_element_prefix(bytes: &[u8]) -> String {
    542     let hash = Sha256::digest(bytes);
    543     format!("fallback https://cache.example/{hash}.")
    544 }
    545 
    546 fn tag_bytes(tag: &[String]) -> usize {
    547     tag.iter().map(String::len).sum()
    548 }
    549 
    550 fn canonical_signed_post_json_bytes(content: &str, tags: &[Vec<String>]) -> usize {
    551     serde_json::json!({
    552         "id": "0".repeat(64),
    553         "pubkey": "0".repeat(64),
    554         "created_at": u64::MAX,
    555         "kind": 1,
    556         "tags": tags,
    557         "content": content,
    558         "sig": "0".repeat(128),
    559     })
    560     .to_string()
    561     .len()
    562 }
    563 
    564 fn fill_image_tag_bytes(
    565     mut image: AuthoredPostImage,
    566     bytes: &[u8],
    567     target: usize,
    568 ) -> AuthoredPostImage {
    569     let minimum_fallback_bytes = fallback_element_prefix(bytes).len() + 1;
    570     while tag_bytes(image.imeta_tag()) < target {
    571         let remaining = target - tag_bytes(image.imeta_tag());
    572         assert!(remaining >= minimum_fallback_bytes);
    573         let mut next = remaining.min(RADROOTS_POST_TAG_ELEMENT_MAX_BYTES);
    574         if remaining > RADROOTS_POST_TAG_ELEMENT_MAX_BYTES
    575             && remaining - RADROOTS_POST_TAG_ELEMENT_MAX_BYTES < minimum_fallback_bytes
    576         {
    577             next = remaining - minimum_fallback_bytes;
    578         }
    579         assert!((minimum_fallback_bytes..=RADROOTS_POST_TAG_ELEMENT_MAX_BYTES).contains(&next));
    580         image = image
    581             .try_with_fallback(fallback_url_with_element_bytes(bytes, next))
    582             .unwrap();
    583     }
    584     assert_eq!(tag_bytes(image.imeta_tag()), target);
    585     image
    586 }