diff --git a/aero-dav/src/caldecoder.rs b/aero-dav/src/caldecoder.rs index b6a843f..ff79845 100644 --- a/aero-dav/src/caldecoder.rs +++ b/aero-dav/src/caldecoder.rs @@ -25,7 +25,7 @@ impl QRead> for MkCalendarResponse { } } -impl QRead> for Report { +impl QRead> for ReportType { async fn qread(xml: &mut Reader) -> Result { match CalendarQuery::::qread(xml).await { Err(ParsingError::Recoverable) => (), diff --git a/aero-dav/src/calencoder.rs b/aero-dav/src/calencoder.rs index 723d95d..48c93d0 100644 --- a/aero-dav/src/calencoder.rs +++ b/aero-dav/src/calencoder.rs @@ -33,7 +33,7 @@ impl QWrite for MkCalendarResponse { } // ----------------------- REPORT METHOD ------------------------------------- -impl QWrite for Report { +impl QWrite for ReportType { async fn qwrite(&self, xml: &mut Writer) -> Result<(), QError> { match self { Self::Query(v) => v.qwrite(xml).await, diff --git a/aero-dav/src/caltypes.rs b/aero-dav/src/caltypes.rs index 50cdb92..a763653 100644 --- a/aero-dav/src/caltypes.rs +++ b/aero-dav/src/caltypes.rs @@ -51,7 +51,7 @@ pub struct MkCalendarResponse(pub Vec>); // --- (REPORT PART) --- #[derive(Debug, PartialEq, Clone)] -pub enum Report { +pub enum ReportType { Query(CalendarQuery), Multiget(CalendarMultiget), FreeBusy(FreeBusyQuery), diff --git a/aero-dav/src/lib.rs b/aero-dav/src/lib.rs index 7507ddc..64be929 100644 --- a/aero-dav/src/lib.rs +++ b/aero-dav/src/lib.rs @@ -16,13 +16,20 @@ pub mod caldecoder; pub mod calencoder; pub mod caltypes; -// acl (wip) +// acl (partial) pub mod acldecoder; pub mod aclencoder; pub mod acltypes; -// versioning (wip) -mod versioningtypes; +// versioning (partial) +pub mod versioningdecoder; +pub mod versioningencoder; +pub mod versioningtypes; + +// sync +pub mod syncdecoder; +pub mod syncencoder; +pub mod synctypes; // final type pub mod realization; diff --git a/aero-dav/src/realization.rs b/aero-dav/src/realization.rs index 7283e68..0f3aec4 100644 --- a/aero-dav/src/realization.rs +++ b/aero-dav/src/realization.rs @@ -1,6 +1,7 @@ use super::acltypes as acl; use super::caltypes as cal; use super::error; +use super::synctypes as sync; use super::types as dav; use super::xml; @@ -31,6 +32,7 @@ impl dav::Extension for Core { type Property = Disabled; type PropertyRequest = Disabled; type ResourceType = Disabled; + type ReportType = Disabled; } // WebDAV with the base Calendar implementation (RFC4791) @@ -41,6 +43,7 @@ impl dav::Extension for Calendar { type Property = cal::Property; type PropertyRequest = cal::PropertyRequest; type ResourceType = cal::ResourceType; + type ReportType = cal::ReportType; } // ACL @@ -51,6 +54,7 @@ impl dav::Extension for Acl { type Property = acl::Property; type PropertyRequest = acl::PropertyRequest; type ResourceType = acl::ResourceType; + type ReportType = Disabled; } // All merged @@ -61,6 +65,7 @@ impl dav::Extension for All { type Property = Property; type PropertyRequest = PropertyRequest; type ResourceType = ResourceType; + type ReportType = ReportType; } #[derive(Debug, PartialEq, Clone)] @@ -142,3 +147,31 @@ impl xml::QWrite for ResourceType { } } } + +#[derive(Debug, PartialEq, Clone)] +pub enum ReportType { + Cal(cal::ReportType), + Sync(sync::SyncCollection), +} +impl xml::QRead> for ReportType { + async fn qread( + xml: &mut xml::Reader, + ) -> Result, error::ParsingError> { + match cal::ReportType::qread(xml).await { + Err(error::ParsingError::Recoverable) => (), + otherwise => return otherwise.map(ReportType::Cal), + } + sync::SyncCollection::qread(xml).await.map(ReportType::Sync) + } +} +impl xml::QWrite for ReportType { + async fn qwrite( + &self, + xml: &mut xml::Writer, + ) -> Result<(), quick_xml::Error> { + match self { + Self::Cal(c) => c.qwrite(xml).await, + Self::Sync(s) => s.qwrite(xml).await, + } + } +} diff --git a/aero-dav/src/syncdecoder.rs b/aero-dav/src/syncdecoder.rs new file mode 100644 index 0000000..8e035ab --- /dev/null +++ b/aero-dav/src/syncdecoder.rs @@ -0,0 +1,175 @@ +use quick_xml::events::Event; + +use super::error::ParsingError; +use super::synctypes::*; +use super::types as dav; +use super::xml::{IRead, QRead, Reader, DAV_URN}; + +impl QRead> for SyncCollection { + async fn qread(xml: &mut Reader) -> Result { + xml.open(DAV_URN, "sync-collection").await?; + let (mut sync_token, mut sync_level, mut limit, mut prop) = (None, None, None, None); + loop { + let mut dirty = false; + xml.maybe_read(&mut sync_token, &mut dirty).await?; + xml.maybe_read(&mut sync_level, &mut dirty).await?; + xml.maybe_read(&mut limit, &mut dirty).await?; + xml.maybe_read(&mut prop, &mut dirty).await?; + + if !dirty { + match xml.peek() { + Event::End(_) => break, + _ => xml.skip().await?, + }; + } + } + + xml.close().await?; + match (sync_token, sync_level, prop) { + (Some(sync_token), Some(sync_level), Some(prop)) => Ok(SyncCollection { + sync_token, + sync_level, + limit, + prop, + }), + _ => Err(ParsingError::MissingChild), + } + } +} + +impl QRead for SyncTokenRequest { + async fn qread(xml: &mut Reader) -> Result { + xml.open(DAV_URN, "sync-token").await?; + let token = match xml.tag_string().await { + Ok(v) => SyncTokenRequest::IncrementalSync(v), + Err(ParsingError::Recoverable) => SyncTokenRequest::InitialSync, + Err(e) => return Err(e), + }; + xml.close().await?; + Ok(token) + } +} + +impl QRead for SyncToken { + async fn qread(xml: &mut Reader) -> Result { + xml.open(DAV_URN, "sync-token").await?; + let token = xml.tag_string().await?; + xml.close().await?; + Ok(SyncToken(token)) + } +} + +impl QRead for SyncLevel { + async fn qread(xml: &mut Reader) -> Result { + xml.open(DAV_URN, "sync-level").await?; + let lvl = match xml.tag_string().await?.to_lowercase().as_str() { + "1" => SyncLevel::One, + "infinite" => SyncLevel::Infinite, + _ => return Err(ParsingError::InvalidValue), + }; + xml.close().await?; + Ok(lvl) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::realization::All; + use crate::types as dav; + use crate::versioningtypes as vers; + use crate::xml::Node; + + async fn deserialize>(src: &str) -> T { + let mut rdr = Reader::new(quick_xml::NsReader::from_reader(src.as_bytes())) + .await + .unwrap(); + rdr.find().await.unwrap() + } + + #[tokio::test] + async fn sync_level() { + { + let expected = SyncLevel::One; + let src = r#"1"#; + let got = deserialize::(src).await; + assert_eq!(got, expected); + } + { + let expected = SyncLevel::Infinite; + let src = r#"infinite"#; + let got = deserialize::(src).await; + assert_eq!(got, expected); + } + } + + #[tokio::test] + async fn sync_token_request() { + { + let expected = SyncTokenRequest::InitialSync; + let src = r#""#; + let got = deserialize::(src).await; + assert_eq!(got, expected); + } + { + let expected = + SyncTokenRequest::IncrementalSync("http://example.com/ns/sync/1232".into()); + let src = + r#"http://example.com/ns/sync/1232"#; + let got = deserialize::(src).await; + assert_eq!(got, expected); + } + } + + #[tokio::test] + async fn sync_token() { + let expected = SyncToken("http://example.com/ns/sync/1232".into()); + let src = r#"http://example.com/ns/sync/1232"#; + let got = deserialize::(src).await; + assert_eq!(got, expected); + } + + #[tokio::test] + async fn sync_collection() { + { + let expected = SyncCollection:: { + sync_token: SyncTokenRequest::IncrementalSync( + "http://example.com/ns/sync/1232".into(), + ), + sync_level: SyncLevel::One, + limit: Some(vers::Limit(vers::NResults(100))), + prop: dav::PropName(vec![dav::PropertyRequest::GetEtag]), + }; + let src = r#" + http://example.com/ns/sync/1232 + 1 + + 100 + + + + + "#; + let got = deserialize::>(src).await; + assert_eq!(got, expected); + } + + { + let expected = SyncCollection:: { + sync_token: SyncTokenRequest::InitialSync, + sync_level: SyncLevel::Infinite, + limit: None, + prop: dav::PropName(vec![dav::PropertyRequest::GetEtag]), + }; + let src = r#" + + infinite + + + + "#; + let got = deserialize::>(src).await; + assert_eq!(got, expected); + } + } +} diff --git a/aero-dav/src/syncencoder.rs b/aero-dav/src/syncencoder.rs new file mode 100644 index 0000000..22b288b --- /dev/null +++ b/aero-dav/src/syncencoder.rs @@ -0,0 +1,144 @@ +use quick_xml::events::{BytesText, Event}; +use quick_xml::Error as QError; + +use super::synctypes::*; +use super::types::Extension; +use super::xml::{IWrite, QWrite, Writer}; + +impl QWrite for SyncCollection { + async fn qwrite(&self, xml: &mut Writer) -> Result<(), QError> { + let start = xml.create_dav_element("sync-collection"); + let end = start.to_end(); + + xml.q.write_event_async(Event::Start(start.clone())).await?; + self.sync_token.qwrite(xml).await?; + self.sync_level.qwrite(xml).await?; + if let Some(limit) = &self.limit { + limit.qwrite(xml).await?; + } + self.prop.qwrite(xml).await?; + xml.q.write_event_async(Event::End(end)).await + } +} + +impl QWrite for SyncTokenRequest { + async fn qwrite(&self, xml: &mut Writer) -> Result<(), QError> { + let start = xml.create_dav_element("sync-token"); + + match self { + Self::InitialSync => xml.q.write_event_async(Event::Empty(start)).await, + Self::IncrementalSync(uri) => { + let end = start.to_end(); + xml.q.write_event_async(Event::Start(start.clone())).await?; + xml.q + .write_event_async(Event::Text(BytesText::new(uri.as_str()))) + .await?; + xml.q.write_event_async(Event::End(end)).await + } + } + } +} + +impl QWrite for SyncToken { + async fn qwrite(&self, xml: &mut Writer) -> Result<(), QError> { + let start = xml.create_dav_element("sync-token"); + let end = start.to_end(); + + xml.q.write_event_async(Event::Start(start.clone())).await?; + xml.q + .write_event_async(Event::Text(BytesText::new(self.0.as_str()))) + .await?; + xml.q.write_event_async(Event::End(end)).await + } +} + +impl QWrite for SyncLevel { + async fn qwrite(&self, xml: &mut Writer) -> Result<(), QError> { + let start = xml.create_dav_element("sync-level"); + let end = start.to_end(); + let text = match self { + Self::One => "1", + Self::Infinite => "infinite", + }; + + xml.q.write_event_async(Event::Start(start.clone())).await?; + xml.q + .write_event_async(Event::Text(BytesText::new(text))) + .await?; + xml.q.write_event_async(Event::End(end)).await + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::realization::All; + use crate::types as dav; + use crate::versioningtypes as vers; + use crate::xml::Node; + use crate::xml::Reader; + use tokio::io::AsyncWriteExt; + + async fn serialize_deserialize>(src: &T) { + let mut buffer = Vec::new(); + let mut tokio_buffer = tokio::io::BufWriter::new(&mut buffer); + let q = quick_xml::writer::Writer::new_with_indent(&mut tokio_buffer, b' ', 4); + let ns_to_apply = vec![ + ("xmlns:D".into(), "DAV:".into()), + ("xmlns:C".into(), "urn:ietf:params:xml:ns:caldav".into()), + ]; + let mut writer = Writer { q, ns_to_apply }; + + src.qwrite(&mut writer).await.expect("xml serialization"); + tokio_buffer.flush().await.expect("tokio buffer flush"); + let got = std::str::from_utf8(buffer.as_slice()).unwrap(); + + // deserialize + let mut rdr = Reader::new(quick_xml::NsReader::from_reader(got.as_bytes())) + .await + .unwrap(); + let res = rdr.find().await.unwrap(); + + // check + assert_eq!(src, &res); + } + + #[tokio::test] + async fn sync_level() { + serialize_deserialize(&SyncLevel::One).await; + serialize_deserialize(&SyncLevel::Infinite).await; + } + + #[tokio::test] + async fn sync_token_request() { + serialize_deserialize(&SyncTokenRequest::InitialSync).await; + serialize_deserialize(&SyncTokenRequest::IncrementalSync( + "http://example.com/ns/sync/1232".into(), + )) + .await; + } + + #[tokio::test] + async fn sync_token() { + serialize_deserialize(&SyncToken("http://example.com/ns/sync/1232".into())).await; + } + + #[tokio::test] + async fn sync_collection() { + serialize_deserialize(&SyncCollection:: { + sync_token: SyncTokenRequest::IncrementalSync("http://example.com/ns/sync/1232".into()), + sync_level: SyncLevel::One, + limit: Some(vers::Limit(vers::NResults(100))), + prop: dav::PropName(vec![dav::PropertyRequest::GetEtag]), + }) + .await; + + serialize_deserialize(&SyncCollection:: { + sync_token: SyncTokenRequest::InitialSync, + sync_level: SyncLevel::Infinite, + limit: None, + prop: dav::PropName(vec![dav::PropertyRequest::GetEtag]), + }) + .await; + } +} diff --git a/aero-dav/src/synctypes.rs b/aero-dav/src/synctypes.rs new file mode 100644 index 0000000..a2f40bd --- /dev/null +++ b/aero-dav/src/synctypes.rs @@ -0,0 +1,68 @@ +use super::types as dav; +use super::versioningtypes as vers; + +// RFC 6578 +// https://datatracker.ietf.org/doc/html/rfc6578 + +//@FIXME add SyncTokenRequest to PropertyRequest +//@FIXME add SyncToken to Property +//@FIXME add SyncToken to Multistatus + +/// Name: sync-collection +/// +/// Namespace: DAV: +/// +/// Purpose: WebDAV report used to synchronize data between client and +/// server. +/// +/// Description: See Section 3. +/// +/// +/// +/// +/// + +#[derive(Debug, PartialEq, Clone)] +pub struct SyncCollection { + pub sync_token: SyncTokenRequest, + pub sync_level: SyncLevel, + pub limit: Option, + pub prop: dav::PropName, +} + +/// Name: sync-token +/// +/// Namespace: DAV: +/// +/// Purpose: The synchronization token provided by the server and +/// returned by the client. +/// +/// Description: See Section 3. +/// +/// +/// +/// +/// Used by multistatus +#[derive(Debug, PartialEq, Clone)] +pub struct SyncToken(pub String); + +/// Used by propfind and report sync-collection +#[derive(Debug, PartialEq, Clone)] +pub enum SyncTokenRequest { + InitialSync, + IncrementalSync(String), +} + +/// Name: sync-level +/// +/// Namespace: DAV: +/// +/// Purpose: Indicates the "scope" of the synchronization report +/// request. +/// +/// Description: See Section 3.3. +#[derive(Debug, PartialEq, Clone)] +pub enum SyncLevel { + One, + Infinite, +} diff --git a/aero-dav/src/types.rs b/aero-dav/src/types.rs index d5466da..6039a26 100644 --- a/aero-dav/src/types.rs +++ b/aero-dav/src/types.rs @@ -11,6 +11,7 @@ pub trait Extension: std::fmt::Debug + PartialEq + Clone { type Property: xml::Node; type PropertyRequest: xml::Node; type ResourceType: xml::Node; + type ReportType: xml::Node; } /// 14.1. activelock XML Element @@ -328,6 +329,10 @@ pub enum LockType { /// response descriptions contained within the responses. /// /// +/// +/// In WebDAV sync (rfc6578), multistatus is extended: +/// +/// #[derive(Debug, PartialEq, Clone)] pub struct Multistatus { pub responses: Vec>, diff --git a/aero-dav/src/versioningdecoder.rs b/aero-dav/src/versioningdecoder.rs new file mode 100644 index 0000000..4816cf1 --- /dev/null +++ b/aero-dav/src/versioningdecoder.rs @@ -0,0 +1,62 @@ +use super::error::ParsingError; +use super::types as dav; +use super::versioningtypes::*; +use super::xml::{IRead, QRead, Reader, DAV_URN}; + +impl QRead> for Report { + async fn qread(xml: &mut Reader) -> Result { + //@FIXME VersionTree not implemented + //@FIXME ExpandTree not implemented + + E::ReportType::qread(xml).await.map(Report::Extension) + } +} + +impl QRead for Limit { + async fn qread(xml: &mut Reader) -> Result { + xml.open(DAV_URN, "limit").await?; + let nres = xml.find().await?; + xml.close().await?; + Ok(Limit(nres)) + } +} + +impl QRead for NResults { + async fn qread(xml: &mut Reader) -> Result { + xml.open(DAV_URN, "nresults").await?; + let sz = xml.tag_string().await?.parse::()?; + xml.close().await?; + Ok(NResults(sz)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::xml::Node; + + async fn deserialize>(src: &str) -> T { + let mut rdr = Reader::new(quick_xml::NsReader::from_reader(src.as_bytes())) + .await + .unwrap(); + rdr.find().await.unwrap() + } + + #[tokio::test] + async fn nresults() { + let expected = NResults(100); + let src = r#"100"#; + let got = deserialize::(src).await; + assert_eq!(got, expected); + } + + #[tokio::test] + async fn limit() { + let expected = Limit(NResults(1024)); + let src = r#" + 1024 + "#; + let got = deserialize::(src).await; + assert_eq!(got, expected); + } +} diff --git a/aero-dav/src/versioningencoder.rs b/aero-dav/src/versioningencoder.rs new file mode 100644 index 0000000..bd40f1b --- /dev/null +++ b/aero-dav/src/versioningencoder.rs @@ -0,0 +1,81 @@ +use quick_xml::events::{BytesText, Event}; +use quick_xml::Error as QError; + +use super::types::Extension; +use super::versioningtypes::*; +use super::xml::{IWrite, QWrite, Writer}; + +impl QWrite for Report { + async fn qwrite(&self, xml: &mut Writer) -> Result<(), QError> { + match self { + Report::VersionTree => unimplemented!(), + Report::ExpandProperty => unimplemented!(), + Report::Extension(inner) => inner.qwrite(xml).await, + } + } +} + +impl QWrite for Limit { + async fn qwrite(&self, xml: &mut Writer) -> Result<(), QError> { + let start = xml.create_dav_element("limit"); + let end = start.to_end(); + + xml.q.write_event_async(Event::Start(start.clone())).await?; + self.0.qwrite(xml).await?; + xml.q.write_event_async(Event::End(end)).await + } +} + +impl QWrite for NResults { + async fn qwrite(&self, xml: &mut Writer) -> Result<(), QError> { + let start = xml.create_dav_element("nresults"); + let end = start.to_end(); + + xml.q.write_event_async(Event::Start(start.clone())).await?; + xml.q + .write_event_async(Event::Text(BytesText::new(&format!("{}", self.0)))) + .await?; + xml.q.write_event_async(Event::End(end)).await + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::xml::Node; + use crate::xml::Reader; + use tokio::io::AsyncWriteExt; + + async fn serialize_deserialize>(src: &T) -> T { + let mut buffer = Vec::new(); + let mut tokio_buffer = tokio::io::BufWriter::new(&mut buffer); + let q = quick_xml::writer::Writer::new_with_indent(&mut tokio_buffer, b' ', 4); + let ns_to_apply = vec![ + ("xmlns:D".into(), "DAV:".into()), + ("xmlns:C".into(), "urn:ietf:params:xml:ns:caldav".into()), + ]; + let mut writer = Writer { q, ns_to_apply }; + + src.qwrite(&mut writer).await.expect("xml serialization"); + tokio_buffer.flush().await.expect("tokio buffer flush"); + let got = std::str::from_utf8(buffer.as_slice()).unwrap(); + + // deserialize + let mut rdr = Reader::new(quick_xml::NsReader::from_reader(got.as_bytes())) + .await + .unwrap(); + rdr.find().await.unwrap() + } + + #[tokio::test] + async fn nresults() { + let orig = NResults(100); + assert_eq!(orig, serialize_deserialize(&orig).await); + } + + #[tokio::test] + async fn limit() { + let orig = Limit(NResults(1024)); + assert_eq!(orig, serialize_deserialize(&orig).await); + } +} diff --git a/aero-dav/src/versioningtypes.rs b/aero-dav/src/versioningtypes.rs index 6c1c204..ba64b05 100644 --- a/aero-dav/src/versioningtypes.rs +++ b/aero-dav/src/versioningtypes.rs @@ -1,3 +1,39 @@ +use super::types as dav; + //@FIXME required for a full DAV implementation // See section 7.1 of the CalDAV RFC // It seems it's mainly due to the fact that the REPORT method is re-used. +// https://datatracker.ietf.org/doc/html/rfc4791#section-7.1 +// +// Defines (required by CalDAV): +// - REPORT method +// - expand-property root report method +// +// Defines (required by Sync): +// - limit, nresults +// - supported-report-set + +// This property identifies the reports that are supported by the +// resource. +// +// +// +// +// ANY value: a report element type + +#[derive(Debug, PartialEq, Clone)] +pub enum Report { + VersionTree, // Not yet implemented + ExpandProperty, // Not yet implemented + Extension(E::ReportType), +} + +/// Limit +/// +#[derive(Debug, PartialEq, Clone)] +pub struct Limit(pub NResults); + +/// NResults +/// +#[derive(Debug, PartialEq, Clone)] +pub struct NResults(pub u64); diff --git a/aero-proto/src/dav/controller.rs b/aero-proto/src/dav/controller.rs index eeb6d43..1b7f7ee 100644 --- a/aero-proto/src/dav/controller.rs +++ b/aero-proto/src/dav/controller.rs @@ -9,8 +9,9 @@ use hyper::{body::Bytes, Request, Response}; use aero_collections::user::User; use aero_dav::caltypes as cal; -use aero_dav::realization::All; +use aero_dav::realization::{self, All}; use aero_dav::types as dav; +use aero_dav::versioningtypes as vers; use aero_ical::query::is_component_match; use crate::dav::codec; @@ -95,7 +96,7 @@ impl Controller { async fn report(self) -> Result { let status = hyper::StatusCode::from_u16(207)?; - let report = match deserialize::>(self.req).await { + let cal_report = match deserialize::>(self.req).await { Ok(v) => v, Err(e) => { tracing::error!(err=?e, "unable to decode REPORT body"); @@ -110,8 +111,8 @@ impl Controller { let calprop: Option>; // Extracting request information - match report { - cal::Report::Multiget(m) => { + match cal_report { + vers::Report::Extension(realization::ReportType::Cal(cal::ReportType::Multiget(m))) => { // Multiget is really like a propfind where Depth: 0|1|Infinity is replaced by an arbitrary // list of URLs // Getting the list of nodes @@ -136,13 +137,16 @@ impl Controller { } calprop = m.selector; } - cal::Report::Query(q) => { + vers::Report::Extension(realization::ReportType::Cal(cal::ReportType::Query(q))) => { calprop = q.selector; ok_node = apply_filter(self.node.children(&self.user).await, &q.filter) .try_collect() .await?; } - cal::Report::FreeBusy(_) => { + vers::Report::Extension(realization::ReportType::Sync(_sync_col)) => { + todo!() + } + _ => { return Ok(Response::builder() .status(501) .body(text_body("Not implemented"))?) diff --git a/aerogramme/tests/behavior.rs b/aerogramme/tests/behavior.rs index c88583f..1097fe7 100644 --- a/aerogramme/tests/behavior.rs +++ b/aerogramme/tests/behavior.rs @@ -691,7 +691,7 @@ fn rfc4791_webdav_caldav() { .send()?; //@FIXME not yet supported. returns DAV: 1 ; expects DAV: 1 calendar-access // Not used by any client I know, so not implementing it now. - + // --- REPORT calendar-multiget --- let cal_query = r#"