zero-trust-rps 0.0.5

Online Multiplayer Rock Paper Scissors
Documentation
use std::collections::HashSet;

use crate::common::{
    message::{RoomState, UserState},
    rps::state::RpsState,
    utils::get_random_bytes,
};

use super::state::ClientState;

pub(super) fn handle_new_room_state(
    state: &mut ClientState,
    new_room_state: &RoomState,
) -> Result<(), String> {
    if let Some(old_state) = &state.room {
        assert_eq!(new_room_state.id, old_state.id);
    }
    // TODO: validate the same state changes in the server
    let mut old_cache_keys = state.cache.keys().copied().collect::<HashSet<_>>();
    for user in &new_room_state.users {
        old_cache_keys.remove(&user.id);
        let id = user.id;
        match state.cache.get(&user.id) {
                        None => match user.state {
                            InRoom => {
                                state.cache.insert(user.id, InRoom);
                            }
                            user_state => {
                                if state.room.is_some() {  // isn't first room update
                                    Err(format!("user {id} joined game in state {user_state:?}"))?
                                } else {
                                    state.cache.insert(user.id, user_state); // cache as it's the first room update
                                }
                            }
                        },
                        Some(InRoom) => match user.state {
                            InRoom => (),
                            Played(hash) => {
                                state.cache.insert(user.id, Played(hash));
                            }
                            Confirmed(_) => {
                                Err(format!("user {id} confirmed without playing before"))?
                            }
                        },
                        Some(Played(hash)) => match user.state {
                            InRoom => {
                                if new_room_state.round.is_some() {
                                    Err(format!(
                                        "user {id} switched to state InRoom without confirming {hash:?}"
                                    ))?
                                } else {
                                    // TODO: improve behaviour, is a bit weird
                                    // Can be ok, other user in round probably left
                                    state.cache.insert(user.id, InRoom);
                                }
                            },
                            Played(new_hash) => {
                                if *hash != new_hash {
                                    Err(format!(
                                        "user {id} changed hash from {hash:?} to {new_hash:?} without confirming"
                                    ))?
                                }
                            },
                            Confirmed(hash_with_data) => {
                                if hash_with_data.as_hash() != *hash {
                                    Err(format!(
                                        "user {id} did not confirm {hash:?}, instead played: {hash_with_data:?}"
                                    ))?
                                }
                                state.cache.insert(user.id, Confirmed(hash_with_data));
                                hash_with_data.verify(
                                    new_room_state.round.as_ref().ok_or_else(
                                        || format!("No current round while {id} confirmed")
                                    )?
                                )?
                            },
                        },
                        Some(Confirmed(hash_with_data)) => {
                            match user.state {
                                InRoom => {
                                    state.cache.insert(user.id, InRoom);
                                },
                                Played(hash) => Err(format!(
                                    "user {id} played {hash:?} directly after confirming without being in room before"
                                ))?,
                                Confirmed(new_hash_with_data) => {
                                    if *hash_with_data != new_hash_with_data {
                                        Err(format!(
                                            "user {id} changed confirmation from {hash_with_data:?} to {new_hash_with_data:?}"
                                        ))?
                                    }
                                },
                            }
                        },
                    }
    }
    for key in old_cache_keys {
        state.cache.remove(&key);
    }
    if let Some(ref round) = new_room_state.round {
        round.validate()?;
    }

    let moves = new_room_state
        .users
        .iter()
        .filter_map(|user| {
            Some((
                user.id,
                match user.state {
                    UserState::Confirmed(data) => data,
                    _ => None?,
                },
            ))
        })
        .collect::<Box<[_]>>();

    state.current_plays = None;
    if let Some(room) = state.room.as_ref() {
        if let Some(round) = room.round.as_ref() {
            if round.users.len() == moves.len() {
                state.current_plays = Some(moves);
            }
        }
    }

    log::trace!("Us: {:?}", state.user);
    log::trace!("Users: {:?}", new_room_state.users);
    let us = new_room_state
        .users
        .iter()
        .find(|user| user.id == state.user)
        .ok_or("we should be in the room we get updates from")?;

    use crate::common::message::UserState::*;
    let new_rps_state = match us.state {
        InRoom => RpsState::InRoom,
        Played(_) => RpsState::Played,
        Confirmed(_) => {
            // TODO: check here
            // assert_eq!(data.get_data(), state.curr_play.trim_end_matches('\0'));
            RpsState::Confirmed
        }
    };
    if state.state != new_rps_state {
        if matches!(new_rps_state, RpsState::InRoom) {
            state.secret = get_random_bytes();
        }
        state.state = new_rps_state;
    }

    state.room = Some(new_room_state.clone());

    Ok(())
}