diff --git a/Cargo.toml b/Cargo.toml index d165a077..e70aa8fe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,7 +43,7 @@ jiff = { version = "0.2.1", default-features = false } arbitrary = { version = "1.4.1", optional = true, features = ["derive"] } bumpalo = { version = "3.12", optional = true } constant_time_eq = { version = "0.4.2", optional = true } -octseq = { version = "0.6.0", default-features = false } +octseq = { version = "0.6.1", default-features = false } rand = { version = "0.10.1", optional = true } arc-swap = { version = "1.7.0", optional = true } bytes = { version = "1.2", optional = true, default-features = false } @@ -60,7 +60,7 @@ proc-macro2 = { version = "1.0.69", optional = true } # Force proc-macro2 to ring = { version = "0.17.2", optional = true } rustversion = { version = "1", optional = true } secrecy = { version = "0.10", optional = true } -serde = { version = "1.0.130", optional = true, features = ["derive"] } +serde = { version = "1.0.130", optional = true, default-features = false, features = ["derive"] } siphasher = { version = "1", optional = true } smallvec = { version = "1.3", optional = true } tokio = { version = "1.33", optional = true, features = ["io-util", "macros", "net", "time", "sync", "rt-multi-thread" ] } @@ -73,15 +73,15 @@ tracing-subscriber = { version = "0.3.18", optional = true, features = ["env-fil default = ["std", "rand"] # Support for libraries -alloc = ["jiff/alloc"] +alloc = ["jiff/alloc", "octseq/alloc", "serde?/alloc"] bumpalo = ["dep:bumpalo", "std"] bytes = ["dep:bytes", "octseq/bytes"] chrono = ["dep:chrono"] heapless = ["dep:heapless", "octseq/heapless"] rand = ["dep:rand"] -serde = ["std", "dep:serde", "octseq/serde"] +serde = ["dep:serde", "octseq/serde"] smallvec = ["dep:smallvec", "octseq/smallvec"] -std = ["alloc", "dep:hashbrown", "bumpalo?/std", "bytes?/std", "octseq/std", "jiff/std"] +std = ["alloc", "dep:hashbrown", "bumpalo?/std", "bytes?/std", "octseq/std", "jiff/std", "serde?/std"] tracing = ["dep:log", "dep:tracing"] # Cryptographic backends diff --git a/Changelog.md b/Changelog.md index a3389a67..629e2705 100644 --- a/Changelog.md +++ b/Changelog.md @@ -8,10 +8,16 @@ New Improvements +* Implemented `to_mnemonic_str` for `Rcode` and `OptCode` ([#668] and [#648] + by [@rossmacarthur]) + Bug fixes Other changes +[#648]: https://github.com/NLnetLabs/domain/pull/648 +[#668]: https://github.com/NLnetLabs/domain/pull/668 +[@rossmacarthur]: https://github.com/rossmacarthur ## 0.12.1 diff --git a/src/base/iana/rcode.rs b/src/base/iana/rcode.rs index 6651c72d..cdd26165 100644 --- a/src/base/iana/rcode.rs +++ b/src/base/iana/rcode.rs @@ -19,6 +19,7 @@ // bits of the wrapped integer. use core::fmt; +use core::str; use core::str::FromStr; //------------ Rcode --------------------------------------------------------- @@ -195,18 +196,27 @@ impl Rcode { /// Returns the mnemonic for this value if there is one. #[must_use] pub const fn to_mnemonic(self) -> Option<&'static [u8]> { + match self.to_mnemonic_str() { + Some(m) => Some(m.as_bytes()), + None => None, + } + } + + /// Returns the mnemonic as a `&str` for this value if there is one + #[must_use] + pub const fn to_mnemonic_str(self) -> Option<&'static str> { match self { - Rcode::NOERROR => Some(b"NOERROR"), - Rcode::FORMERR => Some(b"FORMERR"), - Rcode::SERVFAIL => Some(b"SERVFAIL"), - Rcode::NXDOMAIN => Some(b"NXDOMAIN"), - Rcode::NOTIMP => Some(b"NOTIMP"), - Rcode::REFUSED => Some(b"REFUSED"), - Rcode::YXDOMAIN => Some(b"YXDOMAIN"), - Rcode::YXRRSET => Some(b"YXRRSET"), - Rcode::NXRRSET => Some(b"NXRRSET"), - Rcode::NOTAUTH => Some(b"NOTAUTH"), - Rcode::NOTZONE => Some(b"NOTZONE"), + Rcode::NOERROR => Some("NOERROR"), + Rcode::FORMERR => Some("FORMERR"), + Rcode::SERVFAIL => Some("SERVFAIL"), + Rcode::NXDOMAIN => Some("NXDOMAIN"), + Rcode::NOTIMP => Some("NOTIMP"), + Rcode::REFUSED => Some("REFUSED"), + Rcode::YXDOMAIN => Some("YXDOMAIN"), + Rcode::YXRRSET => Some("YXRRSET"), + Rcode::NXRRSET => Some("NXRRSET"), + Rcode::NOTAUTH => Some("NOTAUTH"), + Rcode::NOTZONE => Some("NOTZONE"), _ => None, } } @@ -255,10 +265,7 @@ impl From for u8 { impl fmt::Display for Rcode { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self - .to_mnemonic() - .and_then(|bytes| core::str::from_utf8(bytes).ok()) - { + match self.to_mnemonic_str() { Some(mnemonic) => f.write_str(mnemonic), None => self.0.fmt(f), } @@ -267,10 +274,7 @@ impl fmt::Display for Rcode { impl fmt::Debug for Rcode { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self - .to_mnemonic() - .and_then(|bytes| core::str::from_utf8(bytes).ok()) - { + match self.to_mnemonic_str() { Some(mnemonic) => write!(f, "Rcode::{}", mnemonic), None => f.debug_tuple("Rcode").field(&self.0).finish(), } @@ -575,20 +579,29 @@ impl OptRcode { /// Returns the mnemonic for this value if there is one. #[must_use] pub const fn to_mnemonic(self) -> Option<&'static [u8]> { + match self.to_mnemonic_str() { + Some(m) => Some(m.as_bytes()), + None => None, + } + } + + /// Returns the mnemonic as a `&str` for this value if there is one + #[must_use] + pub const fn to_mnemonic_str(self) -> Option<&'static str> { match self { - OptRcode::NOERROR => Some(b"NOERROR"), - OptRcode::FORMERR => Some(b"FORMERR"), - OptRcode::SERVFAIL => Some(b"SERVFAIL"), - OptRcode::NXDOMAIN => Some(b"NXDOMAIN"), - OptRcode::NOTIMP => Some(b"NOTIMP"), - OptRcode::REFUSED => Some(b"REFUSED"), - OptRcode::YXDOMAIN => Some(b"YXDOMAIN"), - OptRcode::YXRRSET => Some(b"YXRRSET"), - OptRcode::NXRRSET => Some(b"NXRRSET"), - OptRcode::NOTAUTH => Some(b"NOTAUTH"), - OptRcode::NOTZONE => Some(b"NOTZONE"), - OptRcode::BADVERS => Some(b"BADVERS"), - OptRcode::BADCOOKIE => Some(b"BADCOOKIE"), + OptRcode::NOERROR => Some("NOERROR"), + OptRcode::FORMERR => Some("FORMERR"), + OptRcode::SERVFAIL => Some("SERVFAIL"), + OptRcode::NXDOMAIN => Some("NXDOMAIN"), + OptRcode::NOTIMP => Some("NOTIMP"), + OptRcode::REFUSED => Some("REFUSED"), + OptRcode::YXDOMAIN => Some("YXDOMAIN"), + OptRcode::YXRRSET => Some("YXRRSET"), + OptRcode::NXRRSET => Some("NXRRSET"), + OptRcode::NOTAUTH => Some("NOTAUTH"), + OptRcode::NOTZONE => Some("NOTZONE"), + OptRcode::BADVERS => Some("BADVERS"), + OptRcode::BADCOOKIE => Some("BADCOOKIE"), _ => None, } } @@ -645,10 +658,7 @@ impl From for OptRcode { impl fmt::Display for OptRcode { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self - .to_mnemonic() - .and_then(|bytes| core::str::from_utf8(bytes).ok()) - { + match self.to_mnemonic_str() { Some(mnemonic) => f.write_str(mnemonic), None => self.0.fmt(f), } @@ -657,10 +667,7 @@ impl fmt::Display for OptRcode { impl fmt::Debug for OptRcode { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self - .to_mnemonic() - .and_then(|bytes| core::str::from_utf8(bytes).ok()) - { + match self.to_mnemonic_str() { Some(mnemonic) => write!(f, "Rcode::{}", mnemonic), None => f.debug_tuple("Rcode").field(&self.0).finish(), } @@ -973,6 +980,22 @@ mod test { assert!("#$%!@".parse::().is_err()); } + #[test] + fn rcode_tomnemonicstr() { + assert_eq!(Rcode::NOERROR.to_mnemonic_str(), Some("NOERROR")); + assert_eq!(Rcode::FORMERR.to_mnemonic_str(), Some("FORMERR")); + assert_eq!(Rcode::SERVFAIL.to_mnemonic_str(), Some("SERVFAIL")); + assert_eq!(Rcode::NXDOMAIN.to_mnemonic_str(), Some("NXDOMAIN")); + assert_eq!(Rcode::NOTIMP.to_mnemonic_str(), Some("NOTIMP")); + assert_eq!(Rcode::REFUSED.to_mnemonic_str(), Some("REFUSED")); + assert_eq!(Rcode::YXDOMAIN.to_mnemonic_str(), Some("YXDOMAIN")); + assert_eq!(Rcode::YXRRSET.to_mnemonic_str(), Some("YXRRSET")); + assert_eq!(Rcode::NXRRSET.to_mnemonic_str(), Some("NXRRSET")); + assert_eq!(Rcode::NOTAUTH.to_mnemonic_str(), Some("NOTAUTH")); + assert_eq!(Rcode::NOTZONE.to_mnemonic_str(), Some("NOTZONE")); + assert_eq!(Rcode(42).to_mnemonic_str(), None); + } + #[test] fn optrcode_fromstr() { assert_eq!(Ok(OptRcode::NOERROR), "NOERROR".parse()); @@ -991,6 +1014,24 @@ mod test { assert!("#$%!@".parse::().is_err()); } + #[test] + fn optrcode_tomnemonicstr() { + assert_eq!(OptRcode::NOERROR.to_mnemonic_str(), Some("NOERROR")); + assert_eq!(OptRcode::FORMERR.to_mnemonic_str(), Some("FORMERR")); + assert_eq!(OptRcode::SERVFAIL.to_mnemonic_str(), Some("SERVFAIL")); + assert_eq!(OptRcode::NXDOMAIN.to_mnemonic_str(), Some("NXDOMAIN")); + assert_eq!(OptRcode::NOTIMP.to_mnemonic_str(), Some("NOTIMP")); + assert_eq!(OptRcode::REFUSED.to_mnemonic_str(), Some("REFUSED")); + assert_eq!(OptRcode::YXDOMAIN.to_mnemonic_str(), Some("YXDOMAIN")); + assert_eq!(OptRcode::YXRRSET.to_mnemonic_str(), Some("YXRRSET")); + assert_eq!(OptRcode::NXRRSET.to_mnemonic_str(), Some("NXRRSET")); + assert_eq!(OptRcode::NOTAUTH.to_mnemonic_str(), Some("NOTAUTH")); + assert_eq!(OptRcode::NOTZONE.to_mnemonic_str(), Some("NOTZONE")); + assert_eq!(OptRcode::BADVERS.to_mnemonic_str(), Some("BADVERS")); + assert_eq!(OptRcode::BADCOOKIE.to_mnemonic_str(), Some("BADCOOKIE")); + assert_eq!(OptRcode(42).to_mnemonic_str(), None); + } + #[test] fn optrcode_isext() { assert!(!OptRcode::NOERROR.is_ext()); diff --git a/src/base/record.rs b/src/base/record.rs index e9449352..1c50810f 100644 --- a/src/base/record.rs +++ b/src/base/record.rs @@ -80,6 +80,12 @@ use octseq::parse::Parser; #[cfg_attr(not(feature = "zonefile"), doc = "zonefile")] /// module for that. /// +/// # Equality, Ordering, and Hashing +/// +/// The implementations for the standard equality, ordering, and hashing +/// traits ignore the TTL. That is, two records with the same owner, class, +/// type, and record data are considered identical even if their TTLs differ. +/// /// [`new`]: #method.new /// [`Message`]: ../message/struct.Message.html /// [`MessageBuilder`]: ../message_builder/struct.MessageBuilder.html @@ -300,6 +306,9 @@ where } //--- PartialEq and Eq +// +// These impls ignore the TTL. This may have been a poor choice, but we +// keep it for now and reconsider with the re-write of base. impl PartialEq> for Record where diff --git a/src/new/base/name/absolute.rs b/src/new/base/name/absolute.rs index 788696ce..63a6e38e 100644 --- a/src/new/base/name/absolute.rs +++ b/src/new/base/name/absolute.rs @@ -26,7 +26,7 @@ use crate::new::zonefile::scanner::{Scan, ScanError, Scanner}; use super::{ CanonicalName, Label, LabelBuf, LabelIter, LabelParseError, - NameCompressor, + NameCompressor, RevNameBuf, }; //----------- Name ----------------------------------------------------------- @@ -103,6 +103,11 @@ impl Name { // SAFETY: A 'Name' always contains valid encoded labels. unsafe { LabelIter::new_unchecked(self.as_bytes()) } } + + /// Covert &[`Name`] into [`RevNameBuf`] + pub fn to_revname(&self) -> RevNameBuf { + NameBuf::copy_from(self).into() + } } //--- Canonical operations @@ -995,10 +1000,25 @@ impl fmt::Display for NameParseError { } } +// -- Convert from old Name to new::base::NameBuf ---------------------------- + +/// Upgrade a [`crate::base::Name`] into a +/// [`crate::new::base::name::NameBuf`]. +impl From<&crate::base::Name> for NameBuf +where + Octs: AsRef<[u8]> + ?Sized, +{ + fn from(value: &crate::base::Name) -> Self { + NameBuf::parse_bytes(value.as_slice()) + .expect("Tried to upgrade invalid name") + } +} + //============ Unit tests ==================================================== #[cfg(test)] mod test { + use super::*; #[cfg(feature = "zonefile")] #[test] fn scan() { @@ -1051,4 +1071,23 @@ mod test { } } } + + #[test] + fn test_upgrade_name_to_namebuf() { + let old_name = + crate::base::Name::from_slice(b"\x07example\x03com\x00") + .expect("Invalid name"); + + let new_name: NameBuf = old_name.into(); + assert_eq!(old_name.as_slice(), new_name.as_bytes()) + } + + #[test] + fn test_to_revname() { + let namebuf: NameBuf = "example.com".parse().unwrap(); + assert_eq!( + namebuf.to_revname().as_bytes(), + b"\x00\x03com\x07example", + ); + } } diff --git a/src/new/base/name/reversed.rs b/src/new/base/name/reversed.rs index a902f276..3111444d 100644 --- a/src/new/base/name/reversed.rs +++ b/src/new/base/name/reversed.rs @@ -115,6 +115,11 @@ impl RevName { // SAFETY: A 'RevName' always contains valid encoded labels. unsafe { LabelIter::new_unchecked(self.as_bytes()) } } + + /// Covert &[`RevName`] into [`NameBuf`] + pub fn to_name(&self) -> NameBuf { + RevNameBuf::copy_from(self).into() + } } //--- Building in DNS messages @@ -777,10 +782,30 @@ impl<'a> serde::Deserialize<'a> for std::boxed::Box { } } +// -- Convert from old Name to new::base::RevNameBuf ------------------------- + +/// Upgrade a [`crate::base::Name`] into a +/// [`crate::new::base::name::reversed::RevNameBuf`]. +/// +/// # Panics +/// +/// The [`crate::base::Name`] slice has to contain a valid domain. +impl From<&crate::base::Name> for RevNameBuf +where + Octs: AsRef<[u8]> + ?Sized, +{ + fn from(value: &crate::base::Name) -> Self { + RevNameBuf::parse_bytes(value.as_slice()) + .expect("Tried to upgrade invalid name") + } +} + //============ Unit tests ==================================================== #[cfg(test)] mod test { + use super::*; + #[cfg(feature = "zonefile")] #[test] fn scan() { @@ -830,4 +855,26 @@ mod test { } } } + + #[test] + #[cfg(feature = "alloc")] + fn test_upgrade_revnamebuf() { + let old_name = + crate::base::Name::from_slice(b"\x07example\x03com\x00") + .expect("Invalid name"); + + let new_name: RevNameBuf = old_name.into(); + let mut buf = alloc::vec![0; new_name.built_bytes_size()]; + new_name.build_bytes(&mut buf).unwrap(); + assert_eq!(old_name.as_slice(), buf); + } + + #[test] + fn test_to_name() { + let revnamebuf: RevNameBuf = "example.com".parse().unwrap(); + assert_eq!( + revnamebuf.to_name().as_bytes(), + b"\x07example\x03com\x00", + ); + } } diff --git a/src/new/base/record.rs b/src/new/base/record.rs index cfcbb98f..88e09f8b 100644 --- a/src/new/base/record.rs +++ b/src/new/base/record.rs @@ -506,6 +506,53 @@ impl Scan<'_> for RType { Err(ScanError::Custom("unrecognized record type")) } } +/// Format an [`RType`] in a human-readable way +/// +/// Return the mnemonic of [`RType`]. If [`RType`] is unknown, then the +/// returned string contains the type in the unknown format as defined in +/// Section 5 in [RFC3597]. +/// +/// The mnemonics are consolidated by [IANA]. +/// +/// ``` +/// # use domain::new::base::RType; +/// // Known RType with mnemonic +/// assert_eq!("A", format!("{}", RType::A)); +/// // Unknown RType +/// assert_eq!("TYPE265", format!("{}", RType::from(265))); +/// ``` +/// +/// [RFC3597]: https://datatracker.ietf.org/doc/html/rfc3597#section-5 +/// [IANA]: https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml +impl fmt::Display for RType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match *self { + Self::A => "A", + Self::NS => "NS", + Self::CNAME => "CNAME", + Self::SOA => "SOA", + Self::PTR => "PTR", + Self::HINFO => "HINFO", + Self::MX => "MX", + Self::TXT => "TXT", + Self::RP => "RP", + Self::AAAA => "AAAA", + Self::DNAME => "DNAME", + Self::OPT => "OPT", + Self::DS => "DS", + Self::RRSIG => "RRSIG", + Self::NSEC => "NSEC", + Self::DNSKEY => "DNSKEY", + Self::NSEC3 => "NSEC3", + Self::NSEC3PARAM => "NSEC3PARAM", + Self::CDS => "CDS", + Self::CDNSKEY => "CDNSKEY", + Self::ZONEMD => "ZONEMD", + Self::TSIG => "TSIG", + _ => return write!(f, "TYPE{}", self.code), + }) + } +} //----------- RClass --------------------------------------------------------- @@ -549,6 +596,22 @@ impl RClass { pub const CH: Self = Self::new(3); } +//--- Conversion to and from 'u16' + +impl From for RClass { + fn from(value: u16) -> Self { + Self { + code: U16::new(value), + } + } +} + +impl From for u16 { + fn from(value: RClass) -> Self { + value.code.get() + } +} + //--- Formatting impl fmt::Debug for RClass { @@ -594,6 +657,34 @@ impl Scan<'_> for RClass { } } +/// Format an [`RClass`] in a human-readable way +/// +/// Return the mnemonic of [`RClass`]. If [`RClass`] is unknown, then the +/// returned string contains the class in the unknown format as defined in +/// Section 5 in [RFC3597]. +/// +/// The mnemonics are consolidated by [IANA]. +/// +/// ``` +/// # use domain::new::base::RClass; +/// // Known RClass with mnemonic +/// assert_eq!("IN", format!("{}", RClass::IN)); +/// // Unknown RClass +/// assert_eq!("CLASS42", format!("{}", RClass::from(42))); +/// ``` +/// +/// [RFC3597]: https://datatracker.ietf.org/doc/html/rfc3597#section-5 +/// [IANA]: https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml +impl fmt::Display for RClass { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match *self { + Self::IN => "IN", + Self::CH => "CH", + _ => return write!(f, "CLASS{}", self.code), + }) + } +} + //----------- TTL ------------------------------------------------------------ /// How long a record can be cached. @@ -854,7 +945,7 @@ impl Clone for alloc::boxed::Box { #[cfg(test)] mod test { - use super::{RClass, RType, Record, UnparsedRecordData, TTL}; + use super::{RClass, RType, Record, TTL, UnparsedRecordData}; use crate::new::base::{ name::Name, @@ -958,4 +1049,36 @@ mod test { ); } } + + #[test] + fn test_rclass_from() { + let rclass: RClass = 1.into(); + assert_eq!(rclass, RClass::IN); + + let number: u16 = rclass.into(); + assert_eq!(number, 1); + } + + #[test] + fn test_rtype_from() { + let rtype: RType = 6.into(); + assert_eq!(rtype, RType::SOA); + + let number: u16 = rtype.into(); + assert_eq!(number, 6); + } + + #[test] + fn test_rclass_display() { + assert_eq!("IN", format!("{}", RClass::IN)); + assert_eq!("CH", format!("{}", RClass::CH)); + assert_eq!("CLASS42", format!("{}", RClass::from(42))); + } + + #[test] + fn test_rtype_display() { + assert_eq!("A", format!("{}", RType::A)); + assert_eq!("MX", format!("{}", RType::MX)); + assert_eq!("TYPE265", format!("{}", RType::from(265))); + } } diff --git a/src/new/base/serial.rs b/src/new/base/serial.rs index c9037bd6..e6f81d56 100644 --- a/src/new/base/serial.rs +++ b/src/new/base/serial.rs @@ -13,7 +13,16 @@ use super::wire::U32; //----------- Serial --------------------------------------------------------- -/// A serial number. +/// Serial number arithmetic. +/// +/// [`Serial`] implements the "Serial number arithmetic" defined in [RFC1982] +/// with a `SERIAL_BITS` value of 32. +/// +/// [`Serial`] should not be used interchangably or be confused with the SOA +/// Serial Number, which is a [`Serial`] but not the sole user of "Serial +/// number arithmetic". +/// +/// [RFC1982]: https://datatracker.ietf.org/doc/html/rfc1982 #[derive( Copy, Clone, @@ -30,11 +39,23 @@ use super::wire::U32; UnsizedCopy, )] #[repr(transparent)] -pub struct Serial(U32); +pub struct Serial(pub U32); //--- Construction impl Serial { + /// Construct a new [`Serial`] + #[must_use] + pub const fn new(value: u32) -> Self { + Serial(U32::new(value)) + } + + /// Get [`u32`] value of [`Serial`] + #[must_use] + pub const fn get(&self) -> u32 { + self.0.get() + } + /// Measure the current time (in seconds) in serial number space. #[cfg(feature = "std")] pub fn unix_time() -> Self { @@ -56,9 +77,15 @@ impl Serial { /// instead of a [`u32`] because it is easier to understand and implement /// a non-negative check versus the upper range check. /// + /// Section 7 in [RFC1982] states, in particular for the SOA Serial + /// Number, but for any Serial number using a "SERIAL_BITS" value of 32: + /// "The maximum defined increment is 2147483647 (2^31 - 1)." + /// /// # Panics /// /// Panics if the number is negative. + /// + /// [RFC1982]: https://datatracker.ietf.org/doc/html/rfc1982#section-7 pub fn inc(self, num: i32) -> Self { assert!(num >= 0, "Cannot subtract from a `Serial`"); self.0.get().wrapping_add_signed(num).into() @@ -68,6 +95,10 @@ impl Serial { //--- Ordering impl PartialOrd for Serial { + /// The comparison of Serial Number values is defined in Section 3.2 + /// [RFC1982]. + /// + /// [RFC1982]: https://datatracker.ietf.org/doc/html/rfc1982#section-3.2 fn partial_cmp(&self, other: &Self) -> Option { let (lhs, rhs) = (self.0.get(), other.0.get()); diff --git a/src/new/base/wire/ints.rs b/src/new/base/wire/ints.rs index 8b05b6cb..86409312 100644 --- a/src/new/base/wire/ints.rs +++ b/src/new/base/wire/ints.rs @@ -52,8 +52,7 @@ macro_rules! define_int { } /// Overwrite this value with an integer. - // TODO: Make 'const' at MSRV 1.83.0. - pub fn set(&mut self, value: $base) { + pub const fn set(&mut self, value: $base) { *self = Self::new(value) } }