1mod remote;
2#[cfg(test)]
3mod tests;
4
5use std::{borrow::Cow, collections::BTreeMap, iter::once, mem::replace, sync::Arc};
6
7use futures::{
8 Stream, StreamExt, TryStreamExt,
9 future::{Either, join},
10 stream::empty,
11};
12use ruma::{
13 MxcUri, OwnedMxcUri, OwnedRoomId, OwnedUserId, RoomId, UserId,
14 api::federation::query::get_profile_information,
15 events::room::member::{MembershipState, RoomMemberEventContent},
16 profile::{ProfileFieldName, ProfileFieldValue},
17};
18use serde::Deserialize;
19use serde_json::Value;
20use tuwunel_core::{
21 Err, Result, err, extract_variant, implement,
22 matrix::PduBuilder,
23 smallvec::SmallVec,
24 utils::{
25 MutexMap, MutexMapGuard, ReadyExt,
26 future::TryExtExt,
27 result::NotFound,
28 stream::{IterStream, TryIgnore, TryReadyExt, automatic_width},
29 },
30 warn,
31};
32use tuwunel_database::{
33 Deserialized, Ignore, Interfix, Json, KeyVal, Map, Txn, deserialize_from_slice, serialize_key,
34};
35
36type ProfileLock = MutexMapGuard<OwnedUserId, ()>;
37
38pub struct Service {
39 mutex: MutexMap<OwnedUserId, ()>,
40 services: Arc<crate::services::OnceServices>,
41 profilechangeid_userid: Arc<Map>,
42 useridprofilekey_value: Arc<Map>,
43}
44
45impl crate::Service for Service {
46 fn build(args: &crate::Args<'_>) -> Result<Arc<Self>> {
47 Ok(Arc::new(Self {
48 mutex: MutexMap::new(),
49 services: args.services.clone(),
50 profilechangeid_userid: args.db["profilechangeid_userid"].clone(),
51 useridprofilekey_value: args.db["useridprofilekey_value"].clone(),
52 }))
53 }
54
55 fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
56}
57
58pub type ProfileChange<'a> = (&'a UserId, &'a str);
64
65type ChangeKeyVal<'a> = KeyVal<'a, (&'a str, u64, &'a str), &'a UserId>;
69
70type ProfileValue = (ProfileFieldName, Option<Value>);
73
74type ChangedFields<'a> = SmallVec<[&'a str; 1]>;
77
78type ClearedFields = SmallVec<[ProfileFieldName; 2]>;
83
84type ProspectiveProfile<'a> = BTreeMap<ProfileFieldName, Cow<'a, Value>>;
86
87const MAX_STATUS_TEXT_LENGTH: usize = 256;
89
90const MAX_STATUS_EMOJI_LENGTH: usize = 32;
92
93#[derive(Copy, Clone, Debug, Eq, PartialEq)]
97pub enum Propagation {
98 All,
100
101 Unchanged,
105
106 None,
108}
109
110#[implement(Service)]
111pub async fn update_all_rooms(
112 &self,
113 user_id: &UserId,
114 profile_values: &[(ProfileFieldName, Option<Value>)],
115 propagation: Propagation,
116) {
117 if matches!(propagation, Propagation::None) {
118 return;
119 }
120
121 if !profile_values.iter().any(|(name, _)| {
122 matches!(name, ProfileFieldName::DisplayName | ProfileFieldName::AvatarUrl)
123 }) {
124 return;
125 }
126
127 if self.services.users.is_suspended(user_id).await {
130 return;
131 }
132
133 let (current_displayname, current_avatar_url) =
134 if matches!(propagation, Propagation::Unchanged) {
135 join(self.displayname(user_id).ok(), self.avatar_url(user_id).ok()).await
136 } else {
137 (None, None)
138 };
139
140 let rooms: Vec<OwnedRoomId> = self
141 .services
142 .state_cache
143 .rooms_joined(user_id)
144 .map(Into::into)
145 .collect()
146 .await;
147
148 rooms
149 .iter()
150 .stream()
151 .for_each_concurrent(automatic_width(), async |room_id| {
152 if let Err(e) = self
153 .update_room(
154 user_id,
155 room_id,
156 profile_values,
157 propagation,
158 current_displayname.as_deref(),
159 current_avatar_url.as_deref(),
160 )
161 .await
162 {
163 warn!(
164 %user_id,
165 %room_id,
166 %e,
167 "Failed to update room profile",
168 );
169 }
170 })
171 .await;
172}
173
174#[implement(Service)]
175async fn update_room(
176 &self,
177 user_id: &UserId,
178 room_id: &RoomId,
179 profile_values: &[(ProfileFieldName, Option<Value>)],
180 propagation: Propagation,
181 current_displayname: Option<&str>,
182 current_avatar_url: Option<&MxcUri>,
183) -> Result {
184 let unchanged = match propagation {
185 | Propagation::All => false,
186 | Propagation::Unchanged => true,
187 | Propagation::None => return Ok(()),
188 };
189
190 let state_lock = self.services.state.mutex.lock(room_id).await;
193
194 let content = self
195 .services
196 .state_accessor
197 .get_member(room_id, user_id)
198 .await?;
199
200 if !matches!(content.membership, MembershipState::Join) {
201 return Ok(());
202 }
203
204 let Some(content) =
205 apply_fields(content, profile_values, unchanged, current_displayname, current_avatar_url)
206 else {
207 return Ok(());
208 };
209
210 self.services
211 .timeline
212 .build_and_append_pdu(
213 PduBuilder::state(user_id.as_str(), &content),
214 user_id,
215 room_id,
216 &state_lock,
217 )
218 .await?;
219
220 Ok(())
221}
222
223fn apply_fields(
230 content: RoomMemberEventContent,
231 profile_values: &[ProfileValue],
232 unchanged: bool,
233 current_displayname: Option<&str>,
234 current_avatar_url: Option<&MxcUri>,
235) -> Option<RoomMemberEventContent> {
236 let mut content = RoomMemberEventContent { reason: None, ..content };
237 let mut changed = false;
238
239 for (name, value) in profile_values {
240 match name {
241 | ProfileFieldName::DisplayName
242 if !unchanged || content.displayname.as_deref() == current_displayname =>
243 {
244 let displayname = value.clone().map(|value| {
245 extract_variant!(value, Value::String).expect("invalid profile value type")
246 });
247
248 changed |= assign(&mut content.displayname, displayname);
249 },
250 | ProfileFieldName::AvatarUrl
251 if !unchanged || content.avatar_url.as_deref() == current_avatar_url =>
252 {
253 let avatar_url = value.clone().map(|value| {
254 serde_json::from_value(value).expect("invalid profile value type")
255 });
256
257 changed |= assign(&mut content.avatar_url, avatar_url);
258 },
259 | _ => {},
260 }
261 }
262
263 changed.then_some(content)
264}
265
266fn assign<T: PartialEq>(slot: &mut Option<T>, next: Option<T>) -> bool {
267 replace(slot, next).ne(slot)
268}
269
270#[implement(Service)]
273pub async fn set_displayname(
274 &self,
275 user_id: &UserId,
276 displayname: Option<&str>,
277 propagation: Option<Propagation>,
278) -> Result {
279 self.set_profile_keys(
280 user_id,
281 &[(
282 ProfileFieldName::DisplayName,
283 displayname.map(|displayname| {
284 serde_json::to_value(displayname).expect("displayname serialization cannot fail")
285 }),
286 )],
287 propagation,
288 )
289 .await
290}
291
292#[implement(Service)]
294pub async fn displayname(&self, user_id: &UserId) -> Result<String> {
295 self.profile_key(user_id, &ProfileFieldName::DisplayName)
296 .await
297}
298
299#[implement(Service)]
301pub async fn set_avatar_url(
302 &self,
303 user_id: &UserId,
304 avatar_url: Option<&MxcUri>,
305 propagation: Option<Propagation>,
306) -> Result {
307 self.set_profile_keys(
308 user_id,
309 &[(
310 ProfileFieldName::AvatarUrl,
311 avatar_url.map(|avatar_url| {
312 serde_json::to_value(avatar_url).expect("avatar url serialization cannot fail")
313 }),
314 )],
315 propagation,
316 )
317 .await
318}
319
320#[implement(Service)]
322pub async fn avatar_url(&self, user_id: &UserId) -> Result<OwnedMxcUri> {
323 self.profile_key(user_id, &ProfileFieldName::AvatarUrl)
324 .await
325}
326
327#[implement(Service)]
329pub async fn set_timezone(
330 &self,
331 user_id: &UserId,
332 timezone: Option<&str>,
333 propagation: Option<Propagation>,
334) -> Result {
335 self.set_profile_keys(
336 user_id,
337 &[(
338 ProfileFieldName::TimeZone,
339 timezone.map(|timezone| {
340 serde_json::to_value(timezone).expect("timezone serialization cannot fail")
341 }),
342 )],
343 propagation,
344 )
345 .await
346}
347
348#[implement(Service)]
350pub async fn timezone(&self, user_id: &UserId) -> Result<String> {
351 self.profile_key(user_id, &ProfileFieldName::TimeZone)
352 .await
353}
354
355#[implement(Service)]
360pub fn all_profile_keys(&self, user_id: &UserId) -> impl Stream<Item = ProfileFieldValue> + Send {
361 self.try_all_profile_keys(user_id).ignore_err()
362}
363
364#[implement(Service)]
370pub fn try_all_profile_keys(
371 &self,
372 user_id: &UserId,
373) -> impl Stream<Item = Result<ProfileFieldValue>> + Send {
374 let prefix = (user_id, Interfix);
375
376 self.useridprofilekey_value
377 .stream_prefix(&prefix)
378 .ready_and_then(move |((_, key), Json(val)): ((Ignore, &str), Json<Value>)| {
379 ProfileFieldValue::new(key, val).map_err(|_| {
380 err!(Database(error!(%user_id, %key, "Invalid json in database profile value")))
381 })
382 })
383}
384
385#[implement(Service)]
390pub fn profile_field_names(
391 &self,
392 user_id: &UserId,
393) -> impl Stream<Item = ProfileFieldName> + Send {
394 self.try_profile_field_names(user_id).ignore_err()
395}
396
397#[implement(Service)]
401pub fn try_profile_field_names(
402 &self,
403 user_id: &UserId,
404) -> impl Stream<Item = Result<ProfileFieldName>> + Send {
405 let prefix = (user_id, Interfix);
406
407 self.useridprofilekey_value
408 .keys_prefix(&prefix)
409 .map_ok(|(_, name): (Ignore, &str)| name.into())
410}
411
412#[implement(Service)]
418pub async fn clear_profile_keys(&self, user_id: &UserId) -> Result {
419 let _profile_lock = self.mutex.lock(user_id).await;
420
421 let prefix = (user_id, Interfix);
422 let fields: ClearedFields = self
423 .useridprofilekey_value
424 .keys_prefix(&prefix)
425 .map_ok(|(_, field): (Ignore, &str)| field.into())
426 .try_collect()
427 .await?;
428
429 if self.services.globals.user_is_local(user_id) {
430 self.update_all_rooms(
431 user_id,
432 &[(ProfileFieldName::DisplayName, None), (ProfileFieldName::AvatarUrl, None)],
433 Propagation::All,
434 )
435 .await;
436 }
437
438 let txn = fields
439 .iter()
440 .fold(self.services.db.txn(), |mut txn, field| {
441 txn.del(&self.useridprofilekey_value, (user_id, field.as_str()));
442 txn
443 });
444
445 let rooms = || {
446 self.services
447 .state_cache
448 .rooms_joined_checked(user_id)
449 };
450
451 self.publish_update(user_id, &fields, &fields, txn, rooms)
452 .await
453}
454
455#[implement(Service)]
461pub async fn set_profile_keys(
462 &self,
463 user_id: &UserId,
464 profile_values: &[(ProfileFieldName, Option<Value>)],
465 propagation: Option<Propagation>,
466) -> Result {
467 let profile_lock = self.mutex.lock(user_id).await;
468
469 self.set_profile_keys_locked(&profile_lock, user_id, profile_values, propagation)
470 .await
471}
472
473#[implement(Service)]
478async fn set_profile_keys_locked(
479 &self,
480 _profile_lock: &ProfileLock,
481 user_id: &UserId,
482 profile_values: &[(ProfileFieldName, Option<Value>)],
483 propagation: Option<Propagation>,
484) -> Result {
485 let local = self.services.globals.user_is_local(user_id);
486
487 if local {
488 for (name, value) in profile_values {
489 check_profile_key(name.as_str())?;
490
491 if let Some(value) = value {
492 check_profile_value(name.as_str(), value)?;
493 self.enforce_profile_size(user_id, name.as_str(), value)
494 .await?;
495 }
496 }
497 }
498
499 let changed = self
500 .changed_fields(user_id, profile_values)
501 .await?;
502
503 let propagation = propagation.unwrap_or(
504 if self
505 .services
506 .config
507 .preserve_room_profile_overrides
508 {
509 Propagation::Unchanged
510 } else {
511 Propagation::All
512 },
513 );
514
515 if !matches!(propagation, Propagation::None) && local {
516 self.update_all_rooms(user_id, profile_values, propagation)
517 .await;
518 }
519
520 let txn = profile_values
521 .iter()
522 .fold(self.services.db.txn(), |mut txn, (name, value)| {
523 let key = (user_id, name.as_str());
524
525 if let Some(value) = value {
526 txn.put(&self.useridprofilekey_value, key, Json(value));
527 } else {
528 txn.del(&self.useridprofilekey_value, key);
529 }
530
531 txn
532 });
533
534 let rooms = || {
535 self.services
536 .state_cache
537 .rooms_joined_checked(user_id)
538 };
539
540 let logged = profile_values
542 .iter()
543 .map(|(name, _)| name.as_str())
544 .filter(|name| local || changed.contains(name));
545
546 self.publish_update(user_id, logged, &changed, txn, rooms)
547 .await
548}
549
550#[implement(Service)]
558async fn changed_fields<'a>(
559 &self,
560 user_id: &UserId,
561 profile_values: &'a [(ProfileFieldName, Option<Value>)],
562) -> Result<ChangedFields<'a>> {
563 profile_values
564 .iter()
565 .try_stream()
566 .try_filter_map(async |(name, value)| {
567 let stored = self.profile_key(user_id, name).await.optional()?;
568
569 let changed = stored
570 .as_ref()
571 .ne(&value.as_ref())
572 .then_some(name.as_str());
573
574 Ok(changed)
575 })
576 .try_collect()
577 .await
578}
579
580#[implement(Service)]
589#[tracing::instrument(
590 name = "profile_update",
591 level = "debug",
592 skip_all,
593 fields(
594 %user_id,
595 ),
596)]
597async fn publish_update<'a, L, T, S>(
598 &self,
599 user_id: &UserId,
600 logged: L,
601 changed: &[T],
602 txn: Txn,
603 rooms: impl FnOnce() -> S + Send,
604) -> Result
605where
606 L: IntoIterator<Item: AsRef<str>> + Clone,
607 T: AsRef<str> + Sync,
608 S: Stream<Item = Result<&'a RoomId>> + Send,
609{
610 if logged.clone().into_iter().next().is_none() {
611 txn.execute();
612 return Ok(());
613 }
614
615 let count = self.services.globals.next_count();
616 let txn = self.stage_update(txn, user_id.as_str(), user_id, *count, logged);
617 let txn = if changed.is_empty() {
618 txn
619 } else {
620 rooms()
621 .ready_try_fold(txn, |txn, room_id| {
622 Ok(self.stage_update(txn, room_id.as_str(), user_id, *count, changed))
623 })
624 .await?
625 };
626
627 txn.execute();
628
629 Ok(())
630}
631
632#[implement(Service)]
633fn stage_update<I>(&self, txn: Txn, scope: &str, user_id: &UserId, count: u64, fields: I) -> Txn
634where
635 I: IntoIterator<Item: AsRef<str>>,
636{
637 fields.into_iter().fold(txn, |mut txn, name| {
638 txn.put_raw(&self.profilechangeid_userid, (scope, count, name.as_ref()), user_id);
639 txn
640 })
641}
642
643#[implement(Service)]
649#[inline]
650pub fn profile_changed<'a>(
651 &'a self,
652 user_id: &'a UserId,
653 from: u64,
654 to: Option<u64>,
655) -> impl Stream<Item = ProfileChange<'a>> + Send + 'a {
656 self.try_profile_changed(user_id, from, to)
657 .inspect_err(|error| warn!(%error, "Profile change log row failed to read"))
658 .ignore_err()
659}
660
661#[implement(Service)]
666#[inline]
667pub fn try_profile_changed<'a>(
668 &'a self,
669 user_id: &'a UserId,
670 from: u64,
671 to: Option<u64>,
672) -> impl Stream<Item = Result<ProfileChange<'a>>> + Send + 'a {
673 self.profile_changed_user_or_room(user_id.as_str(), from, to)
674}
675
676#[implement(Service)]
682#[inline]
683pub fn room_profile_changed<'a>(
684 &'a self,
685 room_id: &'a RoomId,
686 from: u64,
687 to: Option<u64>,
688) -> impl Stream<Item = ProfileChange<'a>> + Send + 'a {
689 self.try_room_profile_changed(room_id, from, to)
690 .inspect_err(|error| warn!(%error, "Profile change log row failed to read"))
691 .ignore_err()
692}
693
694#[implement(Service)]
699#[inline]
700pub fn try_room_profile_changed<'a>(
701 &'a self,
702 room_id: &'a RoomId,
703 from: u64,
704 to: Option<u64>,
705) -> impl Stream<Item = Result<ProfileChange<'a>>> + Send + 'a {
706 self.profile_changed_user_or_room(room_id.as_str(), from, to)
707}
708
709#[implement(Service)]
710fn profile_changed_user_or_room<'a>(
711 &'a self,
712 user_or_room_id: &'a str,
713 from: u64,
714 to: Option<u64>,
715) -> impl Stream<Item = Result<ProfileChange<'a>>> + Send + 'a {
716 let to = to.unwrap_or(u64::MAX);
717
718 if from >= to {
719 return Either::Left(empty());
720 }
721
722 let start = (user_or_room_id, from.saturating_add(1));
723 let prefix = serialize_key((user_or_room_id, Interfix)).expect("profile scope prefix");
724 let end = to
725 .checked_add(1)
726 .map(|count| serialize_key((user_or_room_id, count)).expect("profile range end"));
727
728 let changes = self
730 .profilechangeid_userid
731 .stream_from_raw(&start)
732 .ready_try_take_while(move |(key, _)| {
733 Ok(key.starts_with(prefix.as_slice())
734 && end
735 .as_ref()
736 .is_none_or(|end| *key < end.as_slice()))
737 })
738 .ready_and_then(|(key, value)| {
739 let ((_, _, field), user_id): ChangeKeyVal<'_> =
740 (deserialize_from_slice(key)?, deserialize_from_slice(value)?);
741
742 Ok((user_id, field))
743 });
744
745 Either::Right(changes)
746}
747
748#[implement(Service)]
750pub async fn profile_key<T>(&self, user_id: &UserId, profile_key: &ProfileFieldName) -> Result<T>
751where
752 T: for<'de> Deserialize<'de> + Send,
753{
754 let key = (user_id, profile_key);
755 let Json(value) = self
756 .useridprofilekey_value
757 .qry(&key)
758 .await
759 .map_err(|error| {
760 if error.is_not_found() {
761 err!(Request(NotFound("The requested profile key does not exist.")))
762 } else {
763 error
764 }
765 })?
766 .deserialized()
767 .map_err(|_| err!(Database("Cannot deserialize database profile value")))?;
768
769 Ok(value)
770}
771
772#[implement(Service)]
777pub async fn fill_content(
778 &self,
779 user_id: &UserId,
780 mut content: RoomMemberEventContent,
781) -> RoomMemberEventContent {
782 let displayname = self.displayname(user_id).ok();
783 let avatar_url = self.avatar_url(user_id).ok();
784
785 let (displayname, avatar_url) = join(displayname, avatar_url).await;
786
787 content.displayname = displayname;
788 content.avatar_url = avatar_url;
789
790 content
791}
792
793#[implement(Service)]
794pub async fn fetch_remote_profile(&self, user_id: &UserId) -> Result {
795 assert!(
796 !self.services.globals.user_is_local(user_id),
797 "fetch remote profile called with a local user"
798 );
799
800 if let Ok(response) = self
801 .services
802 .federation
803 .execute(user_id.server_name(), get_profile_information::v1::Request {
804 user_id: user_id.to_owned(),
805 field: None,
806 })
807 .await
808 {
809 if !self.services.users.exists(user_id).await {
810 self.services
811 .users
812 .create(user_id, None, None)
813 .await?;
814 }
815
816 for (key, value) in response.iter() {
817 self.set_profile_keys(
818 user_id,
819 &[(key.as_str().into(), Some(value.clone()))],
820 Some(Propagation::None),
821 )
822 .await?;
823 }
824 }
825
826 Ok(())
827}
828
829pub(super) const MAX_PROFILE_SIZE: usize = 65_536;
832
833#[implement(Service)]
839async fn enforce_profile_size(&self, user_id: &UserId, key: &str, value: &Value) -> Result {
840 let replacement = once((ProfileFieldName::from(key), Cow::Borrowed(value))).stream();
841 let profile: ProspectiveProfile<'_> = self
842 .try_all_profile_keys(user_id)
843 .ready_filter_map(|field| {
844 field
845 .inspect_err(|e| warn!(%user_id, %e, "Skipping unreadable profile field"))
846 .ok()
847 })
848 .map(|field| (field.field_name(), Cow::Owned(field.value().into_owned())))
849 .chain(replacement)
850 .collect()
851 .await;
852
853 let profile_size = serde_json::to_vec(&profile).map_or(0, |buf| buf.len());
854
855 if profile_size > MAX_PROFILE_SIZE {
856 return Err!(Request(ProfileTooLarge(
857 "Profile would exceed the maximum size of 64 KiB."
858 )));
859 }
860
861 Ok(())
862}
863
864const MAX_KEY_LENGTH: usize = 255;
866
867fn check_profile_key(name: &str) -> Result {
871 if name.len() > MAX_KEY_LENGTH {
872 return Err!(Request(KeyTooLarge("Profile key names cannot be longer than 255 bytes.")));
873 }
874
875 let ok = name
876 .bytes()
877 .next()
878 .is_some_and(|b| b.is_ascii_lowercase())
879 && name.bytes().all(|b| {
880 b.is_ascii_lowercase() || b.is_ascii_digit() || matches!(b, b'_' | b'.' | b'-')
881 });
882
883 if !ok {
884 return Err!(Request(BadJson(
885 "Profile key names must follow the Common Namespaced Identifier Grammar."
886 )));
887 }
888
889 Ok(())
890}
891
892fn check_profile_value(name: &str, value: &Value) -> Result {
899 if value.is_null() {
900 return Ok(());
901 }
902
903 match name {
904 | "m.status" | "org.matrix.msc4426.status" => check_status(value),
905 | "m.call" | "org.matrix.msc4426.call" => check_call(value),
906 | "m.tz" | "us.cloke.msc4175.tz" => check_timezone(value),
907 | _ => Ok(()),
908 }
909}
910
911fn check_status(value: &Value) -> Result {
919 let (Some(text), Some(emoji)) = (
920 value.get("text").and_then(Value::as_str),
921 value.get("emoji").and_then(Value::as_str),
922 ) else {
923 return Err!(Request(BadJson("Status requires a text and an emoji string.")));
924 };
925
926 check_status_length(text, MAX_STATUS_TEXT_LENGTH, "text")?;
927 check_status_length(emoji, MAX_STATUS_EMOJI_LENGTH, "emoji")
928}
929
930fn check_status_length(value: &str, max: usize, field: &str) -> Result {
936 value.len().le(&max).then_some(()).ok_or_else(|| {
937 err!(RequestStatus(
938 BAD_REQUEST,
939 TooLarge("Status {field} cannot be longer than {max} bytes.")
940 ))
941 })
942}
943
944fn check_call(value: &Value) -> Result {
949 let Some(call) = value.as_object() else {
950 return Err!(Request(BadJson("Call must be an object.")));
951 };
952
953 call.get("call_joined_ts")
954 .is_none_or(Value::is_number)
955 .then_some(())
956 .ok_or_else(|| err!(Request(BadJson("Call join timestamp must be a number."))))
957}
958
959const MAX_TIMEZONE_LENGTH: usize = 64;
964
965const MAX_TIMEZONE_COMPONENTS: usize = 3;
970
971fn check_timezone(value: &Value) -> Result {
980 let Some(name) = value.as_str() else {
981 return Err!(Request(InvalidParam("Time zone must be a string.")));
982 };
983
984 is_timezone_name(name)
985 .then_some(())
986 .ok_or_else(|| {
987 err!(Request(InvalidParam(
988 "Time zone must be a name from the IANA Time Zone Database."
989 )))
990 })
991}
992
993fn is_timezone_name(name: &str) -> bool {
1000 let mut components = name.split('/');
1001
1002 name.len().le(&MAX_TIMEZONE_LENGTH)
1003 && components
1004 .by_ref()
1005 .take(MAX_TIMEZONE_COMPONENTS)
1006 .all(is_timezone_component)
1007 && components.next().is_none()
1008}
1009
1010fn is_timezone_component(component: &str) -> bool {
1015 component
1016 .bytes()
1017 .next()
1018 .is_some_and(|byte| byte.is_ascii_alphabetic())
1019 && component
1020 .bytes()
1021 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'+' | b'-'))
1022}