osu_rs/
parsers.rs

1use std::{
2    borrow::Cow,
3    ops::{Add, Sub},
4};
5
6use thiserror::Error;
7
8use crate::{
9    BeatmapSection, Context, EventSampleSet, EventTrigger, FullHitSound, HitObject, HitObjectFlags,
10    HitObjectKind, HitSound, SampleSet, SliderKind, SoundType, SoundTypes, Span, StaticCow, Time,
11    TimingPoint, TimingPointFlags,
12};
13
14pub trait ParseField<'a>: Sized {
15    fn parse_field(
16        name: impl Into<Cow<'static, str>>,
17        ctx: &Context,
18        line: impl StaticCow<'a>,
19    ) -> Result<Self, ParseError>;
20}
21
22macro_rules! impl_parse_field {
23    ($($T:ty),*) => {
24        $(impl<'a> ParseField<'a> for $T {
25            fn parse_field(
26                name: impl Into<Cow<'static, str>>,
27                _ctx: &Context,
28                line: impl StaticCow<'a>,
29            ) -> Result<Self, ParseError> {
30                line.as_ref().parse().map_err(ParseError::curry(name, line.span()))
31            }
32        })*
33    };
34}
35
36impl_parse_field!(u8, u16, u32, u64, usize);
37impl_parse_field!(i8, i16, i32, i64, isize);
38impl_parse_field!(f32, f64);
39impl_parse_field!(TimingPointFlags);
40
41impl<'a, T: ParseField<'a>> BeatmapSection<'a> for Vec<T> {
42    fn consume_line(
43        &mut self,
44        ctx: &Context,
45        line: impl StaticCow<'a>,
46    ) -> Result<Option<crate::Section>, ParseError> {
47        self.push(ParseField::parse_field("", ctx, line)?);
48        Ok(None)
49    }
50}
51
52impl<'a, T: Copy + Clone + From<i8> + Add<T, Output = T> + Sub<T, Output = T> + ParseField<'a>>
53    ParseField<'a> for Time<T>
54{
55    fn parse_field(
56        name: impl Into<Cow<'static, str>>,
57        ctx: &Context,
58        line: impl StaticCow<'a>,
59    ) -> Result<Self, ParseError> {
60        Ok(Self::new(
61            ParseField::parse_field(name, ctx, line)?,
62            ctx.version,
63        ))
64    }
65}
66
67impl<'a> ParseField<'a> for Vec<i32> {
68    fn parse_field(
69        name: impl Into<Cow<'static, str>>,
70        ctx: &Context,
71        line: impl StaticCow<'a>,
72    ) -> Result<Self, ParseError> {
73        let mut ret = vec![];
74        let name = name.into();
75        for val in line.split(',') {
76            if !val.as_ref().is_empty() {
77                ret.push(ParseField::parse_field(name.clone(), ctx, val)?);
78            }
79        }
80        Ok(ret)
81    }
82}
83
84impl<'a> ParseField<'a> for bool {
85    fn parse_field(
86        name: impl Into<Cow<'static, str>>,
87        _ctx: &Context,
88        line: impl StaticCow<'a>,
89    ) -> Result<Self, ParseError> {
90        match line.as_ref().bytes().next() {
91            Some(b'1') => Ok(true),
92            Some(_) => Ok(false),
93            None => Err(ParseError::curry(name, line.span())(NoBoolValue)),
94        }
95    }
96}
97
98impl<'a> ParseField<'a> for TimingPoint {
99    fn parse_field(
100        _name: impl Into<Cow<'static, str>>,
101        ctx: &Context,
102        line: impl StaticCow<'a>,
103    ) -> Result<Self, ParseError> {
104        let mut end_span = line.span();
105        end_span.start = end_span.end;
106        let mut s = line.split(',');
107        let offset = s
108            .next()
109            .ok_or(InvalidTimingPoint)
110            .map_err(ParseError::curry("timing point offset", end_span))?;
111        let beat_length = s
112            .next()
113            .ok_or(InvalidTimingPoint)
114            .map_err(ParseError::curry("timing point beat length", end_span))?;
115        let mut ret = TimingPoint {
116            offset: ParseField::parse_field("timing point offset", ctx, offset)?,
117            beat_length: ParseField::parse_field("timing point beat length", ctx, beat_length)?,
118            time_signature: 4,
119            sample_set: None,
120            custom_sample_index: 0,
121            sample_volume: 100,
122            changes_timing: true,
123            flags: TimingPointFlags::empty(),
124        };
125        let Some(time_signature) = s.next() else {
126            return Ok(ret);
127        };
128        if !time_signature.as_ref().starts_with('0') {
129            ret.time_signature =
130                ParseField::parse_field("timing point time signature", ctx, time_signature)?;
131        }
132        let Some(sample_set) = s.next() else {
133            return Ok(ret);
134        };
135        ret.sample_set = Some(
136            i32::parse_field("timing point sample set", ctx, sample_set)?
137                .try_into()
138                .map_err(ParseError::curry(
139                    "timing point sample set",
140                    sample_set.span(),
141                ))?,
142        );
143        let Some(custom_sample_index) = s.next() else {
144            return Ok(ret);
145        };
146        ret.custom_sample_index =
147            ParseField::parse_field("timing point sample index", ctx, custom_sample_index)?;
148        let Some(sample_volume) = s.next() else {
149            return Ok(ret);
150        };
151        ret.sample_volume =
152            ParseField::parse_field("timing point sample volume", ctx, sample_volume)?;
153        let Some(changes_timing) = s.next() else {
154            return Ok(ret);
155        };
156        ret.changes_timing = ParseField::parse_field("timing point type", ctx, changes_timing)?;
157        let Some(flags) = s.next() else {
158            return Ok(ret);
159        };
160        ret.flags = ParseField::parse_field("timing point flags", ctx, flags)?;
161        Ok(ret)
162    }
163}
164
165impl<'a> ParseField<'a> for HitObject<'a> {
166    fn parse_field(
167        _name: impl Into<Cow<'static, str>>,
168        ctx: &Context,
169        line: impl StaticCow<'a>,
170    ) -> Result<Self, ParseError> {
171        let mut end_span = line.span();
172        end_span.start = end_span.end;
173        let mut s = line.split(',');
174        let x = s
175            .next()
176            .ok_or(InvalidHitObject)
177            .map_err(ParseError::curry("hit object x", end_span))?;
178        let x = f64::parse_field("hit object x", ctx, x)?;
179        let x = x as i32;
180        let y = s
181            .next()
182            .ok_or(InvalidHitObject)
183            .map_err(ParseError::curry("hit object y", end_span))?;
184        let y = f64::parse_field("hit object y", ctx, y)?;
185        let y = y as i32;
186        let time = s
187            .next()
188            .ok_or(InvalidHitObject)
189            .map_err(ParseError::curry("hit object time", end_span))?;
190        let time = f64::parse_field("hit object time", ctx, time)?;
191        let time = time as i32;
192        let kind = s
193            .next()
194            .ok_or(InvalidHitObject)
195            .map_err(ParseError::curry("hit object type", end_span))?;
196        let type_span = kind.span();
197        let kind = i32::parse_field("hit object type", ctx, kind)?;
198        let kind = HitObjectFlags::from_bits_retain(kind);
199        let sound = s
200            .next()
201            .ok_or(InvalidHitObject)
202            .map_err(ParseError::curry("hit object sound", end_span))?;
203        let sound = i32::parse_field("hit object sound", ctx, sound)?;
204        let sound = SoundTypes::from_bits_retain(sound);
205        let combo_start = kind.contains(HitObjectFlags::COMBO_START);
206        let combo_colour_skip = (kind & HitObjectFlags::COMBO_COLOUR_OFFSET_MASK).bits() >> 4;
207        let (kind, extra) = if kind.contains(HitObjectFlags::CIRCLE) {
208            (HitObjectKind::Circle, s.next())
209        } else if kind.contains(HitObjectFlags::SLIDER) {
210            let points = s
211                .next()
212                .ok_or(InvalidHitObject)
213                .map_err(ParseError::curry("slider points", end_span))?;
214            let mut kind = SliderKind::Catmull;
215            let mut curve_points = Vec::new();
216            for point in points.split('|') {
217                if point.as_ref().len() == 1 {
218                    kind = SliderKind::try_from(point.as_ref().chars().next().unwrap())
219                        .map_err(ParseError::curry("slider type", point.span()))?;
220                    continue;
221                }
222                let (x, y) = point
223                    .split_once(':')
224                    .ok_or(InvalidHitObject)
225                    .map_err(ParseError::curry("slider point coordinates", point.span()))?;
226                let x = f64::parse_field("slider point x", ctx, x)?;
227                let x = x as i32;
228                let y = f64::parse_field("slider point x", ctx, y)?;
229                let y = y as i32;
230                curve_points.push((x, y));
231            }
232            let slide_count = s
233                .next()
234                .ok_or(InvalidHitObject)
235                .map_err(ParseError::curry("slider slide count", end_span))?;
236            let slide_count = i32::parse_field("slider slide count", ctx, slide_count)?;
237            let length = if let Some(length) = s.next() {
238                ParseField::parse_field("slider length", ctx, length)?
239            } else {
240                0.
241            };
242            let mut edge_sounds = if let Some(edge_sounds) = s.next() {
243                let mut sounds = vec![];
244                if !edge_sounds.as_ref().is_empty() {
245                    for sound in edge_sounds.split('|') {
246                        sounds.push(HitSound {
247                            sounds: SoundTypes::from_bits_retain(ParseField::parse_field(
248                                "slider edge sound",
249                                ctx,
250                                sound,
251                            )?),
252                            addition_set: SampleSet::None,
253                            sample_set: SampleSet::None,
254                        });
255                    }
256                }
257                sounds
258            } else {
259                vec![]
260            };
261            if let Some(edge_samples) = s.next() {
262                if !edge_samples.as_ref().is_empty() {
263                    for (i, sound) in edge_samples.split('|').enumerate() {
264                        let (sample, addition) = sound
265                            .split_once(':')
266                            .ok_or(InvalidHitObject)
267                            .map_err(ParseError::curry("slider edge sample sets", sound.span()))?;
268                        let sample = SampleSet::try_from(i32::parse_field(
269                            "slider edge sample set",
270                            ctx,
271                            sample,
272                        )?)
273                        .map_err(ParseError::curry("slider edge sample set", sample.span()))?;
274                        let addition = SampleSet::try_from(i32::parse_field(
275                            "slider edge sample addition set",
276                            ctx,
277                            addition,
278                        )?)
279                        .map_err(ParseError::curry(
280                            "slider edge sample addition set",
281                            addition.span(),
282                        ))?;
283                        if let Some(val) = edge_sounds.get_mut(i) {
284                            val.sample_set = sample;
285                            val.addition_set = addition;
286                        } else {
287                            edge_sounds.push(HitSound {
288                                sounds: SoundTypes::empty(),
289                                sample_set: sample,
290                                addition_set: addition,
291                            });
292                        }
293                    }
294                }
295            }
296            (
297                HitObjectKind::Slider {
298                    kind,
299                    curve_points,
300                    length,
301                    edge_sounds,
302                    slide_count,
303                },
304                s.next(),
305            )
306        } else if kind.contains(HitObjectFlags::SPINNER) {
307            let end_time = s
308                .next()
309                .ok_or(InvalidHitObject)
310                .map_err(ParseError::curry("spinner end time", end_span))?;
311            let end_time = i32::parse_field("spinner end time", ctx, end_time)?;
312            (HitObjectKind::Spinner { end_time }, s.next())
313        } else if kind.contains(HitObjectFlags::HOLD_NOTE) {
314            let end_time_and_extra = s
315                .next()
316                .ok_or(InvalidHitObject)
317                .map_err(ParseError::curry("hold note end time", end_span))?;
318            let (end_time, extra) = end_time_and_extra
319                .split_once(':')
320                .map(|(a, b)| (a, Some(b)))
321                .unwrap_or((end_time_and_extra, None));
322            let end_time = i32::parse_field("hold note end time", ctx, end_time)?;
323            (HitObjectKind::HoldNote { end_time }, extra)
324        } else {
325            return Err(ParseError::curry("hit object type", type_span)(
326                InvalidHitObject,
327            ));
328        };
329        let mut hit_sound = FullHitSound {
330            hit_sound: HitSound {
331                sounds: sound,
332                sample_set: SampleSet::None,
333                addition_set: SampleSet::None,
334            },
335            custom_sample_index: 0,
336            volume: 0,
337            sample_file: "".into(),
338        };
339        if let Some(extra) = extra.filter(|x| !x.as_ref().is_empty()) {
340            let mut s = extra.split(':');
341            let sample = s
342                .next()
343                .ok_or(InvalidHitObject)
344                .map_err(ParseError::curry("hit object sample set", end_span))?;
345            hit_sound.hit_sound.sample_set =
346                SampleSet::try_from(i32::parse_field("hit object sample set", ctx, sample)?)
347                    .map_err(ParseError::curry("hit object sample set", sample.span()))?;
348            let addition = s.next().ok_or(InvalidHitObject).map_err(ParseError::curry(
349                "hit object sample addition set",
350                end_span,
351            ))?;
352            hit_sound.hit_sound.addition_set = SampleSet::try_from(i32::parse_field(
353                "hit object sample addition set",
354                ctx,
355                addition,
356            )?)
357            .map_err(ParseError::curry(
358                "hit object sample addition set",
359                addition.span(),
360            ))?;
361            if let Some(custom) = s.next() {
362                hit_sound.custom_sample_index =
363                    i32::parse_field("hit object custom sample index", ctx, custom)?;
364            }
365            if let Some(volume) = s.next() {
366                let volume = i32::parse_field("hit object sample volume", ctx, volume)?;
367                hit_sound.volume = volume;
368            }
369            if let Some(sample_file) = s.next() {
370                hit_sound.sample_file = sample_file.into_cow();
371            }
372        }
373        Ok(Self {
374            x,
375            y,
376            time,
377            combo_start,
378            combo_colour_skip,
379            hit_sound,
380            kind,
381        })
382    }
383}
384
385impl<'a> ParseField<'a> for EventTrigger {
386    fn parse_field(
387        name: impl Into<Cow<'static, str>>,
388        _ctx: &Context,
389        line: impl StaticCow<'a>,
390    ) -> Result<Self, ParseError> {
391        match line.as_ref() {
392            "Passing" => Ok(Self::Passing),
393            "Failing" => Ok(Self::Failing),
394            "HitObjectHit" => Ok(Self::HitObjectHit),
395            _ => {
396                if let Some(s) = line.as_ref().strip_prefix("HitSound") {
397                    let (set, s) = if let Some(s) = s.strip_prefix("All") {
398                        (Some(EventSampleSet::All), s)
399                    } else if let Some(s) = s.strip_prefix("Normal") {
400                        (Some(EventSampleSet::Normal), s)
401                    } else if let Some(s) = s.strip_prefix("Soft") {
402                        (Some(EventSampleSet::Soft), s)
403                    } else if let Some(s) = s.strip_prefix("Drum") {
404                        (Some(EventSampleSet::Drum), s)
405                    } else {
406                        (None, s)
407                    };
408                    let (sample_set, addition_set, s) = if let Some(set) = set {
409                        let (add, s) = if let Some(s) = s.strip_prefix("All") {
410                            (Some(EventSampleSet::All), s)
411                        } else if let Some(s) = s.strip_prefix("Normal") {
412                            (Some(EventSampleSet::Normal), s)
413                        } else if let Some(s) = s.strip_prefix("Soft") {
414                            (Some(EventSampleSet::Soft), s)
415                        } else if let Some(s) = s.strip_prefix("Drum") {
416                            (Some(EventSampleSet::Drum), s)
417                        } else {
418                            (None, s)
419                        };
420                        (Some(set), add, s)
421                    } else {
422                        (None, None, s)
423                    };
424                    let (sound, s) = if let Some(s) = s.strip_prefix("Whistle") {
425                        (Some(SoundType::Whistle), s)
426                    } else if let Some(s) = s.strip_prefix("Finish") {
427                        (Some(SoundType::Finish), s)
428                    } else if let Some(s) = s.strip_prefix("Clap") {
429                        (Some(SoundType::Clap), s)
430                    } else {
431                        (None, s)
432                    };
433                    let custom_sample_index = s.parse().ok();
434                    Ok(Self::HitSound {
435                        sample_set,
436                        addition_set,
437                        sound,
438                        custom_sample_index,
439                    })
440                } else {
441                    Err(ParseError::curry(name, line.span())(InvalidEventCommand))
442                }
443            }
444        }
445    }
446}
447
448impl std::fmt::Display for EventTrigger {
449    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
450        match self {
451            Self::Passing => f.write_str("Passing"),
452            Self::Failing => f.write_str("Failing"),
453            Self::HitObjectHit => f.write_str("HitObjectHit"),
454            Self::HitSound {
455                sample_set,
456                addition_set,
457                sound,
458                custom_sample_index,
459            } => {
460                let sample_set = if addition_set.is_some() {
461                    Some(sample_set.unwrap_or_default())
462                } else {
463                    *sample_set
464                };
465                f.write_str("HitSound")?;
466                for set in [sample_set, *addition_set].into_iter().flatten() {
467                    write!(f, "{set}")?;
468                }
469                if let Some(sound) = sound {
470                    write!(f, "{sound}")?;
471                }
472                if let Some(idx) = custom_sample_index {
473                    write!(f, "{idx}")?;
474                }
475                Ok(())
476            }
477        }
478    }
479}
480
481impl<'a> ParseField<'a> for Cow<'a, str> {
482    fn parse_field(
483        _name: impl Into<Cow<'static, str>>,
484        _ctx: &Context,
485        line: impl StaticCow<'a>,
486    ) -> Result<Self, ParseError> {
487        Ok(line.into_cow())
488    }
489}
490
491#[derive(Debug, Error)]
492pub struct EnumParseError {
493    pub valid_variants: &'static [&'static str],
494}
495
496impl std::fmt::Display for EnumParseError {
497    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
498        f.write_str("unexpected enum value (valid values: ")?;
499        let mut first = true;
500        for x in self.valid_variants {
501            if first {
502                first = false;
503            } else {
504                f.write_str(", ")?;
505            }
506            f.write_str(x)?;
507        }
508        f.write_str(")")
509    }
510}
511
512#[derive(Debug, Error)]
513pub struct IntEnumParseError {
514    pub variant: i32,
515    pub valid_variants: &'static [i32],
516}
517
518impl std::fmt::Display for IntEnumParseError {
519    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
520        write!(f, "unexpected enum value {} (valid values: ", self.variant)?;
521        let mut first = true;
522        for x in self.valid_variants {
523            if first {
524                first = false;
525            } else {
526                f.write_str(", ")?;
527            }
528            x.fmt(f)?;
529        }
530        f.write_str(")")
531    }
532}
533
534#[derive(Debug, Error)]
535pub struct CharEnumParseError {
536    pub variant: char,
537    pub valid_variants: &'static [char],
538}
539
540impl std::fmt::Display for CharEnumParseError {
541    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
542        write!(f, "unexpected enum value {} (valid values: ", self.variant)?;
543        let mut first = true;
544        for x in self.valid_variants {
545            if first {
546                first = false;
547            } else {
548                f.write_str(", ")?;
549            }
550            x.fmt(f)?;
551        }
552        f.write_str(")")
553    }
554}
555
556#[derive(Debug, Error)]
557pub struct InvalidRecordField;
558
559impl std::fmt::Display for InvalidRecordField {
560    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
561        write!(f, "invalid record field")
562    }
563}
564
565#[derive(Debug, Error)]
566pub struct RecordParseError {
567    pub valid_fields: &'static [&'static str],
568}
569
570impl std::fmt::Display for RecordParseError {
571    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
572        f.write_str("unexpected field (valid fields: ")?;
573        let mut first = true;
574        for x in self.valid_fields {
575            if first {
576                first = false;
577            } else {
578                f.write_str(", ")?;
579            }
580            f.write_str(x)?;
581        }
582        f.write_str(")")
583    }
584}
585
586#[derive(Debug, Error)]
587pub struct NoBoolValue;
588
589impl std::fmt::Display for NoBoolValue {
590    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
591        f.write_str("missing boolean value")
592    }
593}
594
595#[derive(Debug, Error)]
596pub struct InvalidColour;
597
598impl std::fmt::Display for InvalidColour {
599    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
600        f.write_str("invalid colour")
601    }
602}
603
604#[derive(Debug, Error)]
605pub struct InvalidTimingPoint;
606
607impl std::fmt::Display for InvalidTimingPoint {
608    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
609        f.write_str("invalid timing point")
610    }
611}
612
613#[derive(Debug, Error)]
614pub struct InvalidHitObject;
615
616impl std::fmt::Display for InvalidHitObject {
617    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
618        f.write_str("invalid hit object")
619    }
620}
621
622#[derive(Debug, Error)]
623pub struct InvalidEvent;
624
625impl std::fmt::Display for InvalidEvent {
626    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
627        f.write_str("invalid event")
628    }
629}
630
631#[derive(Debug, Error)]
632pub struct InvalidEventCommand;
633
634impl std::fmt::Display for InvalidEventCommand {
635    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
636        f.write_str("invalid event command")
637    }
638}
639
640#[derive(Debug, Error)]
641pub enum ParseErrorReason {
642    #[error("{0}")]
643    Bool(
644        #[from]
645        #[source]
646        NoBoolValue,
647    ),
648    #[error("{0}")]
649    InvalidColour(
650        #[from]
651        #[source]
652        InvalidColour,
653    ),
654    #[error("{0}")]
655    InvalidTimingPoint(
656        #[from]
657        #[source]
658        InvalidTimingPoint,
659    ),
660    #[error("{0}")]
661    InvalidHitObject(
662        #[from]
663        #[source]
664        InvalidHitObject,
665    ),
666    #[error("{0}")]
667    InvalidEvent(
668        #[from]
669        #[source]
670        InvalidEvent,
671    ),
672    #[error("{0}")]
673    InvalidEventCommand(
674        #[from]
675        #[source]
676        InvalidEventCommand,
677    ),
678    #[error("{0}")]
679    Int(
680        #[from]
681        #[source]
682        std::num::ParseIntError,
683    ),
684    #[error("{0}")]
685    Float(
686        #[from]
687        #[source]
688        std::num::ParseFloatError,
689    ),
690    #[error("{0}")]
691    Enum(
692        #[from]
693        #[source]
694        EnumParseError,
695    ),
696    #[error("{0}")]
697    Record(
698        #[from]
699        #[source]
700        RecordParseError,
701    ),
702    #[error("{0}")]
703    InvalidRecordField(
704        #[from]
705        #[source]
706        InvalidRecordField,
707    ),
708    #[error("{0}")]
709    IntEnum(
710        #[from]
711        #[source]
712        IntEnumParseError,
713    ),
714    #[error("{0}")]
715    CharEnum(
716        #[from]
717        #[source]
718        CharEnumParseError,
719    ),
720}
721
722impl From<std::convert::Infallible> for ParseErrorReason {
723    fn from(value: std::convert::Infallible) -> Self {
724        match value {}
725    }
726}
727
728#[derive(Debug, Error)]
729pub struct ParseError {
730    pub field: Cow<'static, str>,
731    pub span: Span,
732    #[source]
733    pub reason: ParseErrorReason,
734}
735
736impl ParseError {
737    pub fn curry<E: Into<ParseErrorReason>>(
738        field: impl Into<Cow<'static, str>>,
739        span: Span,
740    ) -> impl FnOnce(E) -> Self {
741        move |reason| Self {
742            field: field.into(),
743            span,
744            reason: reason.into(),
745        }
746    }
747}
748
749impl std::fmt::Display for ParseError {
750    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
751        f.write_str("failed to parse ")?;
752        f.write_str(&self.field)?;
753        f.write_str(": ")?;
754        self.reason.fmt(f)
755    }
756}