Skip to main content

iggy_binary_protocol/requests/users/
login_user.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18use crate::WireError;
19use crate::codec::{WireDecode, WireEncode, read_str, read_u8, read_u32_le};
20use crate::primitives::identifier::WireName;
21use bytes::{BufMut, BytesMut};
22
23/// `LoginUser` request.
24///
25/// Wire format:
26/// `[username_len:u8][username:N][password_len:u8][password:N]
27///  [version_len:u32_le][version:N?][context_len:u32_le][context:N?]`
28///
29/// Both `version_len` and `context_len` are always present on the wire.
30/// A length of 0 means the field is absent.
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct LoginUserRequest {
33    pub username: WireName,
34    pub password: String,
35    pub version: Option<String>,
36    pub context: Option<String>,
37}
38
39impl WireEncode for LoginUserRequest {
40    fn encoded_size(&self) -> usize {
41        self.username.encoded_size()
42            + 1
43            + self.password.len()
44            + 4
45            + self.version.as_ref().map_or(0, String::len)
46            + 4
47            + self.context.as_ref().map_or(0, String::len)
48    }
49
50    fn encode(&self, buf: &mut BytesMut) {
51        self.username.encode(buf);
52        debug_assert!(
53            u8::try_from(self.password.len()).is_ok(),
54            "password exceeds u8 length prefix; callers must validate before encoding"
55        );
56        #[allow(clippy::cast_possible_truncation)]
57        buf.put_u8(self.password.len() as u8);
58        buf.put_slice(self.password.as_bytes());
59        match &self.version {
60            Some(v) => {
61                #[allow(clippy::cast_possible_truncation)]
62                buf.put_u32_le(v.len() as u32);
63                buf.put_slice(v.as_bytes());
64            }
65            None => {
66                buf.put_u32_le(0);
67            }
68        }
69        match &self.context {
70            Some(c) => {
71                #[allow(clippy::cast_possible_truncation)]
72                buf.put_u32_le(c.len() as u32);
73                buf.put_slice(c.as_bytes());
74            }
75            None => {
76                buf.put_u32_le(0);
77            }
78        }
79    }
80}
81
82impl WireDecode for LoginUserRequest {
83    fn decode(buf: &[u8]) -> Result<(Self, usize), WireError> {
84        let (username, mut pos) = WireName::decode(buf)?;
85
86        let password_len = read_u8(buf, pos)? as usize;
87        pos += 1;
88        let password = read_str(buf, pos, password_len)?;
89        pos += password_len;
90
91        let version_len = read_u32_le(buf, pos)? as usize;
92        pos += 4;
93        let version = if version_len > 0 {
94            let v = read_str(buf, pos, version_len)?;
95            pos += version_len;
96            Some(v)
97        } else {
98            None
99        };
100
101        let context_len = read_u32_le(buf, pos)? as usize;
102        pos += 4;
103        let context = if context_len > 0 {
104            let c = read_str(buf, pos, context_len)?;
105            pos += context_len;
106            Some(c)
107        } else {
108            None
109        };
110
111        Ok((
112            Self {
113                username,
114                password,
115                version,
116                context,
117            },
118            pos,
119        ))
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    #[test]
128    fn roundtrip_full() {
129        let req = LoginUserRequest {
130            username: WireName::new("admin").unwrap(),
131            password: "secret".to_string(),
132            version: Some("1.0.0".to_string()),
133            context: Some("rust-sdk".to_string()),
134        };
135        let bytes = req.to_bytes();
136        let (decoded, consumed) = LoginUserRequest::decode(&bytes).unwrap();
137        assert_eq!(consumed, bytes.len());
138        assert_eq!(decoded, req);
139    }
140
141    #[test]
142    fn roundtrip_no_version_no_context() {
143        let req = LoginUserRequest {
144            username: WireName::new("user").unwrap(),
145            password: "pass".to_string(),
146            version: None,
147            context: None,
148        };
149        let bytes = req.to_bytes();
150        let (decoded, consumed) = LoginUserRequest::decode(&bytes).unwrap();
151        assert_eq!(consumed, bytes.len());
152        assert_eq!(decoded, req);
153    }
154
155    #[test]
156    fn roundtrip_version_only() {
157        let req = LoginUserRequest {
158            username: WireName::new("user").unwrap(),
159            password: "pw".to_string(),
160            version: Some("2.3.4".to_string()),
161            context: None,
162        };
163        let bytes = req.to_bytes();
164        let (decoded, consumed) = LoginUserRequest::decode(&bytes).unwrap();
165        assert_eq!(consumed, bytes.len());
166        assert_eq!(decoded, req);
167    }
168
169    #[test]
170    fn roundtrip_context_only() {
171        let req = LoginUserRequest {
172            username: WireName::new("user").unwrap(),
173            password: "pw".to_string(),
174            version: None,
175            context: Some("test-ctx".to_string()),
176        };
177        let bytes = req.to_bytes();
178        let (decoded, consumed) = LoginUserRequest::decode(&bytes).unwrap();
179        assert_eq!(consumed, bytes.len());
180        assert_eq!(decoded, req);
181    }
182
183    #[test]
184    fn encoded_size_matches_output() {
185        let req = LoginUserRequest {
186            username: WireName::new("admin").unwrap(),
187            password: "p".to_string(),
188            version: Some("v1".to_string()),
189            context: Some("ctx".to_string()),
190        };
191        assert_eq!(req.encoded_size(), req.to_bytes().len());
192    }
193
194    #[test]
195    fn truncated_returns_error() {
196        let req = LoginUserRequest {
197            username: WireName::new("u").unwrap(),
198            password: "p".to_string(),
199            version: Some("v".to_string()),
200            context: Some("c".to_string()),
201        };
202        let bytes = req.to_bytes();
203        for i in 0..bytes.len() {
204            assert!(
205                LoginUserRequest::decode(&bytes[..i]).is_err(),
206                "expected error for truncation at byte {i}"
207            );
208        }
209    }
210
211    #[test]
212    fn wire_layout_no_optionals() {
213        let req = LoginUserRequest {
214            username: WireName::new("u").unwrap(),
215            password: "p".to_string(),
216            version: None,
217            context: None,
218        };
219        let bytes = req.to_bytes();
220        // username: [1, b'u'] + password: [1, b'p'] + version_len: [0,0,0,0] + context_len: [0,0,0,0]
221        let expected: &[u8] = &[1, b'u', 1, b'p', 0, 0, 0, 0, 0, 0, 0, 0];
222        assert_eq!(&bytes[..], expected);
223    }
224}