1use std::io::{Cursor, Error, Result, Write};
4
5use simdnbt::owned::{NbtCompound, NbtList, NbtTag};
6use simdnbt::{FromNbtTag, ToNbtTag};
7use steel_utils::codec::VarInt;
8use steel_utils::hash::{ComponentHasher, HashComponent, HashEntry, sort_map_entries};
9use steel_utils::nbt::NbtNumeric as _;
10use steel_utils::serial::{PrefixedRead, PrefixedWrite, ReadFrom, WriteTo};
11use text_components::TextComponent;
12
13const MAX_NETWORK_STRING_LENGTH: usize = 32_767;
14const MAX_NETWORK_STRING_BYTES: usize = MAX_NETWORK_STRING_LENGTH * 3;
15
16#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct Filterable<T> {
19 raw: T,
20 filtered: Option<T>,
21}
22
23impl<T> Filterable<T> {
24 #[must_use]
25 pub const fn new(raw: T, filtered: Option<T>) -> Self {
26 Self { raw, filtered }
27 }
28
29 #[must_use]
30 pub const fn pass_through(raw: T) -> Self {
31 Self::new(raw, None)
32 }
33
34 #[must_use]
35 pub const fn raw(&self) -> &T {
36 &self.raw
37 }
38
39 #[must_use]
40 pub const fn filtered(&self) -> Option<&T> {
41 self.filtered.as_ref()
42 }
43
44 #[must_use]
45 pub fn get(&self, filter_enabled: bool) -> &T {
46 if filter_enabled {
47 self.filtered.as_ref().unwrap_or(&self.raw)
48 } else {
49 &self.raw
50 }
51 }
52}
53
54#[derive(Debug, Default, Clone, PartialEq, Eq)]
56pub struct WritableBookContent {
57 pages: Vec<Filterable<String>>,
58}
59
60impl WritableBookContent {
61 pub const PAGE_EDIT_LENGTH: usize = 1024;
62 pub const MAX_PAGES: usize = 100;
63
64 #[must_use]
65 pub const fn empty() -> Self {
66 Self { pages: Vec::new() }
67 }
68
69 pub fn new(pages: Vec<Filterable<String>>) -> Result<Self> {
70 if pages.len() > Self::MAX_PAGES {
71 return Err(Error::other(format!(
72 "Got {} pages, but maximum is {}",
73 pages.len(),
74 Self::MAX_PAGES
75 )));
76 }
77 if pages.iter().any(|page| {
78 string_too_long(page.raw(), Self::PAGE_EDIT_LENGTH)
79 || page
80 .filtered()
81 .is_some_and(|value| string_too_long(value, Self::PAGE_EDIT_LENGTH))
82 }) {
83 return Err(Error::other(
84 "Writable book page is longer than 1024 characters",
85 ));
86 }
87 Ok(Self { pages })
88 }
89
90 #[must_use]
91 pub fn pages(&self) -> &[Filterable<String>] {
92 &self.pages
93 }
94
95 fn to_nbt_tag_ref(&self) -> NbtTag {
96 let mut compound = NbtCompound::new();
97 if !self.pages.is_empty() {
98 compound.insert(
99 "pages",
100 NbtTag::List(NbtList::Compound(
101 self.pages.iter().map(filterable_string_nbt).collect(),
102 )),
103 );
104 }
105 NbtTag::Compound(compound)
106 }
107}
108
109impl WriteTo for WritableBookContent {
110 fn write(&self, writer: &mut impl Write) -> Result<()> {
111 write_bounded_count(self.pages.len(), Self::MAX_PAGES, writer)?;
112 for page in &self.pages {
113 write_filterable_string(page, Self::PAGE_EDIT_LENGTH, writer)?;
114 }
115 Ok(())
116 }
117}
118
119impl ReadFrom for WritableBookContent {
120 fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
121 let count = read_bounded_count(data, Self::MAX_PAGES)?;
122 let mut pages = Vec::with_capacity(count);
123 for _ in 0..count {
124 pages.push(read_filterable_string(data, Self::PAGE_EDIT_LENGTH)?);
125 }
126 Self::new(pages)
127 }
128}
129
130impl ToNbtTag for WritableBookContent {
131 fn to_nbt_tag(self) -> NbtTag {
132 self.to_nbt_tag_ref()
133 }
134}
135
136impl FromNbtTag for WritableBookContent {
137 fn from_nbt_tag(tag: simdnbt::borrow::NbtTag) -> Option<Self> {
138 let compound = tag.compound()?;
139 let pages = match compound.get("pages") {
140 Some(tag) => {
141 let values = tag.list()?.to_owned().as_nbt_tags();
142 if values.len() > Self::MAX_PAGES {
143 return None;
144 }
145 values
146 .iter()
147 .map(|tag| filterable_string_from_nbt(tag, Self::PAGE_EDIT_LENGTH))
148 .collect::<Option<Vec<_>>>()?
149 }
150 None => Vec::new(),
151 };
152 Self::new(pages).ok()
153 }
154}
155
156impl HashComponent for WritableBookContent {
157 fn hash_component(&self, hasher: &mut ComponentHasher) {
158 let mut entries = Vec::with_capacity(1);
159 if !self.pages.is_empty() {
160 push_hash_entry(&mut entries, "pages", &FilterableStringList(&self.pages));
161 }
162 hash_entries(hasher, &mut entries);
163 }
164}
165
166#[derive(Debug, Clone, PartialEq, Eq)]
168pub struct WrittenBookContent {
169 title: Filterable<String>,
170 author: String,
171 generation: i32,
172 pages: Vec<Filterable<TextComponent>>,
173 resolved: bool,
174}
175
176impl WrittenBookContent {
177 pub const TITLE_MAX_LENGTH: usize = 32;
178 pub const MAX_GENERATION: i32 = 3;
179 pub const PAGE_LENGTH: usize = 32_767;
180
181 pub fn new(
182 title: Filterable<String>,
183 author: String,
184 generation: i32,
185 pages: Vec<Filterable<TextComponent>>,
186 resolved: bool,
187 ) -> Result<Self> {
188 if string_too_long(title.raw(), Self::TITLE_MAX_LENGTH)
189 || title
190 .filtered()
191 .is_some_and(|value| string_too_long(value, Self::TITLE_MAX_LENGTH))
192 {
193 return Err(Error::other(
194 "Written book title is longer than 32 characters",
195 ));
196 }
197 if !(0..=Self::MAX_GENERATION).contains(&generation) {
198 return Err(Error::other(format!(
199 "Book generation must be in 0..={}, got {generation}",
200 Self::MAX_GENERATION
201 )));
202 }
203 if string_too_long(&author, MAX_NETWORK_STRING_LENGTH)
204 || author.len() > MAX_NETWORK_STRING_BYTES
205 {
206 return Err(Error::other(
207 "Written book author exceeds the network string limit",
208 ));
209 }
210 Ok(Self {
211 title,
212 author,
213 generation,
214 pages,
215 resolved,
216 })
217 }
218
219 #[must_use]
220 pub const fn empty() -> Self {
221 Self {
222 title: Filterable::pass_through(String::new()),
223 author: String::new(),
224 generation: 0,
225 pages: Vec::new(),
226 resolved: true,
227 }
228 }
229
230 #[must_use]
231 pub const fn title(&self) -> &Filterable<String> {
232 &self.title
233 }
234
235 #[must_use]
236 pub fn author(&self) -> &str {
237 &self.author
238 }
239
240 #[must_use]
241 pub const fn generation(&self) -> i32 {
242 self.generation
243 }
244
245 #[must_use]
247 pub fn craft_copy(&self) -> Self {
248 Self {
249 title: self.title.clone(),
250 author: self.author.clone(),
251 generation: self.generation + 1,
252 pages: self.pages.clone(),
253 resolved: self.resolved,
254 }
255 }
256
257 #[must_use]
258 pub fn pages(&self) -> &[Filterable<TextComponent>] {
259 &self.pages
260 }
261
262 #[must_use]
263 pub const fn resolved(&self) -> bool {
264 self.resolved
265 }
266
267 fn to_nbt_tag_ref(&self) -> NbtTag {
268 let mut compound = NbtCompound::new();
269 compound.insert(
270 "title",
271 NbtTag::Compound(filterable_string_nbt(&self.title)),
272 );
273 compound.insert("author", self.author.clone());
274 if self.generation != 0 {
275 compound.insert("generation", self.generation);
276 }
277 if !self.pages.is_empty() {
278 compound.insert(
279 "pages",
280 NbtTag::List(NbtList::Compound(
281 self.pages.iter().map(filterable_component_nbt).collect(),
282 )),
283 );
284 }
285 if self.resolved {
286 compound.insert("resolved", true);
287 }
288 NbtTag::Compound(compound)
289 }
290}
291
292impl WriteTo for WrittenBookContent {
293 fn write(&self, writer: &mut impl Write) -> Result<()> {
294 write_filterable_string(&self.title, Self::TITLE_MAX_LENGTH, writer)?;
295 write_network_string(&self.author, MAX_NETWORK_STRING_LENGTH, writer)?;
296 VarInt(self.generation).write(writer)?;
297 write_count(self.pages.len(), writer)?;
298 for page in &self.pages {
299 write_filterable_component(page, writer)?;
300 }
301 self.resolved.write(writer)
302 }
303}
304
305impl ReadFrom for WrittenBookContent {
306 fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
307 let title = read_filterable_string(data, Self::TITLE_MAX_LENGTH)?;
308 let author = read_network_string(data, MAX_NETWORK_STRING_LENGTH)?;
309 let generation = VarInt::read(data)?.0;
310 let count = read_count(data)?;
311 let mut pages = Vec::with_capacity(count.min(65_536));
312 for _ in 0..count {
313 pages.push(read_filterable_component(data)?);
314 }
315 Self::new(title, author, generation, pages, bool::read(data)?)
316 }
317}
318
319impl ToNbtTag for WrittenBookContent {
320 fn to_nbt_tag(self) -> NbtTag {
321 self.to_nbt_tag_ref()
322 }
323}
324
325impl FromNbtTag for WrittenBookContent {
326 fn from_nbt_tag(tag: simdnbt::borrow::NbtTag) -> Option<Self> {
327 let compound = tag.compound()?;
328 let title =
329 filterable_string_from_nbt(&compound.get("title")?.to_owned(), Self::TITLE_MAX_LENGTH)?;
330 let author = compound.get("author")?.string()?.to_string();
331 let generation = match compound.get("generation") {
332 Some(tag) => tag.codec_i32()?,
333 None => 0,
334 };
335 let pages = match compound.get("pages") {
336 Some(tag) => tag
337 .list()?
338 .to_owned()
339 .as_nbt_tags()
340 .iter()
341 .map(filterable_component_from_nbt)
342 .collect::<Option<Vec<_>>>()?,
343 None => Vec::new(),
344 };
345 let resolved = match compound.get("resolved") {
346 Some(tag) => tag.codec_bool()?,
347 None => false,
348 };
349 Self::new(title, author, generation, pages, resolved).ok()
350 }
351}
352
353impl HashComponent for WrittenBookContent {
354 fn hash_component(&self, hasher: &mut ComponentHasher) {
355 let mut entries = Vec::with_capacity(5);
356 push_hash_entry(&mut entries, "title", &FilterableStringHash(&self.title));
357 push_hash_entry(&mut entries, "author", self.author.as_str());
358 if self.generation != 0 {
359 push_hash_entry(&mut entries, "generation", &self.generation);
360 }
361 if !self.pages.is_empty() {
362 push_hash_entry(&mut entries, "pages", &FilterableComponentList(&self.pages));
363 }
364 if self.resolved {
365 push_hash_entry(&mut entries, "resolved", &true);
366 }
367 hash_entries(hasher, &mut entries);
368 }
369}
370
371fn filterable_string_nbt(value: &Filterable<String>) -> NbtCompound {
372 let mut compound = NbtCompound::new();
373 compound.insert("raw", value.raw.clone());
374 if let Some(filtered) = &value.filtered {
375 compound.insert("filtered", filtered.clone());
376 }
377 compound
378}
379
380fn filterable_string_from_nbt(tag: &NbtTag, max_length: usize) -> Option<Filterable<String>> {
381 if let Some(compound) = tag.compound()
382 && let Some(raw) = compound.get("raw")
383 {
384 let raw = raw.string()?.to_string();
385 let filtered = match compound.get("filtered") {
386 Some(tag) => Some(tag.string()?.to_string()),
387 None => None,
388 };
389 return (!string_too_long(&raw, max_length)
390 && filtered
391 .as_ref()
392 .is_none_or(|value| !string_too_long(value, max_length)))
393 .then_some(Filterable::new(raw, filtered));
394 }
395 let raw = tag.string()?.to_string();
396 (!string_too_long(&raw, max_length)).then_some(Filterable::pass_through(raw))
397}
398
399fn filterable_component_nbt(value: &Filterable<TextComponent>) -> NbtCompound {
400 let mut compound = NbtCompound::new();
401 compound.insert("raw", value.raw.to_codec_nbt());
402 if let Some(filtered) = &value.filtered {
403 compound.insert("filtered", filtered.to_codec_nbt());
404 }
405 compound
406}
407
408fn filterable_component_from_nbt(tag: &NbtTag) -> Option<Filterable<TextComponent>> {
409 if let Some(compound) = tag.compound()
410 && let Some(raw) = compound.get("raw")
411 {
412 let raw = restricted_component_from_nbt(raw)?;
413 let filtered = match compound.get("filtered") {
414 Some(tag) => Some(restricted_component_from_nbt(tag)?),
415 None => None,
416 };
417 return Some(Filterable::new(raw, filtered));
418 }
419 restricted_component_from_nbt(tag).map(Filterable::pass_through)
420}
421
422fn restricted_component_from_nbt(tag: &NbtTag) -> Option<TextComponent> {
423 let component = TextComponent::from_nbt(tag)?;
424 let encoded = serde_json::to_string(&component).ok()?;
425 (encoded.encode_utf16().count() <= WrittenBookContent::PAGE_LENGTH).then_some(component)
426}
427
428fn write_filterable_string(
429 value: &Filterable<String>,
430 max_length: usize,
431 writer: &mut impl Write,
432) -> Result<()> {
433 write_network_string(&value.raw, max_length, writer)?;
434 value.filtered.is_some().write(writer)?;
435 if let Some(filtered) = &value.filtered {
436 write_network_string(filtered, max_length, writer)?;
437 }
438 Ok(())
439}
440
441fn read_filterable_string(
442 data: &mut Cursor<&[u8]>,
443 max_length: usize,
444) -> Result<Filterable<String>> {
445 let raw = read_network_string(data, max_length)?;
446 let filtered = if bool::read(data)? {
447 Some(read_network_string(data, max_length)?)
448 } else {
449 None
450 };
451 Ok(Filterable::new(raw, filtered))
452}
453
454fn write_filterable_component(
455 value: &Filterable<TextComponent>,
456 writer: &mut impl Write,
457) -> Result<()> {
458 write_component_network(&value.raw, writer)?;
459 value.filtered.is_some().write(writer)?;
460 if let Some(filtered) = &value.filtered {
461 write_component_network(filtered, writer)?;
462 }
463 Ok(())
464}
465
466fn read_filterable_component(data: &mut Cursor<&[u8]>) -> Result<Filterable<TextComponent>> {
467 let raw = TextComponent::read(data)?;
468 let filtered = if bool::read(data)? {
469 Some(TextComponent::read(data)?)
470 } else {
471 None
472 };
473 Ok(Filterable::new(raw, filtered))
474}
475
476fn write_component_network(component: &TextComponent, writer: &mut impl Write) -> Result<()> {
477 let mut encoded = Vec::new();
478 component.to_codec_nbt().write(&mut encoded);
479 writer.write_all(&encoded)
480}
481
482fn write_network_string(value: &str, max_length: usize, writer: &mut impl Write) -> Result<()> {
483 if string_too_long(value, max_length) || value.len() > max_length.saturating_mul(3) {
484 return Err(Error::other(format!(
485 "String exceeds the {max_length}-character network limit"
486 )));
487 }
488 value.write_prefixed::<VarInt>(writer)
489}
490
491fn read_network_string(data: &mut Cursor<&[u8]>, max_length: usize) -> Result<String> {
492 let value = String::read_prefixed_bound::<VarInt>(data, max_length.saturating_mul(3))?;
493 if string_too_long(&value, max_length) {
494 return Err(Error::other(format!(
495 "String exceeds the {max_length}-character network limit"
496 )));
497 }
498 Ok(value)
499}
500
501fn string_too_long(value: &str, max_length: usize) -> bool {
502 value.encode_utf16().count() > max_length
503}
504
505fn write_bounded_count(count: usize, max: usize, writer: &mut impl Write) -> Result<()> {
506 if count > max {
507 return Err(Error::other(format!(
508 "Collection size {count} exceeds {max}"
509 )));
510 }
511 write_count(count, writer)
512}
513
514fn write_count(count: usize, writer: &mut impl Write) -> Result<()> {
515 let count = i32::try_from(count).map_err(|_| Error::other("Collection is too large"))?;
516 VarInt(count).write(writer)
517}
518
519fn read_bounded_count(data: &mut Cursor<&[u8]>, max: usize) -> Result<usize> {
520 let count = read_count(data)?;
521 if count > max {
522 return Err(Error::other(format!(
523 "Collection size {count} exceeds {max}"
524 )));
525 }
526 Ok(count)
527}
528
529fn read_count(data: &mut Cursor<&[u8]>) -> Result<usize> {
530 let count = VarInt::read(data)?.0;
531 usize::try_from(count).map_err(|_| Error::other(format!("Negative collection size: {count}")))
532}
533
534struct FilterableStringHash<'a>(&'a Filterable<String>);
535
536impl HashComponent for FilterableStringHash<'_> {
537 fn hash_component(&self, hasher: &mut ComponentHasher) {
538 let mut entries = Vec::with_capacity(2);
539 push_hash_entry(&mut entries, "raw", self.0.raw.as_str());
540 if let Some(filtered) = &self.0.filtered {
541 push_hash_entry(&mut entries, "filtered", filtered.as_str());
542 }
543 hash_entries(hasher, &mut entries);
544 }
545}
546
547struct FilterableComponentHash<'a>(&'a Filterable<TextComponent>);
548
549impl HashComponent for FilterableComponentHash<'_> {
550 fn hash_component(&self, hasher: &mut ComponentHasher) {
551 let mut entries = Vec::with_capacity(2);
552 push_hash_entry(&mut entries, "raw", &self.0.raw);
553 if let Some(filtered) = &self.0.filtered {
554 push_hash_entry(&mut entries, "filtered", filtered);
555 }
556 hash_entries(hasher, &mut entries);
557 }
558}
559
560struct FilterableStringList<'a>(&'a [Filterable<String>]);
561
562impl HashComponent for FilterableStringList<'_> {
563 fn hash_component(&self, hasher: &mut ComponentHasher) {
564 hasher.start_list();
565 for value in self.0 {
566 hasher.put_component_hash(&FilterableStringHash(value));
567 }
568 hasher.end_list();
569 }
570}
571
572struct FilterableComponentList<'a>(&'a [Filterable<TextComponent>]);
573
574impl HashComponent for FilterableComponentList<'_> {
575 fn hash_component(&self, hasher: &mut ComponentHasher) {
576 hasher.start_list();
577 for value in self.0 {
578 hasher.put_component_hash(&FilterableComponentHash(value));
579 }
580 hasher.end_list();
581 }
582}
583
584fn push_hash_entry<T: HashComponent + ?Sized>(entries: &mut Vec<HashEntry>, key: &str, value: &T) {
585 let mut key_hasher = ComponentHasher::new();
586 key_hasher.put_string(key);
587 let mut value_hasher = ComponentHasher::new();
588 value.hash_component(&mut value_hasher);
589 entries.push(HashEntry::new(key_hasher, value_hasher));
590}
591
592fn hash_entries(hasher: &mut ComponentHasher, entries: &mut [HashEntry]) {
593 sort_map_entries(entries);
594 hasher.start_map();
595 for entry in entries {
596 hasher.put_raw_bytes(&entry.key_bytes);
597 hasher.put_raw_bytes(&entry.value_bytes);
598 }
599 hasher.end_map();
600}
601
602#[cfg(test)]
603mod tests {
604 use std::io::Cursor;
605
606 use simdnbt::ToNbtTag as _;
607 use steel_utils::serial::{ReadFrom as _, WriteTo as _};
608 use text_components::TextComponent;
609
610 use super::{Filterable, WritableBookContent, WrittenBookContent};
611 use crate::data_components::vanilla_components::WRITABLE_BOOK_CONTENT;
612 use crate::init_vanilla_registry;
613 use crate::{REGISTRY, RegistryExt};
614
615 fn parse<T: simdnbt::FromNbtTag>(tag: simdnbt::owned::NbtTag) -> Option<T> {
616 let mut bytes = Vec::new();
617 tag.write(&mut bytes);
618 let borrowed = simdnbt::borrow::read_tag(&mut Cursor::new(bytes.as_slice())).ok()?;
619 T::from_nbt_tag(borrowed.as_tag())
620 }
621
622 #[test]
623 fn writable_pages_use_full_filterable_persistence_and_bounded_network() {
624 let value = WritableBookContent::new(vec![Filterable::new(
625 "raw".to_owned(),
626 Some("filtered".to_owned()),
627 )])
628 .expect("valid writable book");
629 let nbt = value.clone().to_nbt_tag();
630 assert_eq!(parse(nbt), Some(value.clone()));
631 let mut network = Vec::new();
632 value.write(&mut network).expect("book should encode");
633 assert_eq!(
634 WritableBookContent::read(&mut Cursor::new(network.as_slice()))
635 .expect("book should decode"),
636 value
637 );
638 assert!(
639 WritableBookContent::new(vec![Filterable::pass_through("x".repeat(1025))]).is_err()
640 );
641 }
642
643 #[test]
644 fn written_book_round_trips_text_pages_and_validates_generation() {
645 let value = WrittenBookContent::new(
646 Filterable::pass_through("Title".to_owned()),
647 "Author".to_owned(),
648 2,
649 vec![Filterable::pass_through(TextComponent::plain("Page"))],
650 true,
651 )
652 .expect("valid written book");
653 let nbt = value.clone().to_nbt_tag();
654 assert_eq!(parse(nbt), Some(value.clone()));
655 let mut network = Vec::new();
656 value.write(&mut network).expect("book should encode");
657 assert_eq!(
658 WrittenBookContent::read(&mut Cursor::new(network.as_slice()))
659 .expect("book should decode"),
660 value
661 );
662 assert!(
663 WrittenBookContent::new(
664 Filterable::pass_through(String::new()),
665 String::new(),
666 4,
667 Vec::new(),
668 false,
669 )
670 .is_err()
671 );
672 }
673
674 #[test]
675 fn extracted_writable_book_starts_with_empty_pages() {
676 init_vanilla_registry();
677 let item = REGISTRY
678 .items
679 .by_key(&steel_utils::Identifier::vanilla_static("writable_book"))
680 .expect("writable book should be registered");
681 assert_eq!(
682 item.components.get(WRITABLE_BOOK_CONTENT),
683 Some(WritableBookContent::empty())
684 );
685 }
686}