<commit_before>//! Module for User-Interactive Authentication API types.

use std::{collections::BTreeMap, fmt};

use bytes::Buf;
use ruma_api::{
    error::{IntoHttpError, ResponseDeserializationError},
    EndpointError, OutgoingResponse,
};
use ruma_serde::Outgoing;
use serde::{Deserialize, Serialize};
use serde_json::{
    from_reader as from_json_reader, to_vec as to_json_vec, value::RawValue as RawJsonValue,
    Value as JsonValue,
};

use crate::error::{Error as MatrixError, ErrorBody};

/// Additional authentication information for the user-interactive authentication API.
#[derive(Clone, Debug, Outgoing, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[serde(untagged)]
pub enum AuthData<'a> {
    /// Used for sending UIAA authentication requests to the homeserver directly from the client.
    // Could be made non-exhaustive by creating a separate struct and making auth_parameters
    // private, but probably not worth the hassle.
    DirectRequest {
        /// The login type that the client is attempting to complete.
        #[serde(rename = "type")]
        kind: &'a str,

        /// The value of the session key given by the homeserver.
        #[serde(skip_serializing_if = "Option::is_none")]
        session: Option<&'a str>,

        /// Parameters submitted for a particular authentication stage.
        #[serde(flatten)]
        auth_parameters: BTreeMap<String, JsonValue>,
    },

    /// Used by the client to acknowledge that the user has completed a UIAA stage through the
    /// fallback method.
    // Exhaustiveness is correct here since this variant is a fallback that explicitly only has a
    // single field. TODO: #[serde(deny_unknown_fields)] not supported on enum variants
    // https://github.com/serde-rs/serde/issues/1982
    FallbackAcknowledgement {
        /// The value of the session key given by the homeserver.
        session: &'a str,
    },
}

impl<'a> AuthData<'a> {
    /// Creates a new `AuthData::DirectRequest` with the given login type.
    pub fn direct_request(kind: &'a str) -> Self {
        Self::DirectRequest { kind, session: None, auth_parameters: BTreeMap::new() }
    }

    /// Creates a new `AuthData::FallbackAcknowledgement` with the given session key.
    pub fn fallback_acknowledgement(session: &'a str) -> Self {
        Self::FallbackAcknowledgement { session }
    }
}

/// Information about available authentication flows and status for User-Interactive Authenticiation
/// API.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct UiaaInfo {
    /// List of authentication flows available for this endpoint.
    pub flows: Vec<AuthFlow>,

    /// List of stages in the current flow completed by the client.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub completed: Vec<String>,

    /// Authentication parameters required for the client to complete authentication.
    ///
    /// To create a `Box<RawJsonValue>`, use `serde_json::value::to_raw_value`.
    pub params: Box<RawJsonValue>,

    /// Session key for client to use to complete authentication.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub session: Option<String>,

    /// Authentication-related errors for previous request returned by homeserver.
    #[serde(flatten, skip_serializing_if = "Option::is_none")]
    pub auth_error: Option<ErrorBody>,
}

impl UiaaInfo {
    /// Creates a new `UiaaInfo` with the given flows and parameters.
    pub fn new(flows: Vec<AuthFlow>, params: Box<RawJsonValue>) -> Self {
        Self { flows, completed: Vec::new(), params, session: None, auth_error: None }
    }
}

/// Description of steps required to authenticate via the User-Interactive Authentication API.
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[cfg_attr(test, derive(PartialEq))]
pub struct AuthFlow {
    /// Ordered list of stages required to complete authentication.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub stages: Vec<String>,
}

impl AuthFlow {
    /// Creates an empty `AuthFlow`.
    pub fn new() -> Self {
        Self { stages: Vec::new() }
    }
}

/// Contains either a User-Interactive Authentication API response body or a Matrix error.
#[derive(Clone, Debug)]
pub enum UiaaResponse {
    /// User-Interactive Authentication API response
    AuthResponse(UiaaInfo),

    /// Matrix error response
    MatrixError(MatrixError),
}

impl fmt::Display for UiaaResponse {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::AuthResponse(_) => write!(f, "User-Interactive Authentication required."),
            Self::MatrixError(err) => write!(f, "{}", err),
        }
    }
}

impl From<MatrixError> for UiaaResponse {
    fn from(error: MatrixError) -> Self {
        Self::MatrixError(error)
    }
}

impl EndpointError for UiaaResponse {
    fn try_from_http_response<T: Buf>(
        response: http::Response<T>,
    ) -> Result<Self, ResponseDeserializationError> {
        if response.status() == http::StatusCode::UNAUTHORIZED {
            Ok(UiaaResponse::AuthResponse(from_json_reader(response.into_body().reader())?))
        } else {
            MatrixError::try_from_http_response(response).map(From::from)
        }
    }
}

impl std::error::Error for UiaaResponse {}

impl OutgoingResponse for UiaaResponse {
    fn try_into_http_response(self) -> Result<http::Response<Vec<u8>>, IntoHttpError> {
        match self {
            UiaaResponse::AuthResponse(authentication_info) => http::Response::builder()
                .header(http::header::CONTENT_TYPE, "application/json")
                .status(&http::StatusCode::UNAUTHORIZED)
                .body(to_json_vec(&authentication_info)?)
                .map_err(Into::into),
            UiaaResponse::MatrixError(error) => error.try_into_http_response(),
        }
    }
}

#[cfg(test)]
mod tests {
    use maplit::btreemap;
    use matches::assert_matches;
    use ruma_api::{EndpointError, OutgoingResponse};
    use serde_json::{
        from_slice as from_json_slice, from_str as from_json_str, from_value as from_json_value,
        json, to_value as to_json_value, value::to_raw_value as to_raw_json_value,
        Value as JsonValue,
    };

    use super::{AuthData, AuthFlow, IncomingAuthData, UiaaInfo, UiaaResponse};
    use crate::error::{ErrorBody, ErrorKind};

    #[test]
    fn serialize_authentication_data_direct_request() {
        let authentication_data = AuthData::DirectRequest {
            kind: "example.type.foo",
            session: Some("ZXY000"),
            auth_parameters: btreemap! {
                "example_credential".to_owned() => json!("verypoorsharedsecret")
            },
        };

        assert_eq!(
            json!({
                "type": "example.type.foo",
                "session": "ZXY000",
                "example_credential": "verypoorsharedsecret",
            }),
            to_json_value(authentication_data).unwrap()
        );
    }

    #[test]
    fn deserialize_authentication_data_direct_request() {
        let json = json!({
            "type": "example.type.foo",
            "session": "opaque_session_id",
            "example_credential": "verypoorsharedsecret",
        });

        assert_matches!(
            from_json_value(json).unwrap(),
            IncomingAuthData::DirectRequest { kind, session: Some(session), auth_parameters }
            if kind == "example.type.foo"
                && session == "opaque_session_id"
                && auth_parameters == btreemap!{
                    "example_credential".to_owned() => json!("verypoorsharedsecret")
                }
        );
    }

    #[test]
    fn serialize_authentication_data_fallback() {
        let authentication_data = AuthData::FallbackAcknowledgement { session: "ZXY000" };

        assert_eq!(json!({ "session": "ZXY000" }), to_json_value(authentication_data).unwrap());
    }

    #[test]
    fn deserialize_authentication_data_fallback() {
        let json = json!({ "session": "opaque_session_id" });

        assert_matches!(
            from_json_value(json).unwrap(),
            IncomingAuthData::FallbackAcknowledgement { session }
            if session == "opaque_session_id"
        );
    }

    #[test]
    fn serialize_uiaa_info() {
        let uiaa_info = UiaaInfo {
            flows: vec![AuthFlow {
                stages: vec!["m.login.password".into(), "m.login.dummy".into()],
            }],
            completed: vec!["m.login.password".into()],
            params: to_raw_json_value(&json!({
                "example.type.baz": {
                    "example_key": "foobar"
                }
            }))
            .unwrap(),
            session: None,
            auth_error: None,
        };

        let json = json!({
            "flows": [{ "stages": ["m.login.password", "m.login.dummy"] }],
            "completed": ["m.login.password"],
            "params": {
                "example.type.baz": {
                    "example_key": "foobar"
                }
            }
        });
        assert_eq!(to_json_value(uiaa_info).unwrap(), json);
    }

    #[test]
    fn deserialize_uiaa_info() {
        let json = json!({
            "errcode": "M_FORBIDDEN",
            "error": "Invalid password",
            "completed": ["example.type.foo"],
            "flows": [
                {
                    "stages": ["example.type.foo", "example.type.bar"]
                },
                {
                    "stages": ["example.type.foo", "example.type.baz"]
                }
            ],
            "params": {
                "example.type.baz": {
                    "example_key": "foobar"
                }
            },
            "session": "xxxxxx"
        });

        assert_matches!(
            from_json_value::<UiaaInfo>(json).unwrap(),
            UiaaInfo {
                auth_error: Some(ErrorBody {
                    kind: ErrorKind::Forbidden,
                    message: error_message,
                }),
                completed,
                flows,
                params,
                session: Some(session),
            } if error_message == "Invalid password"
                && completed == vec!["example.type.foo".to_owned()]
                && flows == vec![
                    AuthFlow {
                        stages: vec![
                            "example.type.foo".into(),
                            "example.type.bar".into(),
                        ],
                    },
                    AuthFlow {
                        stages: vec![
                            "example.type.foo".into(),
                            "example.type.baz".into(),
                        ],
                    },
                ]
                && from_json_str::<JsonValue>(params.get()).unwrap() == json!({
                    "example.type.baz": {
                        "example_key": "foobar"
                    }
                })
                && session == "xxxxxx"
        );
    }

    #[test]
    fn try_uiaa_response_into_http_response() {
        let uiaa_info = UiaaInfo {
            flows: vec![AuthFlow {
                stages: vec!["m.login.password".into(), "m.login.dummy".into()],
            }],
            completed: vec!["m.login.password".into()],
            params: to_raw_json_value(&json!({
                "example.type.baz": {
                    "example_key": "foobar"
                }
            }))
            .unwrap(),
            session: None,
            auth_error: None,
        };
        let uiaa_response = UiaaResponse::AuthResponse(uiaa_info).try_into_http_response();

        assert_matches!(
            from_json_slice::<UiaaInfo>(uiaa_response.body()).unwrap(),
            UiaaInfo {
                flows,
                completed,
                params,
                session: None,
                auth_error: None,
            } if flows == vec![AuthFlow {
                    stages: vec!["m.login.password".into(), "m.login.dummy".into()],
                }]
                && completed == vec!["m.login.password".to_owned()]
                && from_json_str::<JsonValue>(params.get()).unwrap() == json!({
                    "example.type.baz": {
                        "example_key": "foobar"
                    }
                })
        );
        assert_eq!(uiaa_response.status(), http::status::StatusCode::UNAUTHORIZED);
    }

    #[test]
    fn try_uiaa_response_from_http_response() {
        let json = serde_json::to_string(&json!({
            "errcode": "M_FORBIDDEN",
            "error": "Invalid password",
            "completed": [ "example.type.foo" ],
            "flows": [
                {
                    "stages": [ "example.type.foo", "example.type.bar" ]
                },
                {
                    "stages": [ "example.type.foo", "example.type.baz" ]
                }
            ],
            "params": {
                "example.type.baz": {
                    "example_key": "foobar"
                }
            },
            "session": "xxxxxx"
        }))
        .unwrap();

        let http_response = http::Response::builder()
            .status(http::StatusCode::UNAUTHORIZED)
            .body(json.as_bytes())
            .unwrap();

        let parsed_uiaa_info = match UiaaResponse::try_from_http_response(http_response).unwrap() {
            UiaaResponse::AuthResponse(uiaa_info) => uiaa_info,
            _ => panic!("Expected UiaaResponse::AuthResponse"),
        };

        assert_matches!(
            parsed_uiaa_info,
            UiaaInfo {
                auth_error: Some(ErrorBody {
                    kind: ErrorKind::Forbidden,
                    message: error_message,
                }),
                completed,
                flows,
                params,
                session: Some(session),
            } if error_message == "Invalid password"
                && completed == vec!["example.type.foo".to_owned()]
                && flows == vec![
                    AuthFlow {
                        stages: vec![
                            "example.type.foo".into(),
                            "example.type.bar".into(),
                        ],
                    },
                    AuthFlow {
                        stages: vec![
                            "example.type.foo".into(),
                            "example.type.baz".into(),
                        ],
                    },
                ]
                && from_json_str::<JsonValue>(params.get()).unwrap() == json!({
                    "example.type.baz": {
                        "example_key": "foobar"
                    }
                })
                && session == "xxxxxx"
        );
    }
}
<commit_msg>client-api: Fix recently broken test<commit_after>//! Module for User-Interactive Authentication API types.

use std::{collections::BTreeMap, fmt};

use bytes::Buf;
use ruma_api::{
    error::{IntoHttpError, ResponseDeserializationError},
    EndpointError, OutgoingResponse,
};
use ruma_serde::Outgoing;
use serde::{Deserialize, Serialize};
use serde_json::{
    from_reader as from_json_reader, to_vec as to_json_vec, value::RawValue as RawJsonValue,
    Value as JsonValue,
};

use crate::error::{Error as MatrixError, ErrorBody};

/// Additional authentication information for the user-interactive authentication API.
#[derive(Clone, Debug, Outgoing, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[serde(untagged)]
pub enum AuthData<'a> {
    /// Used for sending UIAA authentication requests to the homeserver directly from the client.
    // Could be made non-exhaustive by creating a separate struct and making auth_parameters
    // private, but probably not worth the hassle.
    DirectRequest {
        /// The login type that the client is attempting to complete.
        #[serde(rename = "type")]
        kind: &'a str,

        /// The value of the session key given by the homeserver.
        #[serde(skip_serializing_if = "Option::is_none")]
        session: Option<&'a str>,

        /// Parameters submitted for a particular authentication stage.
        #[serde(flatten)]
        auth_parameters: BTreeMap<String, JsonValue>,
    },

    /// Used by the client to acknowledge that the user has completed a UIAA stage through the
    /// fallback method.
    // Exhaustiveness is correct here since this variant is a fallback that explicitly only has a
    // single field. TODO: #[serde(deny_unknown_fields)] not supported on enum variants
    // https://github.com/serde-rs/serde/issues/1982
    FallbackAcknowledgement {
        /// The value of the session key given by the homeserver.
        session: &'a str,
    },
}

impl<'a> AuthData<'a> {
    /// Creates a new `AuthData::DirectRequest` with the given login type.
    pub fn direct_request(kind: &'a str) -> Self {
        Self::DirectRequest { kind, session: None, auth_parameters: BTreeMap::new() }
    }

    /// Creates a new `AuthData::FallbackAcknowledgement` with the given session key.
    pub fn fallback_acknowledgement(session: &'a str) -> Self {
        Self::FallbackAcknowledgement { session }
    }
}

/// Information about available authentication flows and status for User-Interactive Authenticiation
/// API.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct UiaaInfo {
    /// List of authentication flows available for this endpoint.
    pub flows: Vec<AuthFlow>,

    /// List of stages in the current flow completed by the client.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub completed: Vec<String>,

    /// Authentication parameters required for the client to complete authentication.
    ///
    /// To create a `Box<RawJsonValue>`, use `serde_json::value::to_raw_value`.
    pub params: Box<RawJsonValue>,

    /// Session key for client to use to complete authentication.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub session: Option<String>,

    /// Authentication-related errors for previous request returned by homeserver.
    #[serde(flatten, skip_serializing_if = "Option::is_none")]
    pub auth_error: Option<ErrorBody>,
}

impl UiaaInfo {
    /// Creates a new `UiaaInfo` with the given flows and parameters.
    pub fn new(flows: Vec<AuthFlow>, params: Box<RawJsonValue>) -> Self {
        Self { flows, completed: Vec::new(), params, session: None, auth_error: None }
    }
}

/// Description of steps required to authenticate via the User-Interactive Authentication API.
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[cfg_attr(test, derive(PartialEq))]
pub struct AuthFlow {
    /// Ordered list of stages required to complete authentication.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub stages: Vec<String>,
}

impl AuthFlow {
    /// Creates an empty `AuthFlow`.
    pub fn new() -> Self {
        Self { stages: Vec::new() }
    }
}

/// Contains either a User-Interactive Authentication API response body or a Matrix error.
#[derive(Clone, Debug)]
pub enum UiaaResponse {
    /// User-Interactive Authentication API response
    AuthResponse(UiaaInfo),

    /// Matrix error response
    MatrixError(MatrixError),
}

impl fmt::Display for UiaaResponse {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::AuthResponse(_) => write!(f, "User-Interactive Authentication required."),
            Self::MatrixError(err) => write!(f, "{}", err),
        }
    }
}

impl From<MatrixError> for UiaaResponse {
    fn from(error: MatrixError) -> Self {
        Self::MatrixError(error)
    }
}

impl EndpointError for UiaaResponse {
    fn try_from_http_response<T: Buf>(
        response: http::Response<T>,
    ) -> Result<Self, ResponseDeserializationError> {
        if response.status() == http::StatusCode::UNAUTHORIZED {
            Ok(UiaaResponse::AuthResponse(from_json_reader(response.into_body().reader())?))
        } else {
            MatrixError::try_from_http_response(response).map(From::from)
        }
    }
}

impl std::error::Error for UiaaResponse {}

impl OutgoingResponse for UiaaResponse {
    fn try_into_http_response(self) -> Result<http::Response<Vec<u8>>, IntoHttpError> {
        match self {
            UiaaResponse::AuthResponse(authentication_info) => http::Response::builder()
                .header(http::header::CONTENT_TYPE, "application/json")
                .status(&http::StatusCode::UNAUTHORIZED)
                .body(to_json_vec(&authentication_info)?)
                .map_err(Into::into),
            UiaaResponse::MatrixError(error) => error.try_into_http_response(),
        }
    }
}

#[cfg(test)]
mod tests {
    use maplit::btreemap;
    use matches::assert_matches;
    use ruma_api::{EndpointError, OutgoingResponse};
    use serde_json::{
        from_slice as from_json_slice, from_str as from_json_str, from_value as from_json_value,
        json, to_value as to_json_value, value::to_raw_value as to_raw_json_value,
        Value as JsonValue,
    };

    use super::{AuthData, AuthFlow, IncomingAuthData, UiaaInfo, UiaaResponse};
    use crate::error::{ErrorBody, ErrorKind};

    #[test]
    fn serialize_authentication_data_direct_request() {
        let authentication_data = AuthData::DirectRequest {
            kind: "example.type.foo",
            session: Some("ZXY000"),
            auth_parameters: btreemap! {
                "example_credential".to_owned() => json!("verypoorsharedsecret")
            },
        };

        assert_eq!(
            json!({
                "type": "example.type.foo",
                "session": "ZXY000",
                "example_credential": "verypoorsharedsecret",
            }),
            to_json_value(authentication_data).unwrap()
        );
    }

    #[test]
    fn deserialize_authentication_data_direct_request() {
        let json = json!({
            "type": "example.type.foo",
            "session": "opaque_session_id",
            "example_credential": "verypoorsharedsecret",
        });

        assert_matches!(
            from_json_value(json).unwrap(),
            IncomingAuthData::DirectRequest { kind, session: Some(session), auth_parameters }
            if kind == "example.type.foo"
                && session == "opaque_session_id"
                && auth_parameters == btreemap!{
                    "example_credential".to_owned() => json!("verypoorsharedsecret")
                }
        );
    }

    #[test]
    fn serialize_authentication_data_fallback() {
        let authentication_data = AuthData::FallbackAcknowledgement { session: "ZXY000" };

        assert_eq!(json!({ "session": "ZXY000" }), to_json_value(authentication_data).unwrap());
    }

    #[test]
    fn deserialize_authentication_data_fallback() {
        let json = json!({ "session": "opaque_session_id" });

        assert_matches!(
            from_json_value(json).unwrap(),
            IncomingAuthData::FallbackAcknowledgement { session }
            if session == "opaque_session_id"
        );
    }

    #[test]
    fn serialize_uiaa_info() {
        let uiaa_info = UiaaInfo {
            flows: vec![AuthFlow {
                stages: vec!["m.login.password".into(), "m.login.dummy".into()],
            }],
            completed: vec!["m.login.password".into()],
            params: to_raw_json_value(&json!({
                "example.type.baz": {
                    "example_key": "foobar"
                }
            }))
            .unwrap(),
            session: None,
            auth_error: None,
        };

        let json = json!({
            "flows": [{ "stages": ["m.login.password", "m.login.dummy"] }],
            "completed": ["m.login.password"],
            "params": {
                "example.type.baz": {
                    "example_key": "foobar"
                }
            }
        });
        assert_eq!(to_json_value(uiaa_info).unwrap(), json);
    }

    #[test]
    fn deserialize_uiaa_info() {
        let json = json!({
            "errcode": "M_FORBIDDEN",
            "error": "Invalid password",
            "completed": ["example.type.foo"],
            "flows": [
                {
                    "stages": ["example.type.foo", "example.type.bar"]
                },
                {
                    "stages": ["example.type.foo", "example.type.baz"]
                }
            ],
            "params": {
                "example.type.baz": {
                    "example_key": "foobar"
                }
            },
            "session": "xxxxxx"
        });

        assert_matches!(
            from_json_value::<UiaaInfo>(json).unwrap(),
            UiaaInfo {
                auth_error: Some(ErrorBody {
                    kind: ErrorKind::Forbidden,
                    message: error_message,
                }),
                completed,
                flows,
                params,
                session: Some(session),
            } if error_message == "Invalid password"
                && completed == vec!["example.type.foo".to_owned()]
                && flows == vec![
                    AuthFlow {
                        stages: vec![
                            "example.type.foo".into(),
                            "example.type.bar".into(),
                        ],
                    },
                    AuthFlow {
                        stages: vec![
                            "example.type.foo".into(),
                            "example.type.baz".into(),
                        ],
                    },
                ]
                && from_json_str::<JsonValue>(params.get()).unwrap() == json!({
                    "example.type.baz": {
                        "example_key": "foobar"
                    }
                })
                && session == "xxxxxx"
        );
    }

    #[test]
    fn try_uiaa_response_into_http_response() {
        let uiaa_info = UiaaInfo {
            flows: vec![AuthFlow {
                stages: vec!["m.login.password".into(), "m.login.dummy".into()],
            }],
            completed: vec!["m.login.password".into()],
            params: to_raw_json_value(&json!({
                "example.type.baz": {
                    "example_key": "foobar"
                }
            }))
            .unwrap(),
            session: None,
            auth_error: None,
        };
        let uiaa_response = UiaaResponse::AuthResponse(uiaa_info).try_into_http_response().unwrap();

        assert_matches!(
            from_json_slice::<UiaaInfo>(uiaa_response.body()).unwrap(),
            UiaaInfo {
                flows,
                completed,
                params,
                session: None,
                auth_error: None,
            } if flows == vec![AuthFlow {
                    stages: vec!["m.login.password".into(), "m.login.dummy".into()],
                }]
                && completed == vec!["m.login.password".to_owned()]
                && from_json_str::<JsonValue>(params.get()).unwrap() == json!({
                    "example.type.baz": {
                        "example_key": "foobar"
                    }
                })
        );
        assert_eq!(uiaa_response.status(), http::status::StatusCode::UNAUTHORIZED);
    }

    #[test]
    fn try_uiaa_response_from_http_response() {
        let json = serde_json::to_string(&json!({
            "errcode": "M_FORBIDDEN",
            "error": "Invalid password",
            "completed": [ "example.type.foo" ],
            "flows": [
                {
                    "stages": [ "example.type.foo", "example.type.bar" ]
                },
                {
                    "stages": [ "example.type.foo", "example.type.baz" ]
                }
            ],
            "params": {
                "example.type.baz": {
                    "example_key": "foobar"
                }
            },
            "session": "xxxxxx"
        }))
        .unwrap();

        let http_response = http::Response::builder()
            .status(http::StatusCode::UNAUTHORIZED)
            .body(json.as_bytes())
            .unwrap();

        let parsed_uiaa_info = match UiaaResponse::try_from_http_response(http_response).unwrap() {
            UiaaResponse::AuthResponse(uiaa_info) => uiaa_info,
            _ => panic!("Expected UiaaResponse::AuthResponse"),
        };

        assert_matches!(
            parsed_uiaa_info,
            UiaaInfo {
                auth_error: Some(ErrorBody {
                    kind: ErrorKind::Forbidden,
                    message: error_message,
                }),
                completed,
                flows,
                params,
                session: Some(session),
            } if error_message == "Invalid password"
                && completed == vec!["example.type.foo".to_owned()]
                && flows == vec![
                    AuthFlow {
                        stages: vec![
                            "example.type.foo".into(),
                            "example.type.bar".into(),
                        ],
                    },
                    AuthFlow {
                        stages: vec![
                            "example.type.foo".into(),
                            "example.type.baz".into(),
                        ],
                    },
                ]
                && from_json_str::<JsonValue>(params.get()).unwrap() == json!({
                    "example.type.baz": {
                        "example_key": "foobar"
                    }
                })
                && session == "xxxxxx"
        );
    }
}
<|endoftext|>


<commit_before>use std::{
    collections::HashMap,
    ffi::OsString,
    fs::{create_dir_all, read_to_string},
    path::{Path, PathBuf},
};

use binary_node::{BinaryInstallation, BinaryTrait, NodeBinary};
use buildpack::{
    eyre,
    fs_utils::{copy_if_exists, move_dir_all, symlink_dir, symlink_file},
    hash_utils::str_sha256_hex,
    is_cnb_build, is_local_build,
    libcnb::{
        self,
        build::{BuildContext, BuildResult, BuildResultBuilder},
        data::{build_plan::BuildPlan, layer_content_metadata::LayerTypes, layer_name},
        detect::{DetectContext, DetectResult, DetectResultBuilder},
        generic::{GenericMetadata, GenericPlatform},
        layer::{ExistingLayerStrategy, Layer, LayerResult, LayerResultBuilder},
        layer_env::{LayerEnv, ModificationBehavior, Scope},
        Buildpack,
    },
    tracing, BuildpackTrait, LayerHashMetadata, LayerVersionMetadata, SYSTEM_INSTALLED,
};

pub struct NodeBuildpack;

impl BuildpackTrait for NodeBuildpack {
    fn toml() -> &'static str {
        include_str!("../buildpack.toml")
    }
}

const NODE_MODULES: &str = "node_modules";
const NVMRC: &str = ".nvmrc";
const PACKAGE_JSON: &str = "package.json";
const PACKAGE_LOCK: &str = "package-lock.json";
const TOOL_VERSIONS: &str = ".tool-versions";

impl Buildpack for NodeBuildpack {
    type Platform = GenericPlatform;
    type Metadata = GenericMetadata;
    type Error = eyre::Report;

    fn detect(&self, _context: DetectContext<Self>) -> libcnb::Result<DetectResult, Self::Error> {
        // Read `.tool-versions` for Node.js version
        let tool_versions = Self::tool_versions();

        // Read `.nvmrc` for Node.js version
        let nvmrc = read_to_string(NVMRC)
            .map(|content| content.trim().to_string())
            .ok();

        // Read `package.json` for Node.js version
        let package_json = read_to_string(PACKAGE_JSON)
            .ok()
            .and_then(|json| serde_json::from_str::<serde_json::Value>(&json).ok());

        // Detect `package-lock.json`
        let package_lock = PathBuf::from(PACKAGE_LOCK);

        // Fail early
        if !(tool_versions.contains_key("nodejs")
            || tool_versions.contains_key("node")
            || package_json.is_some()
            || package_lock.exists()
            || nvmrc.is_some()
            || Self::any_exist(&["main.js", "index.js"]))
        {
            return DetectResultBuilder::fail().build();
        }

        let mut requires = Vec::new();
        let mut provides = Vec::new();

        // Resolve Node.js version from `.tool-versions`, `.nvmrc`, `package.json`, or installed `node` version
        let (version, source) = if let Some(version) = tool_versions
            .get("nodejs")
            .or_else(|| tool_versions.get("node"))
        {
            (version.to_string(), TOOL_VERSIONS)
        } else if let Some(versionish) = nvmrc {
            (versionish, NVMRC)
        } else if let Some(semver) = package_json.as_ref().and_then(|package| {
            package
                .pointer("/engines/node")
                .and_then(|semver| semver.as_str().map(|semver| semver.to_string()))
        }) {
            (semver, PACKAGE_JSON)
        } else if let Some(version) = (NodeBinary {}).installed_version(None) {
            (version, SYSTEM_INSTALLED)
        } else {
            ("".to_string(), "")
        };

        // Require and provide Node.js
        let (require, provide) = Self::require_and_provide(
            format!("node {}", version).trim(),
            source,
            format!("Install Node.js {}", version).trim(),
        );
        requires.push(require);
        provides.push(provide);

        // Determine how NPM packages are to be installed
        if package_lock.exists() || package_json.is_some() {
            let (require, provide) = Self::require_and_provide(
                "node_modules",
                if package_lock.exists() {
                    PACKAGE_LOCK
                } else {
                    PACKAGE_JSON
                },
                "Install Node.js packages into `node_modules`",
            );
            requires.push(require);
            provides.push(provide);
        }

        let mut build_plan = BuildPlan::new();
        build_plan.requires = requires;
        build_plan.provides = provides;
        DetectResultBuilder::pass().build_plan(build_plan).build()
    }

    fn build(&self, context: BuildContext<Self>) -> libcnb::Result<BuildResult, Self::Error> {
        let entries: HashMap<_, _> = context
            .buildpack_plan
            .entries
            .iter()
            .map(|entry| Self::split_entry_name(&entry.name))
            .collect();

        if let Some(args) = entries.get("node") {
            context.handle_layer(layer_name!("node"), NodeLayer::new(args.clone()))?;
        }

        if entries.contains_key("node_modules") {
            context.handle_layer(layer_name!("node_modules"), NodeModulesLayer)?;
        }

        BuildResultBuilder::new().build()
    }
}

struct NodeLayer {
    /// The semantic version requirement for the `node` binary
    requirement: String,
}

impl NodeLayer {
    fn new(args: Vec<String>) -> Self {
        // Join args with commas because semver requirement parser expects that is
        // how parts of a requirement are separated
        let versionish = args.join(",");

        // Determine the semver requirement from versionish which could be a well formed semantic version (e.g 14.0.1),
        // a semver requirement (e.g. ^14.0), or an alias (e.g. `lts`)
        let requirement = if let Some(version) = versionish.strip_prefix('v') {
            version.to_string()
        } else if versionish == "lts" {
            // TODO: Determine LTS without doing a fetch, perhaps based on date
            // https://nodejs.org/en/about/releases/
            "^16".to_string()
        } else {
            versionish.clone()
        };

        NodeLayer { requirement }
    }
}

impl Layer for NodeLayer {
    type Buildpack = NodeBuildpack;
    type Metadata = LayerVersionMetadata;

    fn types(&self) -> LayerTypes {
        LayerTypes {
            build: true,
            launch: true,
            cache: true,
        }
    }

    fn existing_layer_strategy(
        &self,
        _context: &BuildContext<Self::Buildpack>,
        layer_data: &libcnb::layer::LayerData<Self::Metadata>,
    ) -> Result<libcnb::layer::ExistingLayerStrategy, <Self::Buildpack as Buildpack>::Error> {
        let installed = NodeBinary {}.semver_version_matches(
            &layer_data.content_metadata.metadata.version,
            &self.requirement,
        )?;

        let strategy = if installed {
            tracing::info!(
                "Existing `node` layer has `./bin/node` matching semver requirement `{}`; will keep",
                self.requirement
            );
            ExistingLayerStrategy::Keep
        } else {
            tracing::info!(
                "Existing `node` layer does not have `./bin/node` matching semver requirement `{}`; will recreate",
                self.requirement
            );
            ExistingLayerStrategy::Recreate
        };
        Ok(strategy)
    }

    fn create(
        &self,
        context: &BuildContext<Self::Buildpack>,
        layer_path: &Path,
    ) -> Result<LayerResult<Self::Metadata>, eyre::Report> {
        tracing::info!(
            "Creating `node` layer with semver requirement `{}`",
            self.requirement
        );

        let node = NodeBinary {}.require_sync(Some(self.requirement.clone()), true)?;
        let version = node.version()?.to_string();

        if is_local_build(context) {
            if node.is_stencila_install() {
                tracing::info!("Linking to `node {}` installed by Stencila", version);
                let source = node.grandparent()?;

                symlink_dir(source.join("bin"), &layer_path.join("bin"))?;
                symlink_dir(source.join("lib"), &layer_path.join("lib"))?;
            } else {
                tracing::info!("Linking to `node {}` installed on system", version);
                let source = node.parent()?;

                let bin_path = layer_path.join("bin");
                create_dir_all(&bin_path)?;
                symlink_file(node.path, bin_path.join(node.name))?;
                symlink_file(source.join("corepack"), bin_path.join("corepack"))?;
                symlink_file(source.join("npm"), bin_path.join("npm"))?;
                symlink_file(source.join("npx"), bin_path.join("npx"))?;

                let lib_path = layer_path.join("lib");
                create_dir_all(&lib_path)?;
                symlink_dir(
                    source.join("..").join("lib").join(NODE_MODULES),
                    lib_path.join(NODE_MODULES),
                )?;
            }
        } else {
            #[allow(clippy::collapsible_else_if)]
            if node.is_stencila_install() {
                tracing::info!("Moving `node {}` installed by Stencila", version);
                let source = node.grandparent()?;

                move_dir_all(&source, layer_path)?;
            } else {
                tracing::info!("Linking to `node {}` installed on stack image", version);
                let source = node.grandparent()?;

                symlink_dir(source.join("bin"), &layer_path.join("bin"))?;
                symlink_dir(source.join("lib"), &layer_path.join("lib"))?;
            }
        }

        LayerResultBuilder::new(LayerVersionMetadata { version }).build()
    }
}

struct NodeModulesLayer;

/// Generate `package_hash` string for an app directory
///
/// The hash is of the combined contens of `package-lock.json` and `package.json`.
/// This means that if either one is changed or removed that the hash will change.
fn generate_package_hash(app_dir: &Path) -> String {
    let content = [
        read_to_string(app_dir.join(PACKAGE_LOCK)).unwrap_or_default(),
        read_to_string(app_dir.join(PACKAGE_JSON)).unwrap_or_default(),
    ]
    .concat();
    str_sha256_hex(&content)
}

impl Layer for NodeModulesLayer {
    type Buildpack = NodeBuildpack;
    type Metadata = LayerHashMetadata;

    fn types(&self) -> LayerTypes {
        LayerTypes {
            build: true,
            launch: true,
            cache: true,
        }
    }

    fn existing_layer_strategy(
        &self,
        context: &BuildContext<Self::Buildpack>,
        layer_data: &libcnb::layer::LayerData<Self::Metadata>,
    ) -> Result<libcnb::layer::ExistingLayerStrategy, <Self::Buildpack as Buildpack>::Error> {
        let package_hash = generate_package_hash(&context.app_dir);
        let strategy = if package_hash == layer_data.content_metadata.metadata.hash {
            tracing::info!("Existing `node_modules` layer has same package hash; will keep",);
            ExistingLayerStrategy::Keep
        } else {
            tracing::info!("Existing `node_modules` layer has different package hash; will update");
            ExistingLayerStrategy::Update
        };
        Ok(strategy)
    }

    fn create(
        &self,
        context: &BuildContext<Self::Buildpack>,
        layer_path: &Path,
    ) -> Result<LayerResult<Self::Metadata>, eyre::Report> {
        tracing::info!("Creating `node_modules` layer");
        self.install(context, layer_path)
    }

    fn update(
        &self,
        context: &BuildContext<Self::Buildpack>,
        layer_data: &libcnb::layer::LayerData<Self::Metadata>,
    ) -> Result<LayerResult<Self::Metadata>, <Self::Buildpack as Buildpack>::Error> {
        tracing::info!("Updating `node_modules` layer");
        self.install(context, &layer_data.path)
    }
}

impl NodeModulesLayer {
    fn install(
        &self,
        context: &BuildContext<NodeBuildpack>,
        layer_path: &Path,
    ) -> Result<LayerResult<LayerHashMetadata>, eyre::Report> {
        let app_path = &context.app_dir;

        // Use the `node` and `npm` binaries installed in the `node` layer
        let node_layer = layer_path
            .parent()
            .expect("Should have parent")
            .join("node");

        // Prepend to the PATH because some package install scripts
        // use `/usr/bin/env node` which looks for `node` on PATH
        let mut envs: Vec<(OsString, OsString)> = vec![(
            "PATH".into(),
            NodeBuildpack::prepend_path(&node_layer.join("bin"))?,
        )];

        // If this is a CNB build use `layer_path/cache` as the NPM cache
        if is_cnb_build(context) {
            envs.push(("NPM_CONFIG_CACHE".into(), layer_path.join("cache").into()));
        }

        // Call the `npm-cli.js` script installed in the `node` layer
        // This is done, rather than executing `bin/npm` directly, there are issues with node `require`
        // module resolution when the latter is done.
        let node = BinaryInstallation::new(
            "node".into(),
            node_layer.join("bin").join("node"),
            None,
            envs,
        );
        let npm = node_layer
            .join("lib")
            .join(NODE_MODULES)
            .join("npm")
            .join("bin")
            .join("npm-cli.js")
            .display()
            .to_string();

        let mut layer_env = LayerEnv::new();
        if is_local_build(context) {
            // Do the install in the app directory as normal
            node.run_sync(&[&npm, "install"])?;
        } else {
            // Do the install in the layer.
            // Alternative, more complicated approaches to this e.g. doing a local install and then copying
            // over to layers and/or symlinking are problematic.

            // Despite some confusion online it seems that at present it is necessary to copy over these
            // files when using `--prefix`
            copy_if_exists(app_path.join(PACKAGE_JSON), layer_path.join(PACKAGE_JSON))?;
            copy_if_exists(app_path.join(PACKAGE_LOCK), layer_path.join(PACKAGE_LOCK))?;

            node.run_sync(&[
                &npm,
                "install",
                "--prefix",
                &layer_path.display().to_string(),
            ])?;

            // Set the `NODE_PATH` so that the `node_modules` can be found
            layer_env.insert(
                Scope::All,
                ModificationBehavior::Prepend,
                "NODE_PATH",
                layer_path.join(NODE_MODULES),
            );
        }

        // Generate a 'package hash' to detect if layer is stale in `existing_layer_strategy()`
        let metadata = LayerHashMetadata {
            hash: generate_package_hash(app_path),
        };

        LayerResultBuilder::new(metadata).env(layer_env).build()
    }
}
<commit_msg>chore(Node buildpack): Use new call signatures<commit_after>use std::{
    collections::HashMap,
    ffi::OsString,
    fs::{create_dir_all, read_to_string},
    path::{Path, PathBuf},
};

use binary_node::{BinaryInstallation, BinaryTrait, NodeBinary};
use buildpack::{
    eyre,
    fs_utils::{copy_if_exists, move_dir_all, symlink_dir, symlink_file},
    hash_utils::str_sha256_hex,
    is_cnb_build, is_local_build,
    libcnb::{
        self,
        build::{BuildContext, BuildResult, BuildResultBuilder},
        data::{build_plan::BuildPlan, layer_content_metadata::LayerTypes, layer_name},
        detect::{DetectContext, DetectResult, DetectResultBuilder},
        generic::{GenericMetadata, GenericPlatform},
        layer::{ExistingLayerStrategy, Layer, LayerResult, LayerResultBuilder},
        layer_env::{LayerEnv, ModificationBehavior, Scope},
        Buildpack,
    },
    tracing, BuildpackTrait, LayerHashMetadata, LayerVersionMetadata, SYSTEM_INSTALLED,
};

pub struct NodeBuildpack;

impl BuildpackTrait for NodeBuildpack {
    fn toml() -> &'static str {
        include_str!("../buildpack.toml")
    }
}

const NODE_MODULES: &str = "node_modules";
const NVMRC: &str = ".nvmrc";
const PACKAGE_JSON: &str = "package.json";
const PACKAGE_LOCK: &str = "package-lock.json";
const TOOL_VERSIONS: &str = ".tool-versions";

impl Buildpack for NodeBuildpack {
    type Platform = GenericPlatform;
    type Metadata = GenericMetadata;
    type Error = eyre::Report;

    fn detect(&self, _context: DetectContext<Self>) -> libcnb::Result<DetectResult, Self::Error> {
        // Read `.tool-versions` for Node.js version
        let tool_versions = Self::tool_versions();

        // Read `.nvmrc` for Node.js version
        let nvmrc = read_to_string(NVMRC)
            .map(|content| content.trim().to_string())
            .ok();

        // Read `package.json` for Node.js version
        let package_json = read_to_string(PACKAGE_JSON)
            .ok()
            .and_then(|json| serde_json::from_str::<serde_json::Value>(&json).ok());

        // Detect `package-lock.json`
        let package_lock = PathBuf::from(PACKAGE_LOCK);

        // Fail early
        if !(tool_versions.contains_key("nodejs")
            || tool_versions.contains_key("node")
            || package_json.is_some()
            || package_lock.exists()
            || nvmrc.is_some()
            || Self::any_exist(&["main.js", "index.js"]))
        {
            return DetectResultBuilder::fail().build();
        }

        let mut requires = Vec::new();
        let mut provides = Vec::new();

        // Resolve Node.js version from `.tool-versions`, `.nvmrc`, `package.json`, or installed `node` version
        let (version, source) = if let Some(version) = tool_versions
            .get("nodejs")
            .or_else(|| tool_versions.get("node"))
        {
            (version.to_string(), TOOL_VERSIONS)
        } else if let Some(versionish) = nvmrc {
            (versionish, NVMRC)
        } else if let Some(semver) = package_json.as_ref().and_then(|package| {
            package
                .pointer("/engines/node")
                .and_then(|semver| semver.as_str().map(|semver| semver.to_string()))
        }) {
            (semver, PACKAGE_JSON)
        } else if let Some(version) = (NodeBinary {}).installed_version(None) {
            (version, SYSTEM_INSTALLED)
        } else {
            ("".to_string(), "")
        };

        // Require and provide Node.js
        let (require, provide) = Self::require_and_provide(
            format!("node {}", version).trim(),
            source,
            format!("Install Node.js {}", version).trim(),
        );
        requires.push(require);
        provides.push(provide);

        // Determine how NPM packages are to be installed
        if package_lock.exists() || package_json.is_some() {
            let (require, provide) = Self::require_and_provide(
                "node_modules",
                if package_lock.exists() {
                    PACKAGE_LOCK
                } else {
                    PACKAGE_JSON
                },
                "Install Node.js packages into `node_modules`",
            );
            requires.push(require);
            provides.push(provide);
        }

        let mut build_plan = BuildPlan::new();
        build_plan.requires = requires;
        build_plan.provides = provides;
        DetectResultBuilder::pass().build_plan(build_plan).build()
    }

    fn build(&self, context: BuildContext<Self>) -> libcnb::Result<BuildResult, Self::Error> {
        let entries: HashMap<_, _> = context
            .buildpack_plan
            .entries
            .iter()
            .map(|entry| Self::split_entry_name(&entry.name))
            .collect();

        if let Some(args) = entries.get("node") {
            context.handle_layer(layer_name!("node"), NodeLayer::new(args.clone()))?;
        }

        if entries.contains_key("node_modules") {
            context.handle_layer(layer_name!("node_modules"), NodeModulesLayer)?;
        }

        BuildResultBuilder::new().build()
    }
}

struct NodeLayer {
    /// The semantic version requirement for the `node` binary
    requirement: String,
}

impl NodeLayer {
    fn new(args: Vec<String>) -> Self {
        // Join args with commas because semver requirement parser expects that is
        // how parts of a requirement are separated
        let versionish = args.join(",");

        // Determine the semver requirement from versionish which could be a well formed semantic version (e.g 14.0.1),
        // a semver requirement (e.g. ^14.0), or an alias (e.g. `lts`)
        let requirement = if let Some(version) = versionish.strip_prefix('v') {
            version.to_string()
        } else if versionish == "lts" {
            // TODO: Determine LTS without doing a fetch, perhaps based on date
            // https://nodejs.org/en/about/releases/
            "^16".to_string()
        } else {
            versionish.clone()
        };

        NodeLayer { requirement }
    }
}

impl Layer for NodeLayer {
    type Buildpack = NodeBuildpack;
    type Metadata = LayerVersionMetadata;

    fn types(&self) -> LayerTypes {
        LayerTypes {
            build: true,
            launch: true,
            cache: true,
        }
    }

    fn existing_layer_strategy(
        &self,
        _context: &BuildContext<Self::Buildpack>,
        layer_data: &libcnb::layer::LayerData<Self::Metadata>,
    ) -> Result<libcnb::layer::ExistingLayerStrategy, <Self::Buildpack as Buildpack>::Error> {
        let installed = NodeBinary {}.semver_version_matches(
            &layer_data.content_metadata.metadata.version,
            &self.requirement,
        )?;

        let strategy = if installed {
            tracing::info!(
                "Existing `node` layer has `./bin/node` matching semver requirement `{}`; will keep",
                self.requirement
            );
            ExistingLayerStrategy::Keep
        } else {
            tracing::info!(
                "Existing `node` layer does not have `./bin/node` matching semver requirement `{}`; will recreate",
                self.requirement
            );
            ExistingLayerStrategy::Recreate
        };
        Ok(strategy)
    }

    fn create(
        &self,
        context: &BuildContext<Self::Buildpack>,
        layer_path: &Path,
    ) -> Result<LayerResult<Self::Metadata>, eyre::Report> {
        tracing::info!(
            "Creating `node` layer with semver requirement `{}`",
            self.requirement
        );

        let node = NodeBinary {}.require_sync(Some(self.requirement.clone()), true)?;
        let version = node.version()?.to_string();

        if is_local_build(context) {
            if node.is_stencila_install() {
                tracing::info!("Linking to `node {}` installed by Stencila", version);
                let source = node.grandparent()?;

                symlink_dir(source.join("bin"), &layer_path.join("bin"))?;
                symlink_dir(source.join("lib"), &layer_path.join("lib"))?;
            } else {
                tracing::info!("Linking to `node {}` installed on system", version);
                let source = node.parent()?;

                let bin_path = layer_path.join("bin");
                create_dir_all(&bin_path)?;
                symlink_file(node.path, bin_path.join(node.name))?;
                symlink_file(source.join("corepack"), bin_path.join("corepack"))?;
                symlink_file(source.join("npm"), bin_path.join("npm"))?;
                symlink_file(source.join("npx"), bin_path.join("npx"))?;

                let lib_path = layer_path.join("lib");
                create_dir_all(&lib_path)?;
                symlink_dir(
                    source.join("..").join("lib").join(NODE_MODULES),
                    lib_path.join(NODE_MODULES),
                )?;
            }
        } else {
            #[allow(clippy::collapsible_else_if)]
            if node.is_stencila_install() {
                tracing::info!("Moving `node {}` installed by Stencila", version);
                let source = node.grandparent()?;

                move_dir_all(&source, layer_path)?;
            } else {
                tracing::info!("Linking to `node {}` installed on stack image", version);
                let source = node.grandparent()?;

                symlink_dir(source.join("bin"), &layer_path.join("bin"))?;
                symlink_dir(source.join("lib"), &layer_path.join("lib"))?;
            }
        }

        LayerResultBuilder::new(LayerVersionMetadata { version }).build()
    }
}

struct NodeModulesLayer;

/// Generate `package_hash` string for an app directory
///
/// The hash is of the combined contens of `package-lock.json` and `package.json`.
/// This means that if either one is changed or removed that the hash will change.
fn generate_package_hash(app_dir: &Path) -> String {
    let content = [
        read_to_string(app_dir.join(PACKAGE_LOCK)).unwrap_or_default(),
        read_to_string(app_dir.join(PACKAGE_JSON)).unwrap_or_default(),
    ]
    .concat();
    str_sha256_hex(&content)
}

impl Layer for NodeModulesLayer {
    type Buildpack = NodeBuildpack;
    type Metadata = LayerHashMetadata;

    fn types(&self) -> LayerTypes {
        LayerTypes {
            build: true,
            launch: true,
            cache: true,
        }
    }

    fn existing_layer_strategy(
        &self,
        context: &BuildContext<Self::Buildpack>,
        layer_data: &libcnb::layer::LayerData<Self::Metadata>,
    ) -> Result<libcnb::layer::ExistingLayerStrategy, <Self::Buildpack as Buildpack>::Error> {
        let package_hash = generate_package_hash(&context.app_dir);
        let strategy = if package_hash == layer_data.content_metadata.metadata.hash {
            tracing::info!("Existing `node_modules` layer has same package hash; will keep",);
            ExistingLayerStrategy::Keep
        } else {
            tracing::info!("Existing `node_modules` layer has different package hash; will update");
            ExistingLayerStrategy::Update
        };
        Ok(strategy)
    }

    fn create(
        &self,
        context: &BuildContext<Self::Buildpack>,
        layer_path: &Path,
    ) -> Result<LayerResult<Self::Metadata>, eyre::Report> {
        tracing::info!("Creating `node_modules` layer");
        self.install(context, layer_path)
    }

    fn update(
        &self,
        context: &BuildContext<Self::Buildpack>,
        layer_data: &libcnb::layer::LayerData<Self::Metadata>,
    ) -> Result<LayerResult<Self::Metadata>, <Self::Buildpack as Buildpack>::Error> {
        tracing::info!("Updating `node_modules` layer");
        self.install(context, &layer_data.path)
    }
}

impl NodeModulesLayer {
    fn install(
        &self,
        context: &BuildContext<NodeBuildpack>,
        layer_path: &Path,
    ) -> Result<LayerResult<LayerHashMetadata>, eyre::Report> {
        let app_path = &context.app_dir;

        // Use the `node` and `npm` binaries installed in the `node` layer
        let node_layer = layer_path
            .parent()
            .expect("Should have parent")
            .join("node");

        // Prepend to the PATH because some package install scripts
        // use `/usr/bin/env node` which looks for `node` on PATH
        let mut envs: Vec<(OsString, OsString)> = vec![(
            "PATH".into(),
            NodeBuildpack::prepend_path(&node_layer.join("bin"))?,
        )];

        // If this is a CNB build use `layer_path/cache` as the NPM cache
        if is_cnb_build(context) {
            envs.push(("NPM_CONFIG_CACHE".into(), layer_path.join("cache").into()));
        }

        // Call the `npm-cli.js` script installed in the `node` layer
        // This is done, rather than executing `bin/npm` directly, there are issues with node `require`
        // module resolution when the latter is done.
        let node = BinaryInstallation::new(
            "node".into(),
            node_layer.join("bin").join("node"),
            None,
            envs,
        );
        let npm = node_layer
            .join("lib")
            .join(NODE_MODULES)
            .join("npm")
            .join("bin")
            .join("npm-cli.js")
            .into_os_string();

        let mut layer_env = LayerEnv::new();
        if is_local_build(context) {
            // Do the install in the app directory as normal
            node.run_sync(&[npm, "install".into()])?;
        } else {
            // Do the install in the layer.
            // Alternative, more complicated approaches to this e.g. doing a local install and then copying
            // over to layers and/or symlinking are problematic.

            // Despite some confusion online it seems that at present it is necessary to copy over these
            // files when using `--prefix`
            copy_if_exists(app_path.join(PACKAGE_JSON), layer_path.join(PACKAGE_JSON))?;
            copy_if_exists(app_path.join(PACKAGE_LOCK), layer_path.join(PACKAGE_LOCK))?;

            node.run_sync(&[npm, "install".into(), "--prefix".into(), layer_path.into()])?;

            // Set the `NODE_PATH` so that the `node_modules` can be found
            layer_env.insert(
                Scope::All,
                ModificationBehavior::Prepend,
                "NODE_PATH",
                layer_path.join(NODE_MODULES),
            );
        }

        // Generate a 'package hash' to detect if layer is stale in `existing_layer_strategy()`
        let metadata = LayerHashMetadata {
            hash: generate_package_hash(app_path),
        };

        LayerResultBuilder::new(metadata).env(layer_env).build()
    }
}
<|endoftext|>


<commit_before>//
// Copyright 2019 The Project Oak Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

//! Macro to derive standard boilerplate code for an Oak Node.

extern crate proc_macro;

use proc_macro::TokenStream;
use quote::quote;

/// Implements the necessary bindings to make the annotated struct act as a
/// gRPC-processing Oak Node.
///
/// May only be used on struct objects.  Assumes that the default pre-defined
/// port names (`"grpc_in"`) is used to identify the gRPC input channel.
///
/// At most one struct may be annotated with this, as it produces global symbols
/// that would otherwise conflict if implemented multiple times.
///
/// ```rust
/// extern crate oak;
/// extern crate protobuf;
/// use oak::grpc::OakNode;
/// use protobuf::ProtobufEnum;
///
/// #[derive(oak_derive::OakExports)]
/// struct Node;
///
/// impl OakNode for Node {
///     fn new() -> Self {
///         Node
///     }
///     fn invoke(&mut self, method: &str, req: &[u8], writer: oak::grpc::ChannelResponseWriter) {
///         /* ... */
///     }
/// }
/// ```
#[proc_macro_derive(OakExports)]
pub fn derive_oak_exports(input: TokenStream) -> TokenStream {
    let input = syn::parse_macro_input!(input as syn::DeriveInput);
    let name = input.ident;
    match input.data {
        syn::Data::Struct(_) => (),
        _ => panic!("#[derive(OakExports)] is only defined for structs"),
    };

    let expanded = quote! {
        #[no_mangle]
        pub extern "C" fn oak_main(handle: u64) -> i32 {
            // A panic in the Rust module code cannot safely pass through the FFI
            // boundary, so catch any panics here and translate to an error return.
            // https://doc.rust-lang.org/nomicon/ffi.html#ffi-and-panics
            std::panic::catch_unwind(||{
                let mut node = <#name>::new();
                oak::grpc::event_loop(node, oak::ReadHandle{ handle: oak::Handle::from_raw(handle) })
            })
            .unwrap_or(Err(oak::OakStatus::ERR_INTERNAL))
            .err()
            .unwrap_or(oak::OakStatus::OK)
            .value()
        }
    };

    TokenStream::from(expanded)
}
<commit_msg>oak_derive: forward to inner_main<commit_after>//
// Copyright 2019 The Project Oak Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

//! Macro to derive standard boilerplate code for an Oak Node.

extern crate proc_macro;

use proc_macro::TokenStream;
use quote::quote;

/// Implements the necessary bindings to make the annotated struct act as a
/// gRPC-processing Oak Node.
///
/// May only be used on struct objects.  Assumes that the default pre-defined
/// port names (`"grpc_in"`) is used to identify the gRPC input channel.
///
/// At most one struct may be annotated with this, as it produces global symbols
/// that would otherwise conflict if implemented multiple times.
///
/// ```rust
/// extern crate oak;
/// extern crate protobuf;
/// use oak::grpc::OakNode;
/// use protobuf::ProtobufEnum;
///
/// #[derive(oak_derive::OakExports)]
/// struct Node;
///
/// impl OakNode for Node {
///     fn new() -> Self {
///         Node
///     }
///     fn invoke(&mut self, method: &str, req: &[u8], writer: oak::grpc::ChannelResponseWriter) {
///         /* ... */
///     }
/// }
/// ```
#[proc_macro_derive(OakExports)]
pub fn derive_oak_exports(input: TokenStream) -> TokenStream {
    let input = syn::parse_macro_input!(input as syn::DeriveInput);
    let name = input.ident;
    match input.data {
        syn::Data::Struct(_) => (),
        _ => panic!("#[derive(OakExports)] is only defined for structs"),
    };

    let expanded = quote! {
        #[no_mangle]
        pub extern "C" fn oak_main(handle: u64) -> i32 {
            // A panic in the Rust module code cannot safely pass through the FFI
            // boundary, so catch any panics here and translate to an error return.
            // https://doc.rust-lang.org/nomicon/ffi.html#ffi-and-panics
            std::panic::catch_unwind(||{
                oak::set_panic_hook();
                inner_main(handle)
            })
            .unwrap_or(Err(oak::OakStatus::ERR_INTERNAL))
            .err()
            .unwrap_or(oak::OakStatus::OK)
            .value()
        }
        // Internal version of the main entrypoint, to allow testing without any
        // panic interception.
        pub fn inner_main(handle: u64) -> Result<(), oak::OakStatus> {
            let mut node = <#name>::new();
            oak::grpc::event_loop(node, oak::ReadHandle{ handle: oak::Handle::from_raw(handle) })
        }
    };

    TokenStream::from(expanded)
}
<|endoftext|>


<commit_before>use crate::connection::ConnectionRef;
use crate::lazy_pin::LazyPin;
use bytes::{Buf, Bytes};
use futures::StreamExt;
use std::io::{self, BufRead, Read};
use tokio_postgres::CopyOutStream;

/// The reader returned by the `copy_out` method.
pub struct CopyOutReader<'a> {
    pub(crate) connection: ConnectionRef<'a>,
    pub(crate) stream: LazyPin<CopyOutStream>,
    cur: Bytes,
}

impl<'a> CopyOutReader<'a> {
    pub(crate) fn new(connection: ConnectionRef<'a>, stream: CopyOutStream) -> CopyOutReader<'a> {
        CopyOutReader {
            connection,
            stream: LazyPin::new(stream),
            cur: Bytes::new(),
        }
    }
}

impl Read for CopyOutReader<'_> {
    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
        let b = self.fill_buf()?;
        let len = usize::min(buf.len(), b.len());
        buf[..len].copy_from_slice(&b[..len]);
        self.consume(len);
        Ok(len)
    }
}

impl BufRead for CopyOutReader<'_> {
    fn fill_buf(&mut self) -> io::Result<&[u8]> {
        if !self.cur.has_remaining() {
            let mut stream = self.stream.pinned();
            match self
                .connection
                .block_on({ async { stream.next().await.transpose() } })
            {
                Ok(Some(cur)) => self.cur = cur,
                Err(e) => return Err(io::Error::new(io::ErrorKind::Other, e)),
                Ok(None) => {}
            };
        }

        Ok(self.cur.bytes())
    }

    fn consume(&mut self, amt: usize) {
        self.cur.advance(amt);
    }
}
<commit_msg>Don't accidentally return early from CopyOutReader<commit_after>use crate::connection::ConnectionRef;
use crate::lazy_pin::LazyPin;
use bytes::{Buf, Bytes};
use futures::StreamExt;
use std::io::{self, BufRead, Read};
use tokio_postgres::CopyOutStream;

/// The reader returned by the `copy_out` method.
pub struct CopyOutReader<'a> {
    pub(crate) connection: ConnectionRef<'a>,
    pub(crate) stream: LazyPin<CopyOutStream>,
    cur: Bytes,
}

impl<'a> CopyOutReader<'a> {
    pub(crate) fn new(connection: ConnectionRef<'a>, stream: CopyOutStream) -> CopyOutReader<'a> {
        CopyOutReader {
            connection,
            stream: LazyPin::new(stream),
            cur: Bytes::new(),
        }
    }
}

impl Read for CopyOutReader<'_> {
    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
        let b = self.fill_buf()?;
        let len = usize::min(buf.len(), b.len());
        buf[..len].copy_from_slice(&b[..len]);
        self.consume(len);
        Ok(len)
    }
}

impl BufRead for CopyOutReader<'_> {
    fn fill_buf(&mut self) -> io::Result<&[u8]> {
        while !self.cur.has_remaining() {
            let mut stream = self.stream.pinned();
            match self
                .connection
                .block_on({ async { stream.next().await.transpose() } })
            {
                Ok(Some(cur)) => self.cur = cur,
                Err(e) => return Err(io::Error::new(io::ErrorKind::Other, e)),
                Ok(None) => break,
            };
        }

        Ok(self.cur.bytes())
    }

    fn consume(&mut self, amt: usize) {
        self.cur.advance(amt);
    }
}
<|endoftext|>


<commit_before>use std::collections::HashMap;
use std::marker;

use crate::core::Message;

use crate::descriptor::DescriptorProto;
use crate::descriptor::FileDescriptorProto;

use crate::reflect::accessor::FieldAccessor;
use crate::reflect::find_message_or_enum::find_message_or_enum;
use crate::reflect::find_message_or_enum::MessageOrEnum;
use crate::reflect::reflect_deep_eq::ReflectDeepEq;
use crate::reflect::FieldDescriptor;

use crate::json;

trait MessageFactory: Send + Sync + 'static {
    fn new_instance(&self) -> Box<dyn Message>;
    fn default_instance(&self) -> &dyn Message;
    fn clone(&self, message: &dyn Message) -> Box<dyn Message>;
    fn eq(&self, a: &dyn Message, b: &dyn Message) -> bool;
}

struct MessageFactoryImpl<M>(marker::PhantomData<M>);

impl<M> MessageFactory for MessageFactoryImpl<M>
where
    M: 'static + Message + Default + Clone + PartialEq,
{
    fn new_instance(&self) -> Box<dyn Message> {
        let m: M = Default::default();
        Box::new(m)
    }

    fn default_instance(&self) -> &dyn Message {
        M::default_instance() as &dyn Message
    }

    fn clone(&self, message: &dyn Message) -> Box<dyn Message> {
        let m: &M = message.downcast_ref().expect("wrong message type");
        Box::new(m.clone())
    }

    fn eq(&self, a: &dyn Message, b: &dyn Message) -> bool {
        let a: &M = a.downcast_ref().expect("wrong message type");
        let b: &M = b.downcast_ref().expect("wrong message type");
        a == b
    }
}

/// Dynamic representation of message type.
///
/// Used for reflection.
pub struct MessageDescriptor {
    full_name: String,
    file_descriptor_proto: &'static FileDescriptorProto,
    proto: &'static DescriptorProto,
    factory: &'static dyn MessageFactory,
    fields: Vec<FieldDescriptor>,

    index_by_name: HashMap<String, usize>,
    index_by_name_or_json_name: HashMap<String, usize>,
    index_by_number: HashMap<u32, usize>,
}

impl MessageDescriptor {
    /// Get underlying `DescriptorProto` object.
    pub fn get_proto(&self) -> &DescriptorProto {
        self.proto
    }

    /// Get a message descriptor for given message type
    pub fn for_type<M: Message>() -> &'static MessageDescriptor {
        M::descriptor_static()
    }

    fn compute_full_name(package: &str, path_to_package: &str, proto: &DescriptorProto) -> String {
        let mut full_name = package.to_owned();
        if path_to_package.len() != 0 {
            if full_name.len() != 0 {
                full_name.push('.');
            }
            full_name.push_str(path_to_package);
        }
        if full_name.len() != 0 {
            full_name.push('.');
        }
        full_name.push_str(proto.get_name());
        full_name
    }

    // Non-generic part of `new` is a separate function
    // to reduce code bloat from multiple instantiations.
    fn new_non_generic(
        protobuf_name_to_package: &'static str,
        fields: Vec<FieldAccessor>,
        file_descriptor_proto: &'static FileDescriptorProto,
        factory: &'static dyn MessageFactory,
    ) -> MessageDescriptor {
        let (path_to_package, proto) =
            match find_message_or_enum(file_descriptor_proto, protobuf_name_to_package) {
                (path_to_package, MessageOrEnum::Message(m)) => (path_to_package, m),
                (_, MessageOrEnum::Enum(_)) => panic!("not a message"),
            };

        let mut field_proto_by_name = HashMap::new();
        for field_proto in &proto.field {
            field_proto_by_name.insert(field_proto.get_name(), field_proto);
        }

        let mut index_by_name = HashMap::new();
        let mut index_by_name_or_json_name = HashMap::new();
        let mut index_by_number = HashMap::new();
        for (i, f) in proto.field.iter().enumerate() {
            assert!(index_by_number.insert(f.get_number() as u32, i).is_none());
            assert!(index_by_name.insert(f.get_name().to_owned(), i).is_none());
            assert!(index_by_name_or_json_name
                .insert(f.get_name().to_owned(), i)
                .is_none());

            let json_name = json::json_name(f.get_name());

            if !f.get_json_name().is_empty() {
                assert_eq!(json_name, f.get_json_name());
            }

            if json_name != f.get_name() {
                assert!(index_by_name_or_json_name.insert(json_name, i).is_none());
            }
        }

        MessageDescriptor {
            full_name: MessageDescriptor::compute_full_name(
                file_descriptor_proto.get_package(),
                &path_to_package,
                &proto,
            ),
            proto,
            factory,
            fields: fields
                .into_iter()
                .map(|f| {
                    let proto = *field_proto_by_name.get(f.name).unwrap();
                    FieldDescriptor::new(f, proto)
                })
                .collect(),
            index_by_name,
            index_by_name_or_json_name,
            index_by_number,
            file_descriptor_proto,
        }
    }

    /// Construct a new message descriptor.
    ///
    /// This operation is called from generated code and rarely
    /// need to be called directly.
    pub fn new<M: 'static + Message + Default + Clone + PartialEq>(
        protobuf_name_to_package: &'static str,
        fields: Vec<FieldAccessor>,
        file_descriptor_proto: &'static FileDescriptorProto,
    ) -> MessageDescriptor {
        let factory = &MessageFactoryImpl(marker::PhantomData::<M>);
        MessageDescriptor::new_non_generic(
            protobuf_name_to_package,
            fields,
            file_descriptor_proto,
            factory,
        )
    }

    /// `FileDescriptorProto` containg this message type
    pub fn file_descriptor_proto(&self) -> &FileDescriptorProto {
        self.file_descriptor_proto
    }

    /// New empty message
    pub fn new_instance(&self) -> Box<dyn Message> {
        self.factory.new_instance()
    }

    /// Shared immutable empty message
    pub fn default_instance(&self) -> &dyn Message {
        self.factory.default_instance()
    }

    /// Clone a message
    pub fn clone(&self, message: &dyn Message) -> Box<dyn Message> {
        self.factory.clone(message)
    }

    /// Check if two messages equal.
    ///
    /// # Panics
    ///
    /// Is any message has different type than this descriptor.
    pub fn eq(&self, a: &dyn Message, b: &dyn Message) -> bool {
        self.factory.eq(a, b)
    }

    /// Similar to `eq`, but considers `NaN` values equal.
    ///
    /// # Panics
    ///
    /// Is any message has different type than this descriptor.
    pub fn deep_eq(&self, a: &dyn Message, b: &dyn Message) -> bool {
        // Explicitly force panic even if field list is empty
        assert_eq!(
            self as *const MessageDescriptor,
            a.descriptor() as *const MessageDescriptor
        );
        assert_eq!(
            self as *const MessageDescriptor,
            b.descriptor() as *const MessageDescriptor
        );

        for field in self.fields() {
            let af = field.get_reflect(a);
            let bf = field.get_reflect(b);
            if !af.reflect_deep_eq(&bf) {
                return false;
            }
        }
        true
    }

    /// Message name as given in `.proto` file
    pub fn name(&self) -> &'static str {
        self.proto.get_name()
    }

    /// Fully qualified protobuf message name
    pub fn full_name(&self) -> &str {
        &self.full_name[..]
    }

    /// Message field descriptors.
    pub fn fields(&self) -> &[FieldDescriptor] {
        &self.fields
    }

    /// Find message field by protobuf field name
    ///
    /// Note: protobuf field name might be different for Rust field name.
    pub fn field_by_name(&self, name: &str) -> Option<&FieldDescriptor> {
        let &index = self.index_by_name.get(name)?;
        Some(&self.fields[index])
    }

    /// Find message field by field name or field JSON name
    pub fn field_by_name_or_json_name<'a>(&'a self, name: &str) -> Option<&'a FieldDescriptor> {
        let &index = self.index_by_name_or_json_name.get(name)?;
        Some(&self.fields[index])
    }

    /// Find message field by field name
    pub fn field_by_number<'a>(&'a self, number: u32) -> Option<&'a FieldDescriptor> {
        let &index = self.index_by_number.get(&number)?;
        Some(&self.fields[index])
    }

    /// This operation is not needed here
    // TODO: remove it
    pub fn cast<M: 'static>(&self, message: Box<dyn Message>) -> Result<M, Box<dyn Message>> {
        message.downcast_box::<M>().map(|m| *m)
    }
}

/// Identity comparison: message descriptor are equal if their addresses are equal
impl PartialEq for MessageDescriptor {
    fn eq(&self, other: &MessageDescriptor) -> bool {
        self as *const MessageDescriptor == other as *const MessageDescriptor
    }
}
<commit_msg>Mark MessageDescriptor::new function non-public<commit_after>use std::collections::HashMap;
use std::marker;

use crate::core::Message;

use crate::descriptor::DescriptorProto;
use crate::descriptor::FileDescriptorProto;

use crate::reflect::accessor::FieldAccessor;
use crate::reflect::find_message_or_enum::find_message_or_enum;
use crate::reflect::find_message_or_enum::MessageOrEnum;
use crate::reflect::reflect_deep_eq::ReflectDeepEq;
use crate::reflect::FieldDescriptor;

use crate::json;

trait MessageFactory: Send + Sync + 'static {
    fn new_instance(&self) -> Box<dyn Message>;
    fn default_instance(&self) -> &dyn Message;
    fn clone(&self, message: &dyn Message) -> Box<dyn Message>;
    fn eq(&self, a: &dyn Message, b: &dyn Message) -> bool;
}

struct MessageFactoryImpl<M>(marker::PhantomData<M>);

impl<M> MessageFactory for MessageFactoryImpl<M>
where
    M: 'static + Message + Default + Clone + PartialEq,
{
    fn new_instance(&self) -> Box<dyn Message> {
        let m: M = Default::default();
        Box::new(m)
    }

    fn default_instance(&self) -> &dyn Message {
        M::default_instance() as &dyn Message
    }

    fn clone(&self, message: &dyn Message) -> Box<dyn Message> {
        let m: &M = message.downcast_ref().expect("wrong message type");
        Box::new(m.clone())
    }

    fn eq(&self, a: &dyn Message, b: &dyn Message) -> bool {
        let a: &M = a.downcast_ref().expect("wrong message type");
        let b: &M = b.downcast_ref().expect("wrong message type");
        a == b
    }
}

/// Dynamic representation of message type.
///
/// Used for reflection.
pub struct MessageDescriptor {
    full_name: String,
    file_descriptor_proto: &'static FileDescriptorProto,
    proto: &'static DescriptorProto,
    factory: &'static dyn MessageFactory,
    fields: Vec<FieldDescriptor>,

    index_by_name: HashMap<String, usize>,
    index_by_name_or_json_name: HashMap<String, usize>,
    index_by_number: HashMap<u32, usize>,
}

impl MessageDescriptor {
    /// Get underlying `DescriptorProto` object.
    pub fn get_proto(&self) -> &DescriptorProto {
        self.proto
    }

    /// Get a message descriptor for given message type
    pub fn for_type<M: Message>() -> &'static MessageDescriptor {
        M::descriptor_static()
    }

    fn compute_full_name(package: &str, path_to_package: &str, proto: &DescriptorProto) -> String {
        let mut full_name = package.to_owned();
        if path_to_package.len() != 0 {
            if full_name.len() != 0 {
                full_name.push('.');
            }
            full_name.push_str(path_to_package);
        }
        if full_name.len() != 0 {
            full_name.push('.');
        }
        full_name.push_str(proto.get_name());
        full_name
    }

    // Non-generic part of `new` is a separate function
    // to reduce code bloat from multiple instantiations.
    fn new_non_generic(
        protobuf_name_to_package: &'static str,
        fields: Vec<FieldAccessor>,
        file_descriptor_proto: &'static FileDescriptorProto,
        factory: &'static dyn MessageFactory,
    ) -> MessageDescriptor {
        let (path_to_package, proto) =
            match find_message_or_enum(file_descriptor_proto, protobuf_name_to_package) {
                (path_to_package, MessageOrEnum::Message(m)) => (path_to_package, m),
                (_, MessageOrEnum::Enum(_)) => panic!("not a message"),
            };

        let mut field_proto_by_name = HashMap::new();
        for field_proto in &proto.field {
            field_proto_by_name.insert(field_proto.get_name(), field_proto);
        }

        let mut index_by_name = HashMap::new();
        let mut index_by_name_or_json_name = HashMap::new();
        let mut index_by_number = HashMap::new();
        for (i, f) in proto.field.iter().enumerate() {
            assert!(index_by_number.insert(f.get_number() as u32, i).is_none());
            assert!(index_by_name.insert(f.get_name().to_owned(), i).is_none());
            assert!(index_by_name_or_json_name
                .insert(f.get_name().to_owned(), i)
                .is_none());

            let json_name = json::json_name(f.get_name());

            if !f.get_json_name().is_empty() {
                assert_eq!(json_name, f.get_json_name());
            }

            if json_name != f.get_name() {
                assert!(index_by_name_or_json_name.insert(json_name, i).is_none());
            }
        }

        MessageDescriptor {
            full_name: MessageDescriptor::compute_full_name(
                file_descriptor_proto.get_package(),
                &path_to_package,
                &proto,
            ),
            proto,
            factory,
            fields: fields
                .into_iter()
                .map(|f| {
                    let proto = *field_proto_by_name.get(f.name).unwrap();
                    FieldDescriptor::new(f, proto)
                })
                .collect(),
            index_by_name,
            index_by_name_or_json_name,
            index_by_number,
            file_descriptor_proto,
        }
    }

    /// Construct a new message descriptor.
    ///
    /// This operation is called from generated code and rarely
    /// need to be called directly.
    ///
    /// This function is not a part of public API.
    #[doc(hidden)]
    pub fn new<M: 'static + Message + Default + Clone + PartialEq>(
        protobuf_name_to_package: &'static str,
        fields: Vec<FieldAccessor>,
        file_descriptor_proto: &'static FileDescriptorProto,
    ) -> MessageDescriptor {
        let factory = &MessageFactoryImpl(marker::PhantomData::<M>);
        MessageDescriptor::new_non_generic(
            protobuf_name_to_package,
            fields,
            file_descriptor_proto,
            factory,
        )
    }

    /// `FileDescriptorProto` containg this message type
    pub fn file_descriptor_proto(&self) -> &FileDescriptorProto {
        self.file_descriptor_proto
    }

    /// New empty message
    pub fn new_instance(&self) -> Box<dyn Message> {
        self.factory.new_instance()
    }

    /// Shared immutable empty message
    pub fn default_instance(&self) -> &dyn Message {
        self.factory.default_instance()
    }

    /// Clone a message
    pub fn clone(&self, message: &dyn Message) -> Box<dyn Message> {
        self.factory.clone(message)
    }

    /// Check if two messages equal.
    ///
    /// # Panics
    ///
    /// Is any message has different type than this descriptor.
    pub fn eq(&self, a: &dyn Message, b: &dyn Message) -> bool {
        self.factory.eq(a, b)
    }

    /// Similar to `eq`, but considers `NaN` values equal.
    ///
    /// # Panics
    ///
    /// Is any message has different type than this descriptor.
    pub fn deep_eq(&self, a: &dyn Message, b: &dyn Message) -> bool {
        // Explicitly force panic even if field list is empty
        assert_eq!(
            self as *const MessageDescriptor,
            a.descriptor() as *const MessageDescriptor
        );
        assert_eq!(
            self as *const MessageDescriptor,
            b.descriptor() as *const MessageDescriptor
        );

        for field in self.fields() {
            let af = field.get_reflect(a);
            let bf = field.get_reflect(b);
            if !af.reflect_deep_eq(&bf) {
                return false;
            }
        }
        true
    }

    /// Message name as given in `.proto` file
    pub fn name(&self) -> &'static str {
        self.proto.get_name()
    }

    /// Fully qualified protobuf message name
    pub fn full_name(&self) -> &str {
        &self.full_name[..]
    }

    /// Message field descriptors.
    pub fn fields(&self) -> &[FieldDescriptor] {
        &self.fields
    }

    /// Find message field by protobuf field name
    ///
    /// Note: protobuf field name might be different for Rust field name.
    pub fn field_by_name(&self, name: &str) -> Option<&FieldDescriptor> {
        let &index = self.index_by_name.get(name)?;
        Some(&self.fields[index])
    }

    /// Find message field by field name or field JSON name
    pub fn field_by_name_or_json_name<'a>(&'a self, name: &str) -> Option<&'a FieldDescriptor> {
        let &index = self.index_by_name_or_json_name.get(name)?;
        Some(&self.fields[index])
    }

    /// Find message field by field name
    pub fn field_by_number<'a>(&'a self, number: u32) -> Option<&'a FieldDescriptor> {
        let &index = self.index_by_number.get(&number)?;
        Some(&self.fields[index])
    }

    /// This operation is not needed here
    // TODO: remove it
    pub fn cast<M: 'static>(&self, message: Box<dyn Message>) -> Result<M, Box<dyn Message>> {
        message.downcast_box::<M>().map(|m| *m)
    }
}

/// Identity comparison: message descriptor are equal if their addresses are equal
impl PartialEq for MessageDescriptor {
    fn eq(&self, other: &MessageDescriptor) -> bool {
        self as *const MessageDescriptor == other as *const MessageDescriptor
    }
}
<|endoftext|>