g1t/services/packages/src/marshal.rs

203 lines7,374 bytesCodeBlame

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 work1//! 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
14use std::collections::HashMap;
15
16/// A Ruby value to marshal.
17#[derive(Clone, Debug, PartialEq)]
18pub 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
38impl 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.
50pub 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
56struct Writer {
57 out: Vec<u8>,
58 symbols: HashMap<String, usize>,
59}
60
61impl 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)]
168mod 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}