Merge branch 'main' into notifications

This commit is contained in:
Simon Fondrie-Teitler
2025-08-09 14:17:22 -04:00
15 changed files with 142 additions and 179 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
name = "rayhunter-daemon" name = "rayhunter-daemon"
version = "0.5.1" version = "0.5.1"
edition = "2024" edition = "2024"
rust-version = "1.88" rust-version = "1.88.0"
[dependencies] [dependencies]
rayhunter = { path = "../lib" } rayhunter = { path = "../lib" }
+4 -5
View File
@@ -96,16 +96,15 @@ impl DiagTask {
/// Stop recording /// Stop recording
async fn stop(&mut self, qmdl_store: &mut RecordingStore) { async fn stop(&mut self, qmdl_store: &mut RecordingStore) {
self.stop_current_recording().await; self.stop_current_recording().await;
if let Some((_, entry)) = qmdl_store.get_current_entry() { if let Some((_, entry)) = qmdl_store.get_current_entry()
if let Err(e) = self && let Err(e) = self
.analysis_sender .analysis_sender
.send(AnalysisCtrlMessage::RecordingFinished( .send(AnalysisCtrlMessage::RecordingFinished(
entry.name.to_string(), entry.name.to_string(),
)) ))
.await .await
{ {
warn!("couldn't send analysis message: {e}"); warn!("couldn't send analysis message: {e}");
}
} }
if let Err(e) = qmdl_store.close_current_entry().await { if let Err(e) = qmdl_store.close_current_entry().await {
error!("couldn't close current entry: {e}"); error!("couldn't close current entry: {e}");
+4 -4
View File
@@ -145,10 +145,10 @@ pub fn update_ui(
// we write the status every second because it may have been overwritten through menu // we write the status every second because it may have been overwritten through menu
// navigation. // navigation.
if display_level != 0 { if display_level != 0
if let Err(e) = tokio::fs::write(OLED_PATH, pixels).await { && let Err(e) = tokio::fs::write(OLED_PATH, pixels).await
error!("failed to write to display: {e}"); {
} error!("failed to write to display: {e}");
} }
tokio::time::sleep(Duration::from_millis(1000)).await; tokio::time::sleep(Duration::from_millis(1000)).await;
+5 -5
View File
@@ -61,11 +61,11 @@ pub fn run_key_input_thread(
// On orbic it was observed that pressing the power button can trigger many successive // On orbic it was observed that pressing the power button can trigger many successive
// events. Drop events that are too close together. // events. Drop events that are too close together.
if let Some(last_time) = last_event_time { if let Some(last_time) = last_event_time
if now.duration_since(last_time) < Duration::from_millis(50) { && now.duration_since(last_time) < Duration::from_millis(50)
last_event_time = Some(now); {
continue; last_event_time = Some(now);
} continue;
} }
last_event_time = Some(now); last_event_time = Some(now);
+14 -16
View File
@@ -174,10 +174,9 @@ pub async fn test_rayhunter(adb_device: &mut ADBUSBDevice) -> Result<()> {
if let Ok(output) = adb_command( if let Ok(output) = adb_command(
adb_device, adb_device,
&["wget", "-O", "-", "http://localhost:8080/index.html"], &["wget", "-O", "-", "http://localhost:8080/index.html"],
) { ) && output.contains("html")
if output.contains("html") { {
return Ok(()); return Ok(());
}
} }
failures += 1; failures += 1;
sleep(Duration::from_secs(3)).await; sleep(Duration::from_secs(3)).await;
@@ -297,14 +296,12 @@ async fn adb_echo_test(mut adb_device: ADBUSBDevice) -> Result<ADBUSBDevice> {
Ok::<(ADBUSBDevice, Vec<u8>), RustADBError>((adb_device, buf)) Ok::<(ADBUSBDevice, Vec<u8>), RustADBError>((adb_device, buf))
}); });
sleep(Duration::from_secs(1)).await; sleep(Duration::from_secs(1)).await;
if thread.is_finished() { if thread.is_finished()
if let Ok(Ok((dev, buf))) = thread.join() { && let Ok(Ok((dev, buf))) = thread.join()
if let Ok(s) = std::str::from_utf8(&buf) { && let Ok(s) = std::str::from_utf8(&buf)
if s.contains(test_echo) { && s.contains(test_echo)
return Ok(dev); {
} return Ok(dev);
}
}
} }
// I'd like to kill the background thread here if that was possible. // I'd like to kill the background thread here if that was possible.
bail!("Could not communicate with the Orbic. Try disconnecting and reconnecting."); bail!("Could not communicate with the Orbic. Try disconnecting and reconnecting.");
@@ -317,10 +314,11 @@ async fn wait_for_usb_device(vendor_id: u16, product_id: u16) -> Result<()> {
loop { loop {
let mut watcher = nusb::watch_devices()?; let mut watcher = nusb::watch_devices()?;
while let Some(event) = watcher.next().await { while let Some(event) = watcher.next().await {
if let HotplugEvent::Connected(dev) = event { if let HotplugEvent::Connected(dev) = event
if dev.vendor_id() == vendor_id && dev.product_id() == product_id { && dev.vendor_id() == vendor_id
return Ok(()); && dev.product_id() == product_id
} {
return Ok(());
} }
} }
} }
+2 -2
View File
@@ -76,14 +76,14 @@ pub struct Event {
/// many hours at a time with dozens of [Analyzers](Analyzer) working in parallel. /// many hours at a time with dozens of [Analyzers](Analyzer) working in parallel.
pub trait Analyzer { pub trait Analyzer {
/// Returns a user-friendly, concise name for your heuristic. /// Returns a user-friendly, concise name for your heuristic.
fn get_name(&self) -> Cow<str>; fn get_name(&self) -> Cow<'_, str>;
/// Returns a user-friendly description of what your heuristic looks for, /// Returns a user-friendly description of what your heuristic looks for,
/// the types of [Events](Event) it may return, as well as possible false-positive /// the types of [Events](Event) it may return, as well as possible false-positive
/// conditions that may trigger an [Event]. If different [Events](Event) have /// conditions that may trigger an [Event]. If different [Events](Event) have
/// different false-positive conditions, consider including them in its /// different false-positive conditions, consider including them in its
/// `message` field. /// `message` field.
fn get_description(&self) -> Cow<str>; fn get_description(&self) -> Cow<'_, str>;
/// Analyze a single [InformationElement], possibly returning an [Event] if your /// Analyze a single [InformationElement], possibly returning an [Event] if your
/// heuristic deems it relevant. Again, be mindful of any state your /// heuristic deems it relevant. Again, be mindful of any state your
@@ -2,7 +2,6 @@ use std::borrow::Cow;
use super::analyzer::{Analyzer, Event, EventType, Severity}; use super::analyzer::{Analyzer, Event, EventType, Severity};
use super::information_element::{InformationElement, LteInformationElement}; use super::information_element::{InformationElement, LteInformationElement};
use super::util::unpack;
use telcom_parser::lte_rrc::{ use telcom_parser::lte_rrc::{
DL_DCCH_MessageType, DL_DCCH_MessageType_c1, RRCConnectionReleaseCriticalExtensions, DL_DCCH_MessageType, DL_DCCH_MessageType_c1, RRCConnectionReleaseCriticalExtensions,
RRCConnectionReleaseCriticalExtensions_c1, RedirectedCarrierInfo, RRCConnectionReleaseCriticalExtensions_c1, RedirectedCarrierInfo,
@@ -14,11 +13,11 @@ pub struct ConnectionRedirect2GDowngradeAnalyzer {}
// TODO: keep track of SIB state to compare LTE reselection blocks w/ 2g/3g ones // TODO: keep track of SIB state to compare LTE reselection blocks w/ 2g/3g ones
impl Analyzer for ConnectionRedirect2GDowngradeAnalyzer { impl Analyzer for ConnectionRedirect2GDowngradeAnalyzer {
fn get_name(&self) -> Cow<str> { fn get_name(&self) -> Cow<'_, str> {
Cow::from("Connection Release/Redirected Carrier 2G Downgrade") Cow::from("Connection Release/Redirected Carrier 2G Downgrade")
} }
fn get_description(&self) -> Cow<str> { fn get_description(&self) -> Cow<'_, str> {
Cow::from("Tests if a cell releases our connection and redirects us to a 2G cell.") Cow::from("Tests if a cell releases our connection and redirects us to a 2G cell.")
} }
@@ -27,27 +26,28 @@ impl Analyzer for ConnectionRedirect2GDowngradeAnalyzer {
} }
fn analyze_information_element(&mut self, ie: &InformationElement) -> Option<Event> { fn analyze_information_element(&mut self, ie: &InformationElement) -> Option<Event> {
unpack!(InformationElement::LTE(lte_ie) = ie); if let InformationElement::LTE(lte_ie) = ie
let message = match &**lte_ie { && let LteInformationElement::DlDcch(msg_cont) = &**lte_ie
LteInformationElement::DlDcch(msg_cont) => &msg_cont.message, && let DL_DCCH_MessageType::C1(c1) = &msg_cont.message
_ => return None, && let DL_DCCH_MessageType_c1::RrcConnectionRelease(release) = c1
}; && let RRCConnectionReleaseCriticalExtensions::C1(c1) = &release.critical_extensions
unpack!(DL_DCCH_MessageType::C1(c1) = message); && let RRCConnectionReleaseCriticalExtensions_c1::RrcConnectionRelease_r8(r8_ies) = c1
unpack!(DL_DCCH_MessageType_c1::RrcConnectionRelease(release) = c1); && let Some(carrier_info) = &r8_ies.redirected_carrier_info
unpack!(RRCConnectionReleaseCriticalExtensions::C1(c1) = &release.critical_extensions); {
unpack!(RRCConnectionReleaseCriticalExtensions_c1::RrcConnectionRelease_r8(r8_ies) = c1); match carrier_info {
unpack!(Some(carrier_info) = &r8_ies.redirected_carrier_info); RedirectedCarrierInfo::Geran(_carrier_freqs_geran) => Some(Event {
match carrier_info { event_type: EventType::QualitativeWarning {
RedirectedCarrierInfo::Geran(_carrier_freqs_geran) => Some(Event { severity: Severity::High,
event_type: EventType::QualitativeWarning { },
severity: Severity::High, message: "Detected 2G downgrade".to_owned(),
}, }),
message: "Detected 2G downgrade".to_owned(), _ => Some(Event {
}), event_type: EventType::Informational,
_ => Some(Event { message: format!("RRCConnectionRelease CarrierInfo: {carrier_info:?}"),
event_type: EventType::Informational, }),
message: format!("RRCConnectionRelease CarrierInfo: {carrier_info:?}"), }
}), } else {
None
} }
} }
} }
+2 -2
View File
@@ -98,11 +98,11 @@ impl ImsiRequestedAnalyzer {
} }
impl Analyzer for ImsiRequestedAnalyzer { impl Analyzer for ImsiRequestedAnalyzer {
fn get_name(&self) -> Cow<str> { fn get_name(&self) -> Cow<'_, str> {
Cow::from("Identity (IMSI or IMEI) requested in suspicious manner") Cow::from("Identity (IMSI or IMEI) requested in suspicious manner")
} }
fn get_description(&self) -> Cow<str> { fn get_description(&self) -> Cow<'_, str> {
Cow::from( Cow::from(
"Tests whether the ME sends an Identity Request NAS message without either an associated attach request or auth accept message", "Tests whether the ME sends an Identity Request NAS message without either an associated attach request or auth accept message",
) )
+8 -10
View File
@@ -2,8 +2,6 @@ use std::borrow::Cow;
use telcom_parser::lte_rrc::{BCCH_DL_SCH_MessageType, BCCH_DL_SCH_MessageType_c1}; use telcom_parser::lte_rrc::{BCCH_DL_SCH_MessageType, BCCH_DL_SCH_MessageType_c1};
use crate::analysis::util::unpack;
use super::analyzer::{Analyzer, Event, EventType, Severity}; use super::analyzer::{Analyzer, Event, EventType, Severity};
use super::information_element::{InformationElement, LteInformationElement}; use super::information_element::{InformationElement, LteInformationElement};
@@ -24,11 +22,11 @@ impl IncompleteSibAnalyzer {
} }
impl Analyzer for IncompleteSibAnalyzer { impl Analyzer for IncompleteSibAnalyzer {
fn get_name(&self) -> Cow<str> { fn get_name(&self) -> Cow<'_, str> {
Cow::from("Incomplete SIB") Cow::from("Incomplete SIB")
} }
fn get_description(&self) -> Cow<str> { fn get_description(&self) -> Cow<'_, str> {
Cow::from("Tests whether a SIB1 message contains a full chain of followup sibs") Cow::from("Tests whether a SIB1 message contains a full chain of followup sibs")
} }
@@ -39,12 +37,12 @@ impl Analyzer for IncompleteSibAnalyzer {
fn analyze_information_element(&mut self, ie: &InformationElement) -> Option<Event> { fn analyze_information_element(&mut self, ie: &InformationElement) -> Option<Event> {
self.packet_num += 1; self.packet_num += 1;
unpack!(InformationElement::LTE(lte_ie) = ie); if let InformationElement::LTE(lte_ie) = ie
unpack!(LteInformationElement::BcchDlSch(sch_msg) = &**lte_ie); && let LteInformationElement::BcchDlSch(sch_msg) = &**lte_ie
unpack!(BCCH_DL_SCH_MessageType::C1(c1) = &sch_msg.message); && let BCCH_DL_SCH_MessageType::C1(c1) = &sch_msg.message
unpack!(BCCH_DL_SCH_MessageType_c1::SystemInformationBlockType1(sib1) = c1); && let BCCH_DL_SCH_MessageType_c1::SystemInformationBlockType1(sib1) = c1
&& sib1.scheduling_info_list.0.len() < 2
if sib1.scheduling_info_list.0.len() < 2 { {
return Some(Event { return Some(Event {
event_type: EventType::QualitativeWarning { event_type: EventType::QualitativeWarning {
severity: Severity::Medium, severity: Severity::Medium,
+14 -14
View File
@@ -24,11 +24,11 @@ impl NasNullCipherAnalyzer {
} }
impl Analyzer for NasNullCipherAnalyzer { impl Analyzer for NasNullCipherAnalyzer {
fn get_name(&self) -> Cow<str> { fn get_name(&self) -> Cow<'_, str> {
Cow::from("NAS Null Cipher Requested") Cow::from("NAS Null Cipher Requested")
} }
fn get_description(&self) -> Cow<str> { fn get_description(&self) -> Cow<'_, str> {
Cow::from( Cow::from(
"Tests whether the MME requests to use a null cipher in the NAS security mode command", "Tests whether the MME requests to use a null cipher in the NAS security mode command",
) )
@@ -48,18 +48,18 @@ impl Analyzer for NasNullCipherAnalyzer {
_ => return None, _ => return None,
}; };
if let NASMessage::EMMMessage(EMMMessage::EMMSecurityModeCommand(req)) = payload { if let NASMessage::EMMMessage(EMMMessage::EMMSecurityModeCommand(req)) = payload
if req.nas_sec_algo.inner.ciph_algo == EPSEncryptionAlgorithmEEA0Null { && req.nas_sec_algo.inner.ciph_algo == EPSEncryptionAlgorithmEEA0Null
return Some(Event { {
event_type: EventType::QualitativeWarning { return Some(Event {
severity: Severity::High, event_type: EventType::QualitativeWarning {
}, severity: Severity::High,
message: format!( },
"NAS Security mode command requested null cipher(packet {})", message: format!(
self.packet_num "NAS Security mode command requested null cipher(packet {})",
), self.packet_num
}); ),
} });
} }
None None
} }
+14 -14
View File
@@ -37,10 +37,10 @@ impl NullCipherAnalyzer {
Some(&rat.security_algorithm_config) Some(&rat.security_algorithm_config)
} }
}; };
if let Some(security_config) = maybe_security_config { if let Some(security_config) = maybe_security_config
if security_config.ciphering_algorithm.0 == CipheringAlgorithm_r12::EEA0 { && security_config.ciphering_algorithm.0 == CipheringAlgorithm_r12::EEA0
return true; {
} return true;
} }
} }
// Use map/flatten to dig into a long chain of nested Option types // Use map/flatten to dig into a long chain of nested Option types
@@ -62,10 +62,10 @@ impl NullCipherAnalyzer {
.as_ref() .as_ref()
.and_then(|scg| scg.mobility_control_info_scg_r12.as_ref()) .and_then(|scg| scg.mobility_control_info_scg_r12.as_ref())
.and_then(|mci| mci.ciphering_algorithm_scg_r12.as_ref()); .and_then(|mci| mci.ciphering_algorithm_scg_r12.as_ref());
if let Some(cipher) = maybe_cipher { if let Some(cipher) = maybe_cipher
if cipher.0 == CipheringAlgorithm_r12::EEA0 { && cipher.0 == CipheringAlgorithm_r12::EEA0
return true; {
} return true;
} }
} }
@@ -90,10 +90,10 @@ impl NullCipherAnalyzer {
Some(&to_5gc.security_algorithm_config_r15) Some(&to_5gc.security_algorithm_config_r15)
} }
}; };
if let Some(security_algorithm) = maybe_security_algorithm { if let Some(security_algorithm) = maybe_security_algorithm
if security_algorithm.ciphering_algorithm.0 == CipheringAlgorithm_r12::EEA0 { && security_algorithm.ciphering_algorithm.0 == CipheringAlgorithm_r12::EEA0
return true; {
} return true;
} }
false false
} }
@@ -119,11 +119,11 @@ impl NullCipherAnalyzer {
} }
impl Analyzer for NullCipherAnalyzer { impl Analyzer for NullCipherAnalyzer {
fn get_name(&self) -> Cow<str> { fn get_name(&self) -> Cow<'_, str> {
Cow::from("Null Cipher") Cow::from("Null Cipher")
} }
fn get_description(&self) -> Cow<str> { fn get_description(&self) -> Cow<'_, str> {
Cow::from("Tests whether the cell suggests using a null cipher (EEA0)") Cow::from("Tests whether the cell suggests using a null cipher (EEA0)")
} }
+37 -37
View File
@@ -16,19 +16,15 @@ impl LteSib6And7DowngradeAnalyzer {
&self, &self,
ie: &'a InformationElement, ie: &'a InformationElement,
) -> Option<&'a SystemInformation_r8_IEsSib_TypeAndInfo> { ) -> Option<&'a SystemInformation_r8_IEsSib_TypeAndInfo> {
if let InformationElement::LTE(lte_ie) = ie { if let InformationElement::LTE(lte_ie) = ie
if let LteInformationElement::BcchDlSch(bcch_dl_sch_message) = &**lte_ie { && let LteInformationElement::BcchDlSch(bcch_dl_sch_message) = &**lte_ie
if let BCCH_DL_SCH_MessageType::C1(BCCH_DL_SCH_MessageType_c1::SystemInformation( && let BCCH_DL_SCH_MessageType::C1(BCCH_DL_SCH_MessageType_c1::SystemInformation(
system_information, system_information,
)) = &bcch_dl_sch_message.message )) = &bcch_dl_sch_message.message
{ && let SystemInformationCriticalExtensions::SystemInformation_r8(sib) =
if let SystemInformationCriticalExtensions::SystemInformation_r8(sib) = &system_information.critical_extensions
&system_information.critical_extensions {
{ return Some(&sib.sib_type_and_info);
return Some(&sib.sib_type_and_info);
}
}
}
} }
None None
} }
@@ -36,11 +32,11 @@ impl LteSib6And7DowngradeAnalyzer {
// TODO: keep track of SIB state to compare LTE reselection blocks w/ 2g/3g ones // TODO: keep track of SIB state to compare LTE reselection blocks w/ 2g/3g ones
impl Analyzer for LteSib6And7DowngradeAnalyzer { impl Analyzer for LteSib6And7DowngradeAnalyzer {
fn get_name(&self) -> Cow<str> { fn get_name(&self) -> Cow<'_, str> {
Cow::from("LTE SIB 6/7 Downgrade") Cow::from("LTE SIB 6/7 Downgrade")
} }
fn get_description(&self) -> Cow<str> { fn get_description(&self) -> Cow<'_, str> {
Cow::from( Cow::from(
"Tests for LTE cells broadcasting a SIB type 6 and 7 which include 2G/3G frequencies with higher priorities.", "Tests for LTE cells broadcasting a SIB type 6 and 7 which include 2G/3G frequencies with higher priorities.",
) )
@@ -62,13 +58,16 @@ impl Analyzer for LteSib6And7DowngradeAnalyzer {
for carrier_info in &carrier_info_list.0 { for carrier_info in &carrier_info_list.0 {
if let Some(CellReselectionPriority(p)) = if let Some(CellReselectionPriority(p)) =
carrier_info.cell_reselection_priority carrier_info.cell_reselection_priority
&& p == 0
{ {
if p == 0 { return Some(Event {
return Some(Event { event_type: EventType::QualitativeWarning {
event_type: EventType::QualitativeWarning { severity: Severity::High }, severity: Severity::High,
message: "LTE cell advertised a 3G cell for priority 0 reselection".to_string(), },
}); message:
} "LTE cell advertised a 3G cell for priority 0 reselection"
.to_string(),
});
} }
} }
} }
@@ -76,13 +75,16 @@ impl Analyzer for LteSib6And7DowngradeAnalyzer {
for carrier_info in &carrier_info_list.0 { for carrier_info in &carrier_info_list.0 {
if let Some(CellReselectionPriority(p)) = if let Some(CellReselectionPriority(p)) =
carrier_info.cell_reselection_priority carrier_info.cell_reselection_priority
&& p == 0
{ {
if p == 0 { return Some(Event {
return Some(Event { event_type: EventType::QualitativeWarning {
event_type: EventType::QualitativeWarning { severity: Severity::High }, severity: Severity::High,
message: "LTE cell advertised a 3G cell for priority 0 reselection".to_string(), },
}); message:
} "LTE cell advertised a 3G cell for priority 0 reselection"
.to_string(),
});
} }
} }
} }
@@ -96,17 +98,15 @@ impl Analyzer for LteSib6And7DowngradeAnalyzer {
for carrier_info in &carrier_info_list.0 { for carrier_info in &carrier_info_list.0 {
if let Some(CellReselectionPriority(p)) = if let Some(CellReselectionPriority(p)) =
carrier_info.common_info.cell_reselection_priority carrier_info.common_info.cell_reselection_priority
&& p == 0
{ {
if p == 0 { return Some(Event {
return Some(Event { event_type: EventType::QualitativeWarning {
event_type: EventType::QualitativeWarning { severity: Severity::High,
severity: Severity::High, },
}, message: "LTE cell advertised a 2G cell for priority 0 reselection"
message: .to_string(),
"LTE cell advertised a 2G cell for priority 0 reselection" });
.to_string(),
});
}
} }
} }
} }
-32
View File
@@ -1,33 +1 @@
// Unpacks a pattern, or returns None.
//
// # Examples
// You can use `unpack!` to unroll highly nested enums like this:
// ```
// enum Foo {
// A(Bar),
// B,
// }
//
// enum Bar {
// C(Baz)
// }
//
// struct Baz;
//
// fn get_bang(foo: Foo) -> Option<Baz> {
// unpack!(Foo::A(bar) = foo);
// unpack!(Bar::C(baz) = bar);
// baz
// }
// ```
//
macro_rules! unpack {
($pat:pat = $val:expr) => {
let $pat = $val else {
return None;
};
};
}
// this is apparently how you make a macro publicly usable from this module
pub(crate) use unpack;
+4 -4
View File
@@ -198,10 +198,10 @@ impl DiagDevice {
return Err(DiagDeviceError::DeviceWriteFailed(err)); return Err(DiagDeviceError::DeviceWriteFailed(err));
} }
} }
if let Err(err) = self.file.flush().await { if let Err(err) = self.file.flush().await
if err.kind() != ErrorKind::WriteZero { && err.kind() != ErrorKind::WriteZero
return Err(DiagDeviceError::DeviceWriteFailed(err)); {
} return Err(DiagDeviceError::DeviceWriteFailed(err));
} }
Ok(()) Ok(())
} }
+9 -9
View File
@@ -77,16 +77,16 @@ where
pub async fn get_next_messages_container( pub async fn get_next_messages_container(
&mut self, &mut self,
) -> Result<Option<MessagesContainer>, std::io::Error> { ) -> Result<Option<MessagesContainer>, std::io::Error> {
if let Some(max_bytes) = self.max_bytes { if let Some(max_bytes) = self.max_bytes
if self.bytes_read >= max_bytes { && self.bytes_read >= max_bytes
if self.bytes_read > max_bytes { {
error!( if self.bytes_read > max_bytes {
"warning: {} bytes read, but max_bytes was {}", error!(
self.bytes_read, max_bytes "warning: {} bytes read, but max_bytes was {}",
); self.bytes_read, max_bytes
} );
return Ok(None);
} }
return Ok(None);
} }
let mut buf = Vec::new(); let mut buf = Vec::new();