Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| RubyGems: the full index (specs.4.8.gz, latest and prerelease, quick/Marshal.4.8 specifications) in Ruby's Marshal format, so gem install --source and gem search work | 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 | } |