| 1 | //! Ruby's Marshal format, version 4.8, written (never read): what the |
| 2 | //! RubyGems full index is made of. `specs.4.8.gz` is a marshalled array of |
| 3 | //! `[name, Gem::Version, platform]`, and each |
| 4 | //! `quick/Marshal.4.8/<gem>.gemspec.rz` a marshalled `Gem::Specification`. |
| 5 | //! |
| 6 | //! Only what those need is here: nil, booleans, small integers, strings |
| 7 | //! (UTF-8, or binary), symbols, arrays, hashes, plain objects with |
| 8 | //! instance variables, and the two kinds of custom dump RubyGems' classes |
| 9 | //! use (`marshal_dump`, which `Gem::Version` and `Gem::Requirement` use, |
| 10 | //! and `_dump`, which `Gem::Specification` uses). Symbols already written |
| 11 | //! are written again as links, as Ruby does; objects never are, which |
| 12 | //! Ruby reads the same. |
| 13 | |
| 14 | use std::collections::HashMap; |
| 15 | |
| 16 | /// A Ruby value to marshal. |
| 17 | #[derive(Clone, Debug, PartialEq)] |
| 18 | pub enum Value { |
| 19 | Nil, |
| 20 | Bool(bool), |
| 21 | /// An integer between -2^31 and 2^31, which Marshal writes as a Fixnum. |
| 22 | Int(i32), |
| 23 | /// A UTF-8 string. |
| 24 | Str(String), |
| 25 | /// A binary (ASCII-8BIT) string. |
| 26 | Bytes(Vec<u8>), |
| 27 | Symbol(String), |
| 28 | Array(Vec<Value>), |
| 29 | Hash(Vec<(Value, Value)>), |
| 30 | /// An object of `class` with these instance variables (`@name`). |
| 31 | Object { class: String, ivars: Vec<(String, Value)> }, |
| 32 | /// What `class#marshal_dump` returned, for `class.marshal_load`. |
| 33 | UserMarshal { class: String, data: Box<Value> }, |
| 34 | /// The bytes `class#_dump` returned, for `class._load`. |
| 35 | UserDef { class: String, data: Vec<u8> }, |
| 36 | } |
| 37 | |
| 38 | impl Value { |
| 39 | pub fn str(text: impl Into<String>) -> Value { |
| 40 | Value::Str(text.into()) |
| 41 | } |
| 42 | |
| 43 | /// A string, or nil for none. |
| 44 | pub fn opt(text: Option<&str>) -> Value { |
| 45 | text.map_or(Value::Nil, Value::str) |
| 46 | } |
| 47 | } |
| 48 | |
| 49 | /// `value`, marshalled, with the 4.8 header. |
| 50 | pub fn dump(value: &Value) -> Vec<u8> { |
| 51 | let mut writer = Writer { out: vec![4, 8], symbols: HashMap::new() }; |
| 52 | writer.value(value); |
| 53 | writer.out |
| 54 | } |
| 55 | |
| 56 | struct Writer { |
| 57 | out: Vec<u8>, |
| 58 | symbols: HashMap<String, usize>, |
| 59 | } |
| 60 | |
| 61 | impl Writer { |
| 62 | /// Marshal's integer: 0 as itself, -123..=122 in one byte offset by |
| 63 | /// five, else a byte count (negated for a negative) and the bytes, |
| 64 | /// least first. |
| 65 | fn long(&mut self, n: i64) { |
| 66 | if n == 0 { |
| 67 | self.out.push(0); |
| 68 | } else if (1..123).contains(&n) { |
| 69 | self.out.push((n + 5) as u8); |
| 70 | } else if (-123..0).contains(&n) { |
| 71 | self.out.push(((n - 5) & 0xff) as u8); |
| 72 | } else { |
| 73 | let mut bytes = Vec::new(); |
| 74 | let mut rest = n; |
| 75 | for _ in 0..4 { |
| 76 | bytes.push((rest & 0xff) as u8); |
| 77 | rest >>= 8; |
| 78 | if (n > 0 && rest == 0) || (n < 0 && rest == -1) { |
| 79 | break; |
| 80 | } |
| 81 | } |
| 82 | let count = bytes.len() as i64; |
| 83 | self.out.push(if n > 0 { count as u8 } else { (-count & 0xff) as u8 }); |
| 84 | self.out.extend_from_slice(&bytes); |
| 85 | } |
| 86 | } |
| 87 | |
| 88 | fn bytes(&mut self, bytes: &[u8]) { |
| 89 | self.long(bytes.len() as i64); |
| 90 | self.out.extend_from_slice(bytes); |
| 91 | } |
| 92 | |
| 93 | fn symbol(&mut self, name: &str) { |
| 94 | if let Some(&index) = self.symbols.get(name) { |
| 95 | self.out.push(b';'); |
| 96 | self.long(index as i64); |
| 97 | return; |
| 98 | } |
| 99 | let index = self.symbols.len(); |
| 100 | self.symbols.insert(name.to_owned(), index); |
| 101 | self.out.push(b':'); |
| 102 | self.bytes(name.as_bytes()); |
| 103 | } |
| 104 | |
| 105 | fn value(&mut self, value: &Value) { |
| 106 | match value { |
| 107 | Value::Nil => self.out.push(b'0'), |
| 108 | Value::Bool(true) => self.out.push(b'T'), |
| 109 | Value::Bool(false) => self.out.push(b'F'), |
| 110 | Value::Int(n) => { |
| 111 | self.out.push(b'i'); |
| 112 | self.long(i64::from(*n)); |
| 113 | } |
| 114 | // A string with an encoding is a string with one instance |
| 115 | // variable, `E`: true for UTF-8. |
| 116 | Value::Str(text) => { |
| 117 | self.out.push(b'I'); |
| 118 | self.out.push(b'"'); |
| 119 | self.bytes(text.as_bytes()); |
| 120 | self.long(1); |
| 121 | self.symbol("E"); |
| 122 | self.out.push(b'T'); |
| 123 | } |
| 124 | Value::Bytes(bytes) => { |
| 125 | self.out.push(b'"'); |
| 126 | self.bytes(bytes); |
| 127 | } |
| 128 | Value::Symbol(name) => self.symbol(name), |
| 129 | Value::Array(items) => { |
| 130 | self.out.push(b'['); |
| 131 | self.long(items.len() as i64); |
| 132 | for item in items { |
| 133 | self.value(item); |
| 134 | } |
| 135 | } |
| 136 | Value::Hash(pairs) => { |
| 137 | self.out.push(b'{'); |
| 138 | self.long(pairs.len() as i64); |
| 139 | for (key, item) in pairs { |
| 140 | self.value(key); |
| 141 | self.value(item); |
| 142 | } |
| 143 | } |
| 144 | Value::Object { class, ivars } => { |
| 145 | self.out.push(b'o'); |
| 146 | self.symbol(class); |
| 147 | self.long(ivars.len() as i64); |
| 148 | for (name, item) in ivars { |
| 149 | self.symbol(name); |
| 150 | self.value(item); |
| 151 | } |
| 152 | } |
| 153 | Value::UserMarshal { class, data } => { |
| 154 | self.out.push(b'U'); |
| 155 | self.symbol(class); |
| 156 | self.value(data); |
| 157 | } |
| 158 | Value::UserDef { class, data } => { |
| 159 | self.out.push(b'u'); |
| 160 | self.symbol(class); |
| 161 | self.bytes(data); |
| 162 | } |
| 163 | } |
| 164 | } |
| 165 | } |
| 166 | |
| 167 | #[cfg(test)] |
| 168 | mod tests { |
| 169 | use super::*; |
| 170 | |
| 171 | #[test] |
| 172 | fn values_are_written_as_ruby_writes_them() { |
| 173 | // Each as `Marshal.dump` writes it in Ruby 3.3. |
| 174 | assert_eq!(dump(&Value::Nil), b"\x04\x080"); |
| 175 | assert_eq!(dump(&Value::Bool(true)), b"\x04\x08T"); |
| 176 | assert_eq!(dump(&Value::Int(0)), b"\x04\x08i\x00"); |
| 177 | assert_eq!(dump(&Value::Int(4)), b"\x04\x08i\x09"); |
| 178 | assert_eq!(dump(&Value::Int(-1)), b"\x04\x08i\xfa"); |
| 179 | assert_eq!(dump(&Value::Int(123)), b"\x04\x08i\x01\x7b"); |
| 180 | assert_eq!(dump(&Value::Int(256)), b"\x04\x08i\x02\x00\x01"); |
| 181 | assert_eq!(dump(&Value::Int(-124)), b"\x04\x08i\xff\x84"); |
| 182 | assert_eq!(dump(&Value::Int(-256)), b"\x04\x08i\xff\x00"); |
| 183 | assert_eq!(dump(&Value::Int(-257)), b"\x04\x08i\xfe\xff\xfe"); |
| 184 | assert_eq!(dump(&Value::str("hi")), b"\x04\x08I\"\x07hi\x06:\x06ET"); |
| 185 | assert_eq!(dump(&Value::Bytes(b"hi".to_vec())), b"\x04\x08\"\x07hi"); |
| 186 | // The second `:a` is a link to the first. |
| 187 | assert_eq!( |
| 188 | dump(&Value::Array(vec![Value::Symbol("a".into()), Value::Symbol("a".into())])), |
| 189 | b"\x04\x08[\x07:\x06a;\x00" |
| 190 | ); |
| 191 | assert_eq!(dump(&Value::Hash(vec![(Value::Int(1), Value::Nil)])), b"\x04\x08{\x06i\x060"); |
| 192 | // Gem::Version.new("1.0") |
| 193 | assert_eq!( |
| 194 | dump(&Value::UserMarshal { class: "Gem::Version".into(), data: Box::new(Value::Array(vec![Value::str("1.0")])) }), |
| 195 | b"\x04\x08U:\x11Gem::Version[\x06I\"\x081.0\x06:\x06ET" |
| 196 | ); |
| 197 | assert_eq!( |
| 198 | dump(&Value::Object { class: "Point".into(), ivars: vec![("@x".into(), Value::Int(1))] }), |
| 199 | b"\x04\x08o:\x0aPoint\x06:\x07@xi\x06" |
| 200 | ); |
| 201 | assert_eq!(dump(&Value::UserDef { class: "X".into(), data: b"ab".to_vec() }), b"\x04\x08u:\x06X\x07ab"); |
| 202 | } |
| 203 | } |