chore(rust): bump to Rust 1.88 (#9714)

Rust 1.88 has been released and brings with it a quite exciting feature:
let-chains! It allows us to mix-and-match `if` and `let` expressions,
therefore often reducing the "right-drift" of the relevant code, making
it easier to read.

Rust.188 also comes with a new clippy lint that warns when creating a
mutable reference from an immutable pointer. Attempting to fix this
revealed that this is exactly what we are doing in the eBPF kernel.
Unfortunately, it doesn't seem to be possible to design this in a way
that is both accepted by the borrow-checker AND by the eBPF verifier.
Hence, we simply make the function `unsafe` and document for the
programmer, what needs to be upheld.
This commit is contained in:
Thomas Eizinger
2025-07-12 08:42:50 +02:00
committed by GitHub
parent e98aa82e8e
commit d6805d7e48
28 changed files with 149 additions and 120 deletions

View File

@@ -64,7 +64,7 @@ runs:
- name: Install nightly Rust
id: nightly
run: |
NIGHTLY="nightly-2024-12-13"
NIGHTLY="nightly-2025-05-30"
rustup toolchain install $NIGHTLY
rustup component add rust-src --toolchain $NIGHTLY

View File

@@ -5,9 +5,9 @@ fn main() {
let bridges = vec!["src/lib.rs"];
for path in &bridges {
println!("cargo:rerun-if-changed={}", path);
println!("cargo:rerun-if-changed={path}");
}
println!("cargo:rerun-if-env-changed={}", XCODE_CONFIGURATION_ENV);
println!("cargo:rerun-if-env-changed={XCODE_CONFIGURATION_ENV}");
swift_bridge_build::parse_bridges(bridges)
.write_all_concatenated(out_dir, env!("CARGO_PKG_NAME"));

View File

@@ -35,7 +35,7 @@ pub fn logs() -> Option<PathBuf> {
/// Runtime directory for temporary files
pub fn runtime() -> Option<PathBuf> {
let user = std::env::var("USER").ok()?;
Some(PathBuf::from("/tmp").join(format!("{}-{}", BUNDLE_ID, user)))
Some(PathBuf::from("/tmp").join(format!("{BUNDLE_ID}-{user}")))
}
/// User session data directory

View File

@@ -3,8 +3,5 @@ disallowed-methods = [
{ path = "std::collections::HashMap::iter", reason = "HashMap has non-deterministic iteration order, use BTreeMap instead" },
{ path = "std::collections::HashSet::iter", reason = "HashSet has non-deterministic iteration order, use BTreeSet instead" },
{ path = "std::collections::HashMap::iter_mut", reason = "HashMap has non-deterministic iteration order, use BTreeMap instead" },
{ path = "std::collections::HashSet::iter_mut", reason = "HashSet has non-deterministic iteration order, use BTreeSet instead" },
{ path = "std::collections::HashMap::into_iter", reason = "HashMap has non-deterministic iteration order, use BTreeMap instead" },
{ path = "std::collections::HashSet::into_iter", reason = "HashSet has non-deterministic iteration order, use BTreeSet instead" },
{ path = "tracing::subscriber::set_global_default", reason = "Does not init `LogTracer`, use `firezone_logging::init` instead." },
]

View File

@@ -136,20 +136,20 @@ impl Server {
}));
}
if let Some(tcp_v4) = self.tcp_v4.as_mut() {
if let Poll::Ready((stream, from)) = tcp_v4.poll_accept(cx)? {
self.reading_tcp_queries
.push(read_tcp_query(stream, from).boxed());
continue;
}
if let Some(tcp_v4) = self.tcp_v4.as_mut()
&& let Poll::Ready((stream, from)) = tcp_v4.poll_accept(cx)?
{
self.reading_tcp_queries
.push(read_tcp_query(stream, from).boxed());
continue;
}
if let Some(tcp_v6) = self.tcp_v6.as_mut() {
if let Poll::Ready((stream, from)) = tcp_v6.poll_accept(cx)? {
self.reading_tcp_queries
.push(read_tcp_query(stream, from).boxed());
continue;
}
if let Some(tcp_v6) = self.tcp_v6.as_mut()
&& let Poll::Ready((stream, from)) = tcp_v6.poll_accept(cx)?
{
self.reading_tcp_queries
.push(read_tcp_query(stream, from).boxed());
continue;
}
self.waker.register(cx.waker());

View File

@@ -622,13 +622,13 @@ impl ClientState {
return Ok(Ok(()));
};
if let Some(old_gateway_id) = self.resources_gateways.insert(resource_id, gateway_id) {
if self.peers.get(&old_gateway_id).is_some() {
assert_eq!(
old_gateway_id, gateway_id,
"Resources are not expected to change gateways without a previous message, resource_id = {resource_id}"
);
}
if let Some(old_gateway_id) = self.resources_gateways.insert(resource_id, gateway_id)
&& self.peers.get(&old_gateway_id).is_some()
{
assert_eq!(
old_gateway_id, gateway_id,
"Resources are not expected to change gateways without a previous message, resource_id = {resource_id}"
)
}
match self.node.upsert_connection(

View File

@@ -36,11 +36,11 @@ impl NatTable {
Protocol::Icmp(_) => ICMP_TTL,
};
if now.duration_since(*e) >= ttl {
if let Some((inside, _)) = self.table.remove_by_right(outside) {
tracing::debug!(?inside, ?outside, ?ttl, "NAT session expired");
self.expired.insert(*outside);
}
if now.duration_since(*e) >= ttl
&& let Some((inside, _)) = self.table.remove_by_right(outside)
{
tracing::debug!(?inside, ?outside, ?ttl, "NAT session expired");
self.expired.insert(*outside);
}
}
}

View File

@@ -168,8 +168,8 @@ impl ReferenceState {
"private keys must be unique",
|(c, gateways, _, _, _, _, _, _, _)| {
let different_keys = gateways
.iter()
.map(|(_, g)| g.inner().key)
.values()
.map(|g| g.inner().key)
.chain(iter::once(c.inner().key))
.collect::<HashSet<_>>();

View File

@@ -204,20 +204,20 @@ impl SimClient {
}
fn update_sent_requests(&mut self, packet: &IpPacket) {
if let Some(icmp) = packet.as_icmpv4() {
if let Icmpv4Type::EchoRequest(echo) = icmp.icmp_type() {
self.sent_icmp_requests
.insert((Seq(echo.seq), Identifier(echo.id)), packet.clone());
return;
}
if let Some(icmp) = packet.as_icmpv4()
&& let Icmpv4Type::EchoRequest(echo) = icmp.icmp_type()
{
self.sent_icmp_requests
.insert((Seq(echo.seq), Identifier(echo.id)), packet.clone());
return;
}
if let Some(icmp) = packet.as_icmpv6() {
if let Icmpv6Type::EchoRequest(echo) = icmp.icmp_type() {
self.sent_icmp_requests
.insert((Seq(echo.seq), Identifier(echo.id)), packet.clone());
return;
}
if let Some(icmp) = packet.as_icmpv6()
&& let Icmpv6Type::EchoRequest(echo) = icmp.icmp_type()
{
self.sent_icmp_requests
.insert((Seq(echo.seq), Identifier(echo.id)), packet.clone());
return;
}
if let Some(udp) = packet.as_udp() {
@@ -311,20 +311,20 @@ impl SimClient {
return;
}
if let Some(icmp) = packet.as_icmpv4() {
if let Icmpv4Type::EchoReply(echo) = icmp.icmp_type() {
self.received_icmp_replies
.insert((Seq(echo.seq), Identifier(echo.id)), packet.clone());
return;
}
if let Some(icmp) = packet.as_icmpv4()
&& let Icmpv4Type::EchoReply(echo) = icmp.icmp_type()
{
self.received_icmp_replies
.insert((Seq(echo.seq), Identifier(echo.id)), packet.clone());
return;
}
if let Some(icmp) = packet.as_icmpv6() {
if let Icmpv6Type::EchoReply(echo) = icmp.icmp_type() {
self.received_icmp_replies
.insert((Seq(echo.seq), Identifier(echo.id)), packet.clone());
return;
}
if let Some(icmp) = packet.as_icmpv6()
&& let Icmpv6Type::EchoReply(echo) = icmp.icmp_type()
{
self.received_icmp_replies
.insert((Seq(echo.seq), Identifier(echo.id)), packet.clone());
return;
}
tracing::error!(?packet, "Unhandled packet");
@@ -521,12 +521,12 @@ impl RefClient {
continue;
}
if let Some(overlapping_resource) = table.exact_match(resource.address) {
if self.is_connected_to_internet_or_cidr(*overlapping_resource) {
tracing::debug!(%overlapping_resource, resource = %resource.id, address = %resource.address, "Already connected to resource with this exact address, retaining existing route");
if let Some(overlapping_resource) = table.exact_match(resource.address)
&& self.is_connected_to_internet_or_cidr(*overlapping_resource)
{
tracing::debug!(%overlapping_resource, resource = %resource.id, address = %resource.address, "Already connected to resource with this exact address, retaining existing route");
continue;
}
continue;
}
tracing::debug!(resource = %resource.id, address = %resource.address, "Adding CIDR route");

View File

@@ -187,24 +187,24 @@ impl SimGateway {
.icmp_error_for_ip(dst_ip)
.map(|icmp_error| icmp_error_reply(&packet, icmp_error).unwrap());
if let Some(icmp) = packet.as_icmpv4() {
if let Icmpv4Type::EchoRequest(echo) = icmp.icmp_type() {
let packet_id = u64::from_be_bytes(*icmp.payload().first_chunk().unwrap());
tracing::debug!(%packet_id, "Received ICMP request");
self.received_icmp_requests
.insert(packet_id, packet.clone());
return self.handle_icmp_request(&packet, echo, icmp.payload(), icmp_error, now);
}
if let Some(icmp) = packet.as_icmpv4()
&& let Icmpv4Type::EchoRequest(echo) = icmp.icmp_type()
{
let packet_id = u64::from_be_bytes(*icmp.payload().first_chunk().unwrap());
tracing::debug!(%packet_id, "Received ICMP request");
self.received_icmp_requests
.insert(packet_id, packet.clone());
return self.handle_icmp_request(&packet, echo, icmp.payload(), icmp_error, now);
}
if let Some(icmp) = packet.as_icmpv6() {
if let Icmpv6Type::EchoRequest(echo) = icmp.icmp_type() {
let packet_id = u64::from_be_bytes(*icmp.payload().first_chunk().unwrap());
tracing::debug!(%packet_id, "Received ICMP request");
self.received_icmp_requests
.insert(packet_id, packet.clone());
return self.handle_icmp_request(&packet, echo, icmp.payload(), icmp_error, now);
}
if let Some(icmp) = packet.as_icmpv6()
&& let Icmpv6Type::EchoRequest(echo) = icmp.icmp_type()
{
let packet_id = u64::from_be_bytes(*icmp.payload().first_chunk().unwrap());
tracing::debug!(%packet_id, "Received ICMP request");
self.received_icmp_requests
.insert(packet_id, packet.clone());
return self.handle_icmp_request(&packet, echo, icmp.payload(), icmp_error, now);
}
if let Some(udp) = packet.as_udp() {

View File

@@ -200,16 +200,16 @@ async fn try_main(cli: Cli, telemetry: &mut Telemetry) -> Result<()> {
}
async fn get_firezone_id(env_id: Option<String>) -> Result<String> {
if let Some(id) = env_id {
if !id.is_empty() {
return Ok(id);
}
if let Some(id) = env_id
&& !id.is_empty()
{
return Ok(id);
}
if let Ok(id) = tokio::fs::read_to_string(ID_PATH).await {
if !id.is_empty() {
return Ok(id);
}
if let Ok(id) = tokio::fs::read_to_string(ID_PATH).await
&& !id.is_empty()
{
return Ok(id);
}
let device_id = device_id::get_or_create_at(Path::new(ID_PATH))?;

View File

@@ -54,8 +54,7 @@ fn try_admx(attr: TokenStream, item: TokenStream) -> syn::Result<proc_macro2::To
Err(syn::Error::new(
span,
format!(
"No supported type element found for policy '{}'",
value_name
"No supported type element found for policy '{value_name}'"
),
))
})?;

View File

@@ -361,7 +361,7 @@ mod tests {
// e.g. `\\\\?\\C:\\cygwin64\\home\\User\\projects\\firezone\\rust\\target\\debug\\deps\\firezone_windows_client-5f44800b2dafef90.exe`
let path = tauri_utils::platform::current_exe()?.display().to_string();
assert!(path.contains("target"));
assert!(!path.contains('\"'), "`{}`", path);
assert!(!path.contains('\"'), "`{path}`");
Ok(())
}
}

View File

@@ -29,14 +29,14 @@ fn set_registry_values(id: &str, exe: &str) -> Result<(), io::Error> {
let base = Path::new("Software").join("Classes").join(FZ_SCHEME);
let (key, _) = hkcu.create_subkey(&base)?;
key.set_value("", &format!("URL:{}", id))?;
key.set_value("", &format!("URL:{id}"))?;
key.set_value("URL Protocol", &"")?;
let (icon, _) = hkcu.create_subkey(base.join("DefaultIcon"))?;
icon.set_value("", &format!("{},0", &exe))?;
icon.set_value("", &format!("{exe},0"))?;
let (cmd, _) = hkcu.create_subkey(base.join("shell").join("open").join("command"))?;
cmd.set_value("", &format!("{} open-deep-link \"%1\"", &exe))?;
cmd.set_value("", &format!("{exe} open-deep-link \"%1\""))?;
Ok(())
}

View File

@@ -169,7 +169,7 @@ fn ipc_path(id: SocketId) -> String {
/// Public because the GUI Client reuses this for deep links. Eventually that code
/// will be de-duped into this code.
pub fn named_pipe_path(id: &str) -> String {
format!(r"\\.\pipe\{}", id)
format!(r"\\.\pipe\{id}")
}
#[cfg(test)]

View File

@@ -15,7 +15,7 @@ impl fmt::Display for ErrorWithSources<'_> {
write!(f, "{}", self.e)?;
for cause in anyhow::Chain::new(self.e).skip(1) {
write!(f, ": {}", cause)?;
write!(f, ": {cause}")?;
}
Ok(())

View File

@@ -109,7 +109,7 @@ where
if self.level {
let fmt_level = FmtLevel::new(meta.level(), writer.has_ansi_escapes());
write!(writer, "{} ", fmt_level)?;
write!(writer, "{fmt_level} ")?;
}
let dimmed = if writer.has_ansi_escapes() {

View File

@@ -10,9 +10,14 @@ pub struct ChannelData<'a> {
}
impl<'a> ChannelData<'a> {
/// # SAFETY
///
/// You must not create multiple [`ChannelData`] structs at same time.
#[inline(always)]
pub fn parse(ctx: &'a XdpContext, ip_header_length: usize) -> Result<Self, Error> {
let hdr = ref_mut_at::<CdHdr>(ctx, EthHdr::LEN + ip_header_length + UdpHdr::LEN)?;
pub unsafe fn parse(ctx: &'a XdpContext, ip_header_length: usize) -> Result<Self, Error> {
// Safety: We are forwarding the constraint.
let hdr =
unsafe { ref_mut_at::<CdHdr>(ctx, EthHdr::LEN + ip_header_length + UdpHdr::LEN) }?;
if !(0x4000..0x4FFF).contains(&u16::from_be_bytes(hdr.number)) {
return Err(Error::NotAChannelDataMessage);

View File

@@ -15,10 +15,14 @@ pub struct Eth<'a> {
}
impl<'a> Eth<'a> {
/// # SAFETY
///
/// You must not create multiple [`Eth`] structs at same time.
#[inline(always)]
pub fn parse(ctx: &'a XdpContext) -> Result<Self, Error> {
pub unsafe fn parse(ctx: &'a XdpContext) -> Result<Self, Error> {
Ok(Self {
inner: ref_mut_at::<EthHdr>(ctx, 0)?,
// Safety: We are forwarding the constraint.
inner: unsafe { ref_mut_at::<EthHdr>(ctx, 0) }?,
ctx,
})
}

View File

@@ -15,9 +15,13 @@ pub struct Ip4<'a> {
}
impl<'a> Ip4<'a> {
/// # SAFETY
///
/// You must not create multiple [`Ip4`] structs at same time.
#[inline(always)]
pub fn parse(ctx: &'a XdpContext) -> Result<Self, Error> {
let ip4_hdr = ref_mut_at::<Ipv4Hdr>(ctx, EthHdr::LEN)?;
pub unsafe fn parse(ctx: &'a XdpContext) -> Result<Self, Error> {
// Safety: We are forwarding the constraint.
let ip4_hdr = unsafe { ref_mut_at::<Ipv4Hdr>(ctx, EthHdr::LEN) }?;
// IPv4 packets with options are handled in user-space.
if usize::from(ip4_hdr.ihl()) * 4 != Ipv4Hdr::LEN {

View File

@@ -15,11 +15,15 @@ pub struct Ip6<'a> {
}
impl<'a> Ip6<'a> {
/// # SAFETY
///
/// You must not create multiple [`Ip6`] structs at same time.
#[inline(always)]
pub fn parse(ctx: &'a XdpContext) -> Result<Self, Error> {
pub unsafe fn parse(ctx: &'a XdpContext) -> Result<Self, Error> {
Ok(Self {
ctx,
inner: ref_mut_at::<Ipv6Hdr>(ctx, EthHdr::LEN)?,
// Safety: We are forwarding the constraint.
inner: unsafe { ref_mut_at::<Ipv6Hdr>(ctx, EthHdr::LEN) }?,
})
}

View File

@@ -109,7 +109,8 @@ pub fn handle_turn(ctx: XdpContext) -> u32 {
#[inline(always)]
fn try_handle_turn(ctx: &XdpContext) -> Result<u32, Error> {
let eth = Eth::parse(ctx)?;
// Safety: This is the only instance of `Eth`.
let eth = unsafe { Eth::parse(ctx) }?;
match eth.ether_type() {
EtherType::Ipv4 => try_handle_turn_ipv4(ctx, eth)?,
@@ -124,7 +125,8 @@ fn try_handle_turn(ctx: &XdpContext) -> Result<u32, Error> {
#[inline(always)]
fn try_handle_turn_ipv4(ctx: &XdpContext, eth: Eth) -> Result<(), Error> {
let ipv4 = Ip4::parse(ctx)?;
// Safety: This is the only instance of `Ip4`.
let ipv4 = unsafe { Ip4::parse(ctx) }?;
eth::save_mac_for_ipv4(ipv4.src(), eth.src());
eth::save_mac_for_ipv4(ipv4.dst(), eth.dst());
@@ -133,7 +135,8 @@ fn try_handle_turn_ipv4(ctx: &XdpContext, eth: Eth) -> Result<(), Error> {
return Err(Error::NotUdp);
}
let udp = Udp::parse(ctx, Ipv4Hdr::LEN)?; // TODO: Change the API so we parse the UDP header _from_ the ipv4 struct?
// Safety: This is the only instance of `Udp` in this scope.
let udp = unsafe { Udp::parse(ctx, Ipv4Hdr::LEN) }?; // TODO: Change the API so we parse the UDP header _from_ the ipv4 struct?
let udp_payload_len = udp.payload_len();
trace!(
@@ -170,7 +173,8 @@ fn try_handle_ipv4_channel_data_to_udp(
ipv4: Ip4,
udp: Udp,
) -> Result<(), Error> {
let cd = ChannelData::parse(ctx, Ipv4Hdr::LEN)?;
// Safety: This is the only instance of `Udp` in this scope.
let cd = unsafe { ChannelData::parse(ctx, Ipv4Hdr::LEN) }?;
let key = ClientAndChannelV4::new(ipv4.src(), udp.src(), cd.number());
@@ -259,7 +263,8 @@ fn try_handle_ipv4_udp_to_channel_data(
#[inline(always)]
fn try_handle_turn_ipv6(ctx: &XdpContext, eth: Eth) -> Result<(), Error> {
let ipv6 = Ip6::parse(ctx)?;
// Safety: This is the only instance of `Ip6` in this scope.
let ipv6 = unsafe { Ip6::parse(ctx) }?;
eth::save_mac_for_ipv6(ipv6.src(), eth.src());
eth::save_mac_for_ipv6(ipv6.dst(), eth.dst());
@@ -268,7 +273,8 @@ fn try_handle_turn_ipv6(ctx: &XdpContext, eth: Eth) -> Result<(), Error> {
return Err(Error::NotUdp);
}
let udp = Udp::parse(ctx, Ipv6Hdr::LEN)?; // TODO: Change the API so we parse the UDP header _from_ the ipv6 struct?
// Safety: This is the only instance of `Udp` in this scope.
let udp = unsafe { Udp::parse(ctx, Ipv6Hdr::LEN) }?; // TODO: Change the API so we parse the UDP header _from_ the ipv6 struct?
let udp_payload_len = udp.payload_len();
trace!(
@@ -354,7 +360,8 @@ fn try_handle_ipv6_channel_data_to_udp(
ipv6: Ip6,
udp: Udp,
) -> Result<(), Error> {
let cd = ChannelData::parse(ctx, Ipv6Hdr::LEN)?;
// Safety: This is the only instance of `ChannelData` in this scope.
let cd = unsafe { ChannelData::parse(ctx, Ipv6Hdr::LEN) }?;
let key = ClientAndChannelV6::new(ipv6.src(), udp.src(), cd.number());

View File

@@ -6,8 +6,13 @@ use crate::error::Error;
///
/// The length is based on the size of `T` and the bytes at the specified offset will simply be cast into `T`.
/// `T` should therefore most definitely be `repr(C)`.
///
/// # SAFETY
///
/// You must not obtain overlapping mutable references from the context.
#[inline(always)]
pub(crate) fn ref_mut_at<T>(ctx: &XdpContext, offset: usize) -> Result<&mut T, Error> {
#[expect(clippy::mut_from_ref, reason = "The function is unsafe.")]
pub(crate) unsafe fn ref_mut_at<T>(ctx: &XdpContext, offset: usize) -> Result<&mut T, Error> {
let start = ctx.data();
let end = ctx.data_end();
let len = core::mem::size_of::<T>();

View File

@@ -10,11 +10,15 @@ pub struct Udp<'a> {
}
impl<'a> Udp<'a> {
/// # SAFETY
///
/// You must not create multiple [`Udp`] structs at same time.
#[inline(always)]
pub fn parse(ctx: &'a XdpContext, ip_header_length: usize) -> Result<Self, Error> {
pub unsafe fn parse(ctx: &'a XdpContext, ip_header_length: usize) -> Result<Self, Error> {
Ok(Self {
ctx,
inner: ref_mut_at::<UdpHdr>(ctx, EthHdr::LEN + ip_header_length)?,
// Safety: We are forwarding the constraint.
inner: unsafe { ref_mut_at::<UdpHdr>(ctx, EthHdr::LEN + ip_header_length) }?,
})
}

View File

@@ -14,7 +14,7 @@ fn main() -> anyhow::Result<()> {
aya_build::build_ebpf(
[package],
aya_build::Toolchain::Custom("nightly-2024-12-13"),
aya_build::Toolchain::Custom("nightly-2025-05-30"),
)?;
Ok(())

View File

@@ -80,7 +80,7 @@ impl MessageIntegrityExt for MessageIntegrity {
let password = generate_password(relay_secret, expired, salt);
self.check_long_term_credential(
&Username::new(format!("{}:{}", expiry_unix_timestamp, salt))
&Username::new(format!("{expiry_unix_timestamp}:{salt}"))
.map_err(|_| Error::InvalidUsername)?,
&FIREZONE,
&password,
@@ -109,7 +109,7 @@ impl AuthenticatedMessage {
let (expiry_unix_timestamp, salt) = split_username(username)?;
let expired = systemtime_from_unix(expiry_unix_timestamp);
let username = Username::new(format!("{}:{}", expiry_unix_timestamp, salt))
let username = Username::new(format!("{expiry_unix_timestamp}:{salt}"))
.map_err(|_| Error::InvalidUsername)?;
let password = generate_password(relay_secret, expired, salt);

View File

@@ -753,7 +753,7 @@ impl TestServer {
let Some(actual_output) = self.server.next_command() else {
let msg = match expected_output {
Output::SendMessage((recipient, msg)) => {
format!("to send message {:?} to {recipient}", msg)
format!("to send message {msg:?} to {recipient}")
}
CreateAllocation(port, family) => {
format!("to create allocation on port {port} for address family {family}")
@@ -793,8 +793,8 @@ impl TestServer {
.unwrap();
if expected_bytes != payload {
let expected_message = format!("{:?}", message);
let actual_message = format!("{:?}", sent_message);
let expected_message = format!("{message:?}");
let actual_message = format!("{sent_message:?}");
difference::assert_diff!(&expected_message, &actual_message, "\n", 0);
}

View File

@@ -1,4 +1,4 @@
[toolchain]
channel = "1.87.0"
channel = "1.88.0"
components = ["rust-src", "rust-analyzer", "clippy"]
targets = ["x86_64-unknown-linux-musl"]