/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ use crate::error::Result; use serde::Serialize; use std::io::{self, Write}; /// A writer that counts the number of bytes it's asked to write, and discards /// the data. Used to calculate the serialized size of the commands list. #[derive(Clone, Copy, Default)] pub struct WriteCount(usize); impl WriteCount { #[inline] pub fn len(self) -> usize { self.0 } } impl Write for WriteCount { #[inline] fn write(&mut self, buf: &[u8]) -> io::Result { self.0 += buf.len(); Ok(buf.len()) } #[inline] fn flush(&mut self) -> io::Result<()> { Ok(()) } } /// Returns the size of the given value, in bytes, when serialized to JSON. fn compute_serialized_size(value: &T) -> Result { let mut w = WriteCount::default(); serde_json::to_writer(&mut w, value)?; Ok(w.len()) } /// Truncates `list` to fit within `payload_size_max_bytes` when serialized to /// JSON. pub fn shrink_to_fit(list: &mut Vec, payload_size_max_bytes: usize) -> Result<()> { let size = compute_serialized_size(&list)?; // See bug 535326 comment 8 for an explanation of the estimation match ((payload_size_max_bytes / 4) * 3).checked_sub(1500) { Some(max_serialized_size) => { if size > max_serialized_size { // Estimate a little more than the direct fraction to maximize packing let cutoff = (list.len() * max_serialized_size - 1) / size + 1; list.truncate(cutoff + 1); // Keep dropping off the last entry until the data fits. while compute_serialized_size(&list)? > max_serialized_size { if list.pop().is_none() { break; } } } Ok(()) } None => { list.clear(); Ok(()) } } } #[cfg(test)] mod tests { use super::super::record::CommandRecord; use super::*; #[test] fn test_compute_serialized_size() { assert_eq!(compute_serialized_size(&1).unwrap(), 1); assert_eq!(compute_serialized_size(&"hi").unwrap(), 4); assert_eq!( compute_serialized_size(&["hi", "hello", "bye"]).unwrap(), 20 ); } #[test] fn test_shrink_to_fit() { let mut commands = vec![ CommandRecord { name: "wipeEngine".into(), args: vec!["bookmarks".into()], flow_id: Some("flow".into()), }, CommandRecord { name: "resetEngine".into(), args: vec!["history".into()], flow_id: Some("flow".into()), }, CommandRecord { name: "logout".into(), args: Vec::new(), flow_id: None, }, ]; // 4096 bytes is enough to fit all three commands. shrink_to_fit(&mut commands, 4096).unwrap(); assert_eq!(commands.len(), 3); let sizes = commands .iter() .map(|c| compute_serialized_size(c).unwrap()) .collect::>(); assert_eq!(sizes, &[61, 60, 30]); // `logout` won't fit within 2168 bytes. shrink_to_fit(&mut commands, 2168).unwrap(); assert_eq!(commands.len(), 2); // `resetEngine` won't fit within 2084 bytes. shrink_to_fit(&mut commands, 2084).unwrap(); assert_eq!(commands.len(), 1); // `wipeEngine` won't fit at all. shrink_to_fit(&mut commands, 1024).unwrap(); assert!(commands.is_empty()); } }