chiark / gitweb /
wip links - does not compile
authorIan Jackson <ijackson@chiark.greenend.org.uk>
Sun, 3 Jan 2021 20:02:12 +0000 (20:02 +0000)
committerIan Jackson <ijackson@chiark.greenend.org.uk>
Sun, 3 Jan 2021 20:02:12 +0000 (20:02 +0000)
Signed-off-by: Ian Jackson <ijackson@chiark.greenend.org.uk>
Cargo.lock.example
Cargo.toml
daemon/cmdlistener.rs
src/bin/otter.rs
src/commands.rs
src/global.rs
src/imports.rs
src/spec.rs
src/sse.rs
src/updates.rs

index 8d78b0414f8d7d1243a6f354dc7253373b968df7..319d251d2b95f20762fc9a3c58a74ff6bf82d224 100644 (file)
@@ -1622,6 +1622,7 @@ dependencies = [
  "toml 0.5.8",
  "typetag",
  "uds",
+ "url 2.2.0",
  "vecdeque-stableix",
 ]
 
index 25383485c26dd1c12e5d204802f431e1f888abfc..8647c62d25c8edf3b0fa827ec3a289a3ff8132a7 100644 (file)
@@ -63,4 +63,5 @@ tera = "0.11"
 toml = "0.5"
 typetag = "0.1.6"
 uds = "0.2"
+url = "2"
 vecdeque-stableix = "1"
index 3a3bc05b6b54571cbc9030fbdb35e60b7a4313e4..d018d107eca706fae1b1258b89d72ae3a42c152a 100644 (file)
@@ -435,12 +435,30 @@ fn execute_game_insn<'cs, 'igr, 'ig: 'igr>(
       let table_size = ig.gs.table_size;
       let links = ig.links.iter().filter_map(
         |(k,v)|
-        Some((k.clone(), v.as_ref()?.clone().0))
+        Some((k.clone(), UrlSpec(v.as_ref()?.clone())))
       ).collect();
       let info = MgmtGameResponseGameInfo { table_size, players, links };
       Ok(Resp::Info(info))
     })?,
 
+    Insn::SetLinks(spec_links) =>  {
+      let ig = cs.check_acl(ag, ig, PCH::Instance, &[TP::SetLinks])?.0;
+      let mut new_links : LinksTable = default();
+      for (k,v) in spec_links.drain() {
+        let url : Url = (&v).try_into()?;
+        new_links[k] = Some(url.into_string());
+      }
+      ig.links = new_links;
+      let log = vec![LogEntry {
+        html: Html(format!("{} set the links to off-server game resources",
+                           &who.0)),
+      }];
+      (U{ log,
+          pcs: vec![],
+          raw: vec![ PreparedUpdateEntry::SetLinks ]},
+       Fine, eg)
+    }
+
     ResetPlayerAccess(player) => {
       let (ig, auth) = cs.check_acl_manip_player_access
         (ag, ig, player, TP::ResetOthersAccess)?;
index cc96328852fc416c7aa3153179841390ddbb3b6e..a9a5f1f555f0a05ee5d19c26c29c151d0741224a 100644 (file)
@@ -866,7 +866,7 @@ mod set_link {
       None => {
         let MgmtGameResponseGameInfo { links, .. } = chan.info()?;
         for (tk, v) in links {
-          // xxx check syntax
+          let v : Url = (&v).try_into().context("reparse sererr's UrlSpec")?;
           match args.kind {
             None => {
               println!("{:<10} {}", tk, &v);
@@ -884,9 +884,9 @@ mod set_link {
         let kind = args.kind.unwrap();
         chan.alter_game(vec![
           if url == "" {
-            MGI::SetLink { kind, url }
-          } else {
             MGI::RemoveLink { kind }
+          } else {
+            MGI::SetLink { kind, url: UrlSpec(url) }
           }
         ], None)?;
       },
index 7408d3eb40eb1b1a2916574bef03c1896720de05..09f9eb2cb08896839befc5eaeaec647394fb28cf 100644 (file)
@@ -86,9 +86,9 @@ pub enum MgmtGameInstruction {
   UpdatePlayer { player: PlayerId, details: MgmtPlayerDetails },
   LeaveGame(PlayerId),
 
-  SetLinks(HashMap<LinkKind,String>),
+  SetLinks(HashMap<LinkKind, UrlSpec>),
   RemoveLink { kind: LinkKind },
-  SetLink { kind: LinkKind, url: String },
+  SetLink { kind: LinkKind, url: UrlSpec },
 
   ClearLog,
   SetACL { acl: Acl<TablePermission> },
@@ -126,7 +126,7 @@ pub struct AccessTokenReport { pub lines: Vec<String> }
 pub struct MgmtGameResponseGameInfo {
   pub table_size: Pos,
   pub players: SecondarySlotMap<PlayerId, MgmtPlayerInfo>,
-  pub links: Vec<(LinkKind, String)>,
+  pub links: Vec<(LinkKind, UrlSpec)>,
 }
 
 #[derive(Debug,Clone,Serialize,Deserialize)]
index 696e7921ac8ac0d2233af750640a1f1ba376217c..db9b950e5edf797daba04163543b2b5bdade0699 100644 (file)
@@ -38,7 +38,9 @@ pub struct InstanceName {
 #[derive(Debug,Clone)]
 pub struct InstanceRef (Arc<Mutex<InstanceContainer>>);
 
-pub type LinksTable = EnumMap<LinkKind, Option<Html>>;
+#[derive(Debug,Clone,Serialize,Deserialize,Default)]
+#[serde(transparent)]
+pub struct LinksTable(pub EnumMap<LinkKind, Option<String>>);
 
 pub struct Instance {
   pub name: Arc<InstanceName>,
@@ -453,6 +455,19 @@ impl Display for InstanceName {
   }
 }
 
+impl From<&LinksTable> for Html {
+  fn from(links: &LinksTable) -> Html {
+    links.iter()
+      .filter_map(|(k,v)| {
+        let v = v.as_ref()?;
+        let url = htmlescape::encode_minimal(v);
+        Some(Html(format!("<a href={url}>{kind}</a>",
+                          url=url, kind=k)))
+      })
+      .join(" | ")
+  }
+}
+
 // ---------- Player and token functionality ----------
 
 impl<Id> InstanceAccessDetails<Id>
index ff078e8d3f3f5ebcea0050f50b18733c39322527..c1fd183bf95e56d6c72b68d1b7732d3c6cf93a3b 100644 (file)
@@ -74,6 +74,7 @@ pub use serde_with::SerializeDisplay;
 pub use slotmap::dense::DenseSlotMap;
 pub use strum::EnumString;
 pub use thiserror::Error;
+pub use url::Url;
 pub use vecdeque_stableix::Deque as StableIndexVecDeque;
 pub use zcoord::{self, ZCoord};
 
index 4e4b3b252a64069d45bf42b99c06390b452cd354..5b33af9b5d36432c53c519cd3794120eeefe5511 100644 (file)
@@ -50,7 +50,13 @@ define_index_type! {
 #[derive(Debug,Default)]
 #[repr(transparent)]
 #[serde(transparent)]
-pub struct ColourSpec(String);
+pub struct ColourSpec(pub String);
+
+#[derive(Debug,Default,Clone,Eq,PartialEq,Hash,Ord,PartialOrd)]
+#[derive(Serialize,Deserialize)]
+#[repr(transparent)]
+#[serde(transparent)]
+pub struct UrlSpec(pub String);
 
 #[derive(Error,Clone,Serialize,Deserialize,Debug)]
 pub enum SpecError {
@@ -64,6 +70,8 @@ pub enum SpecError {
   AclInvalidAccountGlob,
   AclEntryOverlappingAllowDeny,
   InconsistentPieceCount,
+  BadUrlSyntax,
+  UrlTooLong,
 }
 display_as_debug!{SpecError}
 
@@ -74,7 +82,7 @@ pub struct TableSpec {
   #[serde(default)] pub players: Vec<TablePlayerSpec>,
   pub player_perms: Option<HashSet<TablePermission>>,
   #[serde(default)] pub acl: Acl<TablePermission>,
-  #[serde(default)] pub links: HashMap<LinkKind, String>,
+  #[serde(default)] pub links: HashMap<LinkKind, UrlSpec>,
 }
 
 #[derive(Debug,Serialize,Deserialize)]
@@ -108,6 +116,7 @@ pub enum TablePermission {
   ViewNotSecret,
   Play,
   ChangePieces,
+  SetLinks,
   ResetOthersAccess,
   RedeliverOthersAccess,
   ModifyOtherPlayer,
@@ -524,4 +533,27 @@ pub mod implementation {
       Html(spec.0.clone())
     }
   }
+
+  impl UrlSpec {
+    const MAX_LEN : usize = 200;
+  }
+
+  impl TryFrom<&UrlSpec> for Url {
+    type Error = SpecError;
+    #[throws(SpecError)]
+    fn try_from(spec: &UrlSpec) -> Url {
+      if spec.0.len() > UrlSpec::MAX_LEN {
+        throw!(SE::UrlTooLong);
+      }
+      let base = Url::parse(&config().public_url)
+        .or_else(|_| Url::parse(
+          "https://bad-otter-config-public-url.example.net/"
+        )).unwrap();
+      let url = Url::options()
+        .base_url(Some(&base))
+        .parse(&spec.0)
+        .map_err(|_| SE::BadUrlSyntax)?;
+      url
+    }
+  }
 }
index 36b1800d8d2a7f67f6246604a6d047556136f17e..01482159968b2af10b365992cab88534abc1dcd9 100644 (file)
@@ -91,7 +91,9 @@ impl Read for UpdateReader {
              self.player, self.client, ig.gs.gen, self.to_send)?;
     }
 
-    let iplayer = &mut match ig.iplayers.get_mut(self.player) {
+    let g = &mut *ig;
+    let for_json_len = InstanceForJsonLen { links: &g.links };
+    let iplayer = &mut match g.iplayers.get_mut(self.player) {
       Some(x) => x,
       None => {
         let data = format!("event: player-gone\n\
@@ -131,7 +133,7 @@ impl Read for UpdateReader {
           break
         }
       };
-      let next_len = UPDATE_MAX_FRAMING_SIZE + next.json_len();
+      let next_len = UPDATE_MAX_FRAMING_SIZE + next.json_len(&for_json_len);
       if next_len > buf.len() {
         if buf.len() != orig_wanted { break }
 
index db1f9e83250fd339098c78192f260293dadddf92..a979c0852846211c4d9d713ee4bc7ed6586f7a05 100644 (file)
@@ -48,6 +48,7 @@ pub enum PreparedUpdateEntry {
   },
   SetTableSize(Pos),
   SetTableColour(Colour),
+  SetLinks, // we use whatever the most recent is
   AddPlayer { player: PlayerId, data: DataLoadPlayer },
   RemovePlayer { player: PlayerId },
   Log (Arc<CommittedLogEntry>),
@@ -115,6 +116,7 @@ enum TransmitUpdateEntry<'u> {
   SetTableColour(&'u Colour),
   AddPlayer { player: PlayerId, data: &'u DataLoadPlayer },
   RemovePlayer { player: PlayerId },
+  SetLinks(Html),
   #[serde(serialize_with="serialize_logentry")]
   Log(TransmitUpdateLogEntry<'u>),
   Error(&'u ErrorSignaledViaUpdate),
@@ -145,6 +147,10 @@ impl PlayerUpdatesBuildContext {
   }
 }
 
+pub struct InstanceForJsonLen<'i> {
+  links: &'i LinksTable,
+}
+
 impl PlayerUpdates {
   pub fn new_begin(gs: &GameState) -> PlayerUpdatesBuildContext {
     let u1 = Arc::new(PreparedUpdate {
@@ -181,13 +187,13 @@ impl PlayerUpdates {
 }
 
 impl PreparedUpdate {
-  pub fn json_len(&self) -> usize {
-    self.us.iter().map(|u| 20 + u.json_len()).sum()
+  pub fn json_len(&self, ig: &InstanceForJsonLen) -> usize {
+    self.us.iter().map(|u| 20 + u.json_len(ig)).sum()
   }
 }
 
 impl PreparedUpdateEntry {
-  pub fn json_len(&self) -> usize {
+  pub fn json_len(&self, ig: &InstanceForJsonLen) -> usize {
     use PreparedUpdateEntry::*;
     match self {
       Piece { ref op, .. } => {
@@ -203,6 +209,11 @@ impl PreparedUpdateEntry {
       SetTableColour(colour) => {
         colour.0.as_bytes().len() + 50
       }
+      SetLinks => {
+        ig.links.iter().filter_map(
+          |(_k,v)| Some(50 + v.as_ref()?.len())
+        ).sum::<usize>() + 50
+      }
       SetTableSize(_) |
       RemovePlayer { player:_ } |
       Error(_,_) => {
@@ -552,6 +563,9 @@ impl PreparedUpdate {
             continue
           }
         }
+        PUE::SetLinks => {
+          
+        }
       };
       ents.push(ue);
     };