Skip to main content

znippy_common/plugins/native/
rpm_native.rs

1//! Native RPM plugin.
2//!
3//! An `.rpm` is a **Lead** (96 bytes) + a **signature header** + the **main
4//! header**, then a compressed cpio payload. The authoritative NEVRA — Name,
5//! Epoch, Version, Release, Arch — lives in the main header's **tag table**, NOT
6//! the filename: the filename omits `Epoch` entirely (`foo-1.2-3.x86_64.rpm` says
7//! nothing about `Epoch: 2`), and a renamed file would mislead a filename parser.
8//!
9//! The header is **uncompressed** (only the cpio payload is compressed), so
10//! extraction needs no decompressor — we parse the tag table directly. The data is
11//! attacker-influenced, so every read is bounds-checked; on any malformation we
12//! fall back to the filename (never panic, always return a row), exactly like the
13//! `gem`/`conda` native plugins.
14
15use crate::plugin::{ArchiveTypePlugin, ExtensionRow, ExtensionValue, HandlerCommand, HandlerMeta};
16use arrow::datatypes::{DataType, Field};
17use std::collections::HashMap;
18
19/// Native RPM plugin. NEVRA from the header tag table when the `.rpm` is parseable,
20/// else a best-effort `(name, version, release, arch)` split from the filename.
21pub struct RpmPlugin;
22
23/// RPM lead magic (`\xed\xab\xee\xdb`).
24const RPM_LEAD_MAGIC: [u8; 4] = [0xed, 0xab, 0xee, 0xdb];
25/// RPM header-structure magic (`\x8e\xad\xe8`), shared by the signature + main header.
26const RPM_HEADER_MAGIC: [u8; 3] = [0x8e, 0xad, 0xe8];
27
28// Main-header tag ids.
29const RPMTAG_NAME: u32 = 1000;
30const RPMTAG_VERSION: u32 = 1001;
31const RPMTAG_RELEASE: u32 = 1002;
32const RPMTAG_EPOCH: u32 = 1003;
33const RPMTAG_SUMMARY: u32 = 1004;
34const RPMTAG_VENDOR: u32 = 1011;
35const RPMTAG_LICENSE: u32 = 1014;
36const RPMTAG_URL: u32 = 1020;
37const RPMTAG_ARCH: u32 = 1022;
38const RPMTAG_SOURCERPM: u32 = 1044;
39const RPMTAG_PROVIDENAME: u32 = 1047;
40const RPMTAG_REQUIRENAME: u32 = 1049;
41
42// Index-entry value types.
43const RPM_TYPE_INT32: u32 = 4;
44const RPM_TYPE_STRING: u32 = 6;
45const RPM_TYPE_STRING_ARRAY: u32 = 8;
46const RPM_TYPE_I18NSTRING: u32 = 9;
47
48/// Metadata parsed out of an RPM main header — NEVRA plus the rpm-md `primary.xml`
49/// fields (summary / license / url / vendor / sourcerpm + the provide/require
50/// dependency names). All optional: a field absent from the header stays `None`/`[]`.
51#[derive(Debug, Default, Clone, PartialEq, Eq)]
52pub(crate) struct RpmMeta {
53    pub name: Option<String>,
54    pub epoch: Option<u32>,
55    pub version: Option<String>,
56    pub release: Option<String>,
57    pub arch: Option<String>,
58    pub summary: Option<String>,
59    pub license: Option<String>,
60    pub url: Option<String>,
61    pub vendor: Option<String>,
62    pub sourcerpm: Option<String>,
63    pub provides: Vec<String>,
64    pub requires: Vec<String>,
65}
66
67impl RpmPlugin {
68    fn be_u32(b: &[u8], off: usize) -> Option<u32> {
69        let s = b.get(off..off.checked_add(4)?)?;
70        Some(u32::from_be_bytes([s[0], s[1], s[2], s[3]]))
71    }
72
73    /// Parse a header structure at `pos`. Returns
74    /// `(entries_offset, nindex, data_offset, data_len, total_len)` where
75    /// `total_len` is the full byte length (16-byte intro + index + data). `None`
76    /// if the magic is wrong or the header runs past the buffer.
77    fn header_extent(b: &[u8], pos: usize) -> Option<(usize, usize, usize, usize, usize)> {
78        // intro = 3 magic + 1 version + 4 reserved + 4 nindex + 4 hsize = 16 bytes
79        if b.get(pos..pos.checked_add(3)?)? != RPM_HEADER_MAGIC {
80            return None;
81        }
82        let nindex = Self::be_u32(b, pos + 8)? as usize;
83        let hsize = Self::be_u32(b, pos + 12)? as usize;
84        let index_bytes = nindex.checked_mul(16)?;
85        let entries_off = pos.checked_add(16)?;
86        let data_off = entries_off.checked_add(index_bytes)?;
87        let total = 16usize.checked_add(index_bytes)?.checked_add(hsize)?;
88        if pos.checked_add(total)? > b.len() {
89            return None;
90        }
91        Some((entries_off, nindex, data_off, hsize, total))
92    }
93
94    /// Parse the metadata out of an `.rpm`'s main header. `None` on any
95    /// malformation (or a header carrying no NAME/VERSION).
96    pub(crate) fn parse_meta(data: &[u8]) -> Option<RpmMeta> {
97        // 1. Lead magic.
98        if data.get(0..4)? != RPM_LEAD_MAGIC {
99            return None;
100        }
101        // 2. Signature header at offset 96; skip it. The main header that follows is
102        //    aligned up to an 8-byte boundary.
103        let (_e, _n, _d, _h, sig_total) = Self::header_extent(data, 96)?;
104        let after_sig = 96usize.checked_add(sig_total)?;
105        let main_pos = after_sig.checked_add(7)? & !7;
106        // 3. Main header: walk the index entries, reading values out of the store.
107        let (entries_off, nindex, data_off, hsize, _total) = Self::header_extent(data, main_pos)?;
108        let store = data.get(data_off..data_off.checked_add(hsize)?)?;
109        let mut m = RpmMeta::default();
110        for i in 0..nindex {
111            let e = entries_off.checked_add(i.checked_mul(16)?)?;
112            let tag = Self::be_u32(data, e)?;
113            let typ = Self::be_u32(data, e + 4)?;
114            let off = Self::be_u32(data, e + 8)? as usize;
115            let count = Self::be_u32(data, e + 12)? as usize;
116            match tag {
117                RPMTAG_NAME if typ == RPM_TYPE_STRING => m.name = read_cstr(store, off),
118                RPMTAG_VERSION if typ == RPM_TYPE_STRING => m.version = read_cstr(store, off),
119                RPMTAG_RELEASE if typ == RPM_TYPE_STRING => m.release = read_cstr(store, off),
120                RPMTAG_ARCH if typ == RPM_TYPE_STRING => m.arch = read_cstr(store, off),
121                RPMTAG_LICENSE if typ == RPM_TYPE_STRING => m.license = read_cstr(store, off),
122                RPMTAG_URL if typ == RPM_TYPE_STRING => m.url = read_cstr(store, off),
123                RPMTAG_VENDOR if typ == RPM_TYPE_STRING => m.vendor = read_cstr(store, off),
124                RPMTAG_SOURCERPM if typ == RPM_TYPE_STRING => m.sourcerpm = read_cstr(store, off),
125                // SUMMARY is an I18NSTRING (one string per locale); the first is the
126                // C-locale default. Accept a plain STRING too, defensively.
127                RPMTAG_SUMMARY if typ == RPM_TYPE_I18NSTRING || typ == RPM_TYPE_STRING => {
128                    m.summary = read_cstr(store, off)
129                }
130                RPMTAG_EPOCH if typ == RPM_TYPE_INT32 => m.epoch = Self::be_u32(store, off),
131                RPMTAG_PROVIDENAME if typ == RPM_TYPE_STRING_ARRAY => {
132                    m.provides = read_string_array(store, off, count)
133                }
134                RPMTAG_REQUIRENAME if typ == RPM_TYPE_STRING_ARRAY => {
135                    m.requires = read_string_array(store, off, count)
136                }
137                _ => {}
138            }
139        }
140        // A real main header carries at least NAME + VERSION.
141        (m.name.is_some() && m.version.is_some()).then_some(m)
142    }
143
144    /// Best-effort `(name, version, release, arch)` from a canonical NEVRA filename
145    /// `{name}-{version}-{release}.{arch}.rpm` (no epoch — that only lives in the
146    /// header). The fallback when the `.rpm` can't be parsed.
147    fn parse_filename(path: &str) -> (String, Option<String>, Option<String>, Option<String>) {
148        let fname = path.rsplit('/').next().unwrap_or(path);
149        let Some(stem) = fname.strip_suffix(".rpm") else {
150            return (fname.to_string(), None, None, None);
151        };
152        let Some((rest, arch)) = stem.rsplit_once('.') else {
153            return (stem.to_string(), None, None, None);
154        };
155        let Some((name_ver, release)) = rest.rsplit_once('-') else {
156            return (stem.to_string(), None, None, Some(arch.to_string()));
157        };
158        let Some((name, version)) = name_ver.rsplit_once('-') else {
159            return (rest.to_string(), None, None, Some(arch.to_string()));
160        };
161        (
162            name.to_string(),
163            Some(version.to_string()),
164            Some(release.to_string()),
165            Some(arch.to_string()),
166        )
167    }
168
169}
170
171/// Read a NUL-terminated UTF-8(-lossy) string from `store` at `off`.
172fn read_cstr(store: &[u8], off: usize) -> Option<String> {
173    let s = store.get(off..)?;
174    let end = s.iter().position(|&b| b == 0)?;
175    Some(String::from_utf8_lossy(&s[..end]).into_owned())
176}
177
178/// Read `count` consecutive NUL-terminated strings (an RPM `STRING_ARRAY`) from
179/// `store` at `off`. Bounded + bounds-checked: stops at the store end (a truncated
180/// array yields the strings read so far) and caps the iteration so a lying `count`
181/// can't spin.
182fn read_string_array(store: &[u8], off: usize, count: usize) -> Vec<String> {
183    let mut out = Vec::with_capacity(count.min(1024));
184    let mut pos = off;
185    for _ in 0..count.min(100_000) {
186        let Some(s) = store.get(pos..) else { break };
187        let Some(end) = s.iter().position(|&b| b == 0) else { break };
188        out.push(String::from_utf8_lossy(&s[..end]).into_owned());
189        pos = pos.saturating_add(end + 1);
190    }
191    out
192}
193
194/// Join a dependency-name list into a newline-separated column value (`None` when
195/// empty), the shape [`RpmView`](crate::views::RpmView) splits back apart.
196fn join_lines(v: &[String]) -> Option<String> {
197    (!v.is_empty()).then(|| v.join("\n"))
198}
199
200impl ArchiveTypePlugin for RpmPlugin {
201    fn name(&self) -> &str {
202        "rpm"
203    }
204
205    fn type_id(&self) -> i8 {
206        8
207    }
208
209    fn meta(&self) -> HandlerMeta {
210        HandlerMeta {
211            name: "rpm".into(),
212            aliases: vec!["yum".into(), "dnf".into(), "redhat".into()],
213            type_id: 8,
214            ecosystem: "RPM / Red Hat family (dnf/yum)".into(),
215            extensions: vec![".rpm".into()],
216            description: "RPM packages — authoritative NEVRA (incl. epoch) from the header tag table"
217                .into(),
218            commands: vec![HandlerCommand::new(
219                "coords",
220                "Print rpm name + version (header if readable, else filename)",
221            )],
222        }
223    }
224
225    fn run_command(&self, cmd: &str, args: &[String]) -> anyhow::Result<()> {
226        match cmd {
227            "coords" => {
228                let path =
229                    args.first().ok_or_else(|| anyhow::anyhow!("usage: rpm coords <file.rpm>"))?;
230                let (name, version, _r, _a) = Self::parse_filename(path);
231                match version {
232                    Some(v) => println!("{} {}", name, v),
233                    None => println!("{}", name),
234                }
235                Ok(())
236            }
237            other => anyhow::bail!("rpm: unknown subcommand '{}'", other),
238        }
239    }
240
241    fn matches_path(&self, path: &str) -> bool {
242        path.ends_with(".rpm")
243    }
244
245    /// Columns the read-side [`RpmView`](crate::views::RpmView) resolves on: the
246    /// NEVRA (`epoch` is the field the filename can't supply) plus the rpm-md
247    /// `primary.xml` metadata — summary/license/url/vendor/sourcerpm and the
248    /// newline-joined provide/require dependency names.
249    fn schema_fields(&self) -> Vec<Field> {
250        ["name", "version", "release", "arch", "epoch", "summary", "license", "url", "vendor",
251         "sourcerpm", "provides", "requires"]
252            .iter()
253            .map(|n| Field::new(*n, DataType::Utf8, true))
254            .collect()
255    }
256
257    fn extract_metadata(&self, path: &str, data: &[u8]) -> Option<ExtensionRow> {
258        // Authoritative header first; on any parse failure fall back to the
259        // filename NEVRA (no rich metadata, exactly as before).
260        let m = Self::parse_meta(data);
261        let mut fields = HashMap::new();
262        let put = |fields: &mut HashMap<String, ExtensionValue>, k: &str, v: Option<String>| {
263            fields.insert(k.to_string(), ExtensionValue::OptStr(v));
264        };
265        match m {
266            Some(m) => {
267                fields.insert("name".into(), ExtensionValue::Str(m.name.unwrap_or_default()));
268                put(&mut fields, "version", m.version);
269                put(&mut fields, "release", m.release);
270                put(&mut fields, "arch", m.arch);
271                put(&mut fields, "epoch", m.epoch.map(|e| e.to_string()));
272                put(&mut fields, "summary", m.summary);
273                put(&mut fields, "license", m.license);
274                put(&mut fields, "url", m.url);
275                put(&mut fields, "vendor", m.vendor);
276                put(&mut fields, "sourcerpm", m.sourcerpm);
277                put(&mut fields, "provides", join_lines(&m.provides));
278                put(&mut fields, "requires", join_lines(&m.requires));
279            }
280            None => {
281                let (name, version, release, arch) = Self::parse_filename(path);
282                fields.insert("name".into(), ExtensionValue::Str(name));
283                put(&mut fields, "version", version);
284                put(&mut fields, "release", release);
285                put(&mut fields, "arch", arch);
286                for k in ["epoch", "summary", "license", "url", "vendor", "sourcerpm", "provides", "requires"] {
287                    put(&mut fields, k, None);
288                }
289            }
290        }
291        Some(ExtensionRow { fields })
292    }
293}
294
295#[cfg(test)]
296mod tests {
297    use super::*;
298
299    /// Build a minimal RPM header structure (intro + index entries + data store).
300    fn build_header(entries: &[(u32, u32, u32, u32)], store: &[u8]) -> Vec<u8> {
301        let mut h = Vec::new();
302        h.extend_from_slice(&RPM_HEADER_MAGIC);
303        h.push(0x01); // version
304        h.extend_from_slice(&[0, 0, 0, 0]); // reserved
305        h.extend_from_slice(&(entries.len() as u32).to_be_bytes());
306        h.extend_from_slice(&(store.len() as u32).to_be_bytes());
307        for (tag, typ, off, count) in entries {
308            h.extend_from_slice(&tag.to_be_bytes());
309            h.extend_from_slice(&typ.to_be_bytes());
310            h.extend_from_slice(&off.to_be_bytes());
311            h.extend_from_slice(&count.to_be_bytes());
312        }
313        h.extend_from_slice(store);
314        h
315    }
316
317    /// A whole synthetic `.rpm`: lead + (empty) signature header + main header.
318    fn build_rpm(nevra_entries: &[(u32, u32, u32, u32)], store: &[u8]) -> Vec<u8> {
319        let mut rpm = vec![0u8; 96];
320        rpm[0..4].copy_from_slice(&RPM_LEAD_MAGIC);
321        // Empty signature header (0 index entries, 0 data).
322        let sig = build_header(&[], &[]);
323        rpm.extend_from_slice(&sig);
324        // Pad to an 8-byte boundary before the main header.
325        while rpm.len() % 8 != 0 {
326            rpm.push(0);
327        }
328        rpm.extend_from_slice(&build_header(nevra_entries, store));
329        rpm
330    }
331
332    #[test]
333    fn parses_real_nevra_including_epoch_from_header() {
334        // store: "bash\0" @0, "5.1.8\0" @5, "1.el9\0" @11, "x86_64\0" @17
335        let mut store = Vec::new();
336        let name_off = store.len() as u32;
337        store.extend_from_slice(b"bash\0");
338        let ver_off = store.len() as u32;
339        store.extend_from_slice(b"5.1.8\0");
340        let rel_off = store.len() as u32;
341        store.extend_from_slice(b"1.el9\0");
342        let arch_off = store.len() as u32;
343        store.extend_from_slice(b"x86_64\0");
344        let epoch_off = store.len() as u32;
345        store.extend_from_slice(&2u32.to_be_bytes()); // EPOCH = 2 (INT32)
346
347        let entries = [
348            (RPMTAG_NAME, RPM_TYPE_STRING, name_off, 1),
349            (RPMTAG_VERSION, RPM_TYPE_STRING, ver_off, 1),
350            (RPMTAG_RELEASE, RPM_TYPE_STRING, rel_off, 1),
351            (RPMTAG_ARCH, RPM_TYPE_STRING, arch_off, 1),
352            (RPMTAG_EPOCH, RPM_TYPE_INT32, epoch_off, 1),
353        ];
354        let rpm = build_rpm(&entries, &store);
355        let n = RpmPlugin::parse_meta(&rpm).expect("parses");
356        assert_eq!(n.name.as_deref(), Some("bash"));
357        assert_eq!(n.version.as_deref(), Some("5.1.8"));
358        assert_eq!(n.release.as_deref(), Some("1.el9"));
359        assert_eq!(n.arch.as_deref(), Some("x86_64"));
360        assert_eq!(n.epoch, Some(2), "epoch comes from the header — the filename omits it");
361    }
362
363    #[test]
364    fn falls_back_to_filename_when_not_an_rpm() {
365        // Bytes without the lead magic ⇒ parse_nevra None ⇒ filename fallback.
366        assert!(RpmPlugin::parse_meta(b"not an rpm").is_none());
367        let row = RpmPlugin
368            .extract_metadata("Packages/zlib-1.2.11-31.el9.x86_64.rpm", b"garbage")
369            .expect("row");
370        assert_eq!(row.fields.get("name"), Some(&ExtensionValue::Str("zlib".into())));
371        assert_eq!(
372            row.fields.get("version"),
373            Some(&ExtensionValue::OptStr(Some("1.2.11".into())))
374        );
375        assert_eq!(
376            row.fields.get("arch"),
377            Some(&ExtensionValue::OptStr(Some("x86_64".into())))
378        );
379        // No epoch recoverable from a filename.
380        assert_eq!(row.fields.get("epoch"), Some(&ExtensionValue::OptStr(None)));
381    }
382
383    #[test]
384    fn extract_prefers_header_over_filename() {
385        // A header that says epoch=7/version=9.9 while the filename says version=1.0:
386        // the header wins.
387        let mut store = Vec::new();
388        store.extend_from_slice(b"curl\0"); // name @0
389        let ver_off = store.len() as u32;
390        store.extend_from_slice(b"9.9\0"); // version
391        let epoch_off = store.len() as u32;
392        store.extend_from_slice(&7u32.to_be_bytes());
393        let entries = [
394            (RPMTAG_NAME, RPM_TYPE_STRING, 0, 1),
395            (RPMTAG_VERSION, RPM_TYPE_STRING, ver_off, 1),
396            (RPMTAG_EPOCH, RPM_TYPE_INT32, epoch_off, 1),
397        ];
398        let rpm = build_rpm(&entries, &store);
399        let row = RpmPlugin.extract_metadata("Packages/curl-1.0-1.noarch.rpm", &rpm).unwrap();
400        assert_eq!(row.fields.get("name"), Some(&ExtensionValue::Str("curl".into())));
401        assert_eq!(row.fields.get("version"), Some(&ExtensionValue::OptStr(Some("9.9".into()))));
402        assert_eq!(row.fields.get("epoch"), Some(&ExtensionValue::OptStr(Some("7".into()))));
403    }
404
405    #[test]
406    fn matches_rpm_only() {
407        assert!(RpmPlugin.matches_path("Packages/foo-1.0-1.x86_64.rpm"));
408        assert!(!RpmPlugin.matches_path("foo.deb"));
409    }
410
411    #[test]
412    fn schema_has_nevra_and_primary_columns() {
413        let f = RpmPlugin.schema_fields();
414        let names: Vec<&str> = f.iter().map(|x| x.name().as_str()).collect();
415        assert_eq!(
416            names,
417            vec![
418                "name", "version", "release", "arch", "epoch", "summary", "license", "url",
419                "vendor", "sourcerpm", "provides", "requires"
420            ]
421        );
422    }
423
424    #[test]
425    fn extracts_rich_primary_fields() {
426        // A header carrying the rpm-md primary fields — summary (I18NSTRING),
427        // license/url/sourcerpm (STRING), and provide/require names (STRING_ARRAY).
428        let mut store = Vec::new();
429        store.extend_from_slice(b"curl\0"); // name @0
430        let ver = store.len() as u32;
431        store.extend_from_slice(b"8.0\0");
432        let sum = store.len() as u32;
433        store.extend_from_slice(b"A URL transfer tool\0");
434        let lic = store.len() as u32;
435        store.extend_from_slice(b"MIT\0");
436        let url = store.len() as u32;
437        store.extend_from_slice(b"https://curl.se\0");
438        let src = store.len() as u32;
439        store.extend_from_slice(b"curl-8.0-1.src.rpm\0");
440        let prov = store.len() as u32;
441        store.extend_from_slice(b"curl\0libcurl\0"); // 2 strings
442        let req = store.len() as u32;
443        store.extend_from_slice(b"/bin/sh\0libc.so.6\0"); // 2 strings
444        let entries = [
445            (RPMTAG_NAME, RPM_TYPE_STRING, 0, 1),
446            (RPMTAG_VERSION, RPM_TYPE_STRING, ver, 1),
447            (RPMTAG_SUMMARY, RPM_TYPE_I18NSTRING, sum, 1),
448            (RPMTAG_LICENSE, RPM_TYPE_STRING, lic, 1),
449            (RPMTAG_URL, RPM_TYPE_STRING, url, 1),
450            (RPMTAG_SOURCERPM, RPM_TYPE_STRING, src, 1),
451            (RPMTAG_PROVIDENAME, RPM_TYPE_STRING_ARRAY, prov, 2),
452            (RPMTAG_REQUIRENAME, RPM_TYPE_STRING_ARRAY, req, 2),
453        ];
454        let rpm = build_rpm(&entries, &store);
455        let m = RpmPlugin::parse_meta(&rpm).unwrap();
456        assert_eq!(m.summary.as_deref(), Some("A URL transfer tool"));
457        assert_eq!(m.license.as_deref(), Some("MIT"));
458        assert_eq!(m.url.as_deref(), Some("https://curl.se"));
459        assert_eq!(m.sourcerpm.as_deref(), Some("curl-8.0-1.src.rpm"));
460        assert_eq!(m.provides, vec!["curl", "libcurl"]);
461        assert_eq!(m.requires, vec!["/bin/sh", "libc.so.6"]);
462        // extract_metadata surfaces them as columns (dep names newline-joined).
463        let row = RpmPlugin.extract_metadata("x.rpm", &rpm).unwrap();
464        assert_eq!(row.fields.get("license"), Some(&ExtensionValue::OptStr(Some("MIT".into()))));
465        assert_eq!(
466            row.fields.get("provides"),
467            Some(&ExtensionValue::OptStr(Some("curl\nlibcurl".into())))
468        );
469        assert_eq!(
470            row.fields.get("requires"),
471            Some(&ExtensionValue::OptStr(Some("/bin/sh\nlibc.so.6".into())))
472        );
473    }
474
475    #[test]
476    fn malformed_headers_never_panic() {
477        // Truncated / lying-length inputs must return None, not panic.
478        for bad in [
479            vec![0xed, 0xab, 0xee, 0xdb], // lead magic only
480            {
481                let mut v = vec![0u8; 96];
482                v[0..4].copy_from_slice(&RPM_LEAD_MAGIC);
483                v.extend_from_slice(&RPM_HEADER_MAGIC); // sig magic, then truncated
484                v
485            },
486            {
487                // sig header claims a huge nindex
488                let mut v = vec![0u8; 96];
489                v[0..4].copy_from_slice(&RPM_LEAD_MAGIC);
490                v.extend_from_slice(&RPM_HEADER_MAGIC);
491                v.push(1);
492                v.extend_from_slice(&[0, 0, 0, 0]);
493                v.extend_from_slice(&u32::MAX.to_be_bytes()); // nindex = huge
494                v.extend_from_slice(&u32::MAX.to_be_bytes()); // hsize = huge
495                v
496            },
497        ] {
498            assert!(RpmPlugin::parse_meta(&bad).is_none());
499        }
500    }
501}