|
| 1 | +//! Reading proc-macro rustc version information from binary data |
| 2 | +
|
| 3 | +use std::{ |
| 4 | + fs::File, |
| 5 | + io::{self, Read}, |
| 6 | + path::Path, |
| 7 | +}; |
| 8 | + |
| 9 | +use memmap::Mmap; |
| 10 | +use object::read::{File as BinaryFile, Object, ObjectSection}; |
| 11 | +use snap::read::FrameDecoder as SnapDecoder; |
| 12 | + |
| 13 | +#[derive(Debug)] |
| 14 | +pub(crate) struct RustCInfo { |
| 15 | + pub(crate) version: (usize, usize, usize), |
| 16 | + pub(crate) channel: String, |
| 17 | + pub(crate) commit: String, |
| 18 | + pub(crate) date: String, |
| 19 | +} |
| 20 | + |
| 21 | +pub(crate) fn read_info(dylib_path: &Path) -> io::Result<RustCInfo> { |
| 22 | + macro_rules! err { |
| 23 | + ($e:literal) => { |
| 24 | + io::Error::new(io::ErrorKind::InvalidData, $e) |
| 25 | + }; |
| 26 | + } |
| 27 | + |
| 28 | + let ver_str = read_version(dylib_path)?; |
| 29 | + let mut items = ver_str.split_whitespace(); |
| 30 | + let tag = items.next().ok_or(err!("version format error"))?; |
| 31 | + if tag != "rustc" { |
| 32 | + return Err(err!("version format error (No rustc tag)")); |
| 33 | + } |
| 34 | + |
| 35 | + let version_part = items.next().ok_or(err!("no version string"))?; |
| 36 | + let mut version_parts = version_part.split("-"); |
| 37 | + let version = version_parts.next().ok_or(err!("no version"))?; |
| 38 | + let channel = version_parts.next().unwrap_or_default().to_string(); |
| 39 | + |
| 40 | + let commit = items.next().ok_or(err!("no commit info"))?; |
| 41 | + // remove ( |
| 42 | + if commit.len() == 0 { |
| 43 | + return Err(err!("commit format error")); |
| 44 | + } |
| 45 | + let commit = commit[1..].to_string(); |
| 46 | + let date = items.next().ok_or(err!("no date info"))?; |
| 47 | + // remove ) |
| 48 | + if date.len() == 0 { |
| 49 | + return Err(err!("date format error")); |
| 50 | + } |
| 51 | + let date = date[0..date.len() - 2].to_string(); |
| 52 | + |
| 53 | + let version_numbers = version |
| 54 | + .split(".") |
| 55 | + .map(|it| it.parse::<usize>()) |
| 56 | + .collect::<Result<Vec<_>, _>>() |
| 57 | + .map_err(|_| err!("version number error"))?; |
| 58 | + |
| 59 | + if version_numbers.len() != 3 { |
| 60 | + return Err(err!("version number format error")); |
| 61 | + } |
| 62 | + let version = (version_numbers[0], version_numbers[1], version_numbers[2]); |
| 63 | + |
| 64 | + Ok(RustCInfo { version, channel, commit, date }) |
| 65 | +} |
| 66 | + |
| 67 | +/// This is used inside read_version() to locate the ".rustc" section |
| 68 | +/// from a proc macro crate's binary file. |
| 69 | +fn read_section<'a>(dylib_binary: &'a [u8], section_name: &str) -> io::Result<&'a [u8]> { |
| 70 | + BinaryFile::parse(dylib_binary) |
| 71 | + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))? |
| 72 | + .section_by_name(section_name) |
| 73 | + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "section read error"))? |
| 74 | + .data() |
| 75 | + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e)) |
| 76 | +} |
| 77 | + |
| 78 | +/// Check the version of rustc that was used to compile a proc macro crate's |
| 79 | +/// |
| 80 | +/// binary file. |
| 81 | +/// A proc macro crate binary's ".rustc" section has following byte layout: |
| 82 | +/// * [b'r',b'u',b's',b't',0,0,0,5] is the first 8 bytes |
| 83 | +/// * ff060000 734e6150 is followed, it's the snappy format magic bytes, |
| 84 | +/// means bytes from here(including this sequence) are compressed in |
| 85 | +/// snappy compression format. Version info is inside here, so decompress |
| 86 | +/// this. |
| 87 | +/// The bytes you get after decompressing the snappy format portion has |
| 88 | +/// following layout: |
| 89 | +/// * [b'r',b'u',b's',b't',0,0,0,5] is the first 8 bytes(again) |
| 90 | +/// * [crate root bytes] next 4 bytes is to store crate root position, |
| 91 | +/// according to rustc's source code comment |
| 92 | +/// * [length byte] next 1 byte tells us how many bytes we should read next |
| 93 | +/// for the version string's utf8 bytes |
| 94 | +/// * [version string bytes encoded in utf8] <- GET THIS BOI |
| 95 | +/// * [some more bytes that we don really care but still there] :-) |
| 96 | +/// Check this issue for more about the bytes layout: |
| 97 | +/// https://github.com/rust-analyzer/rust-analyzer/issues/6174 |
| 98 | +fn read_version(dylib_path: &Path) -> io::Result<String> { |
| 99 | + let dylib_file = File::open(dylib_path)?; |
| 100 | + let dylib_mmaped = unsafe { Mmap::map(&dylib_file) }?; |
| 101 | + |
| 102 | + let dot_rustc = read_section(&dylib_mmaped, ".rustc")?; |
| 103 | + |
| 104 | + let header = &dot_rustc[..8]; |
| 105 | + const EXPECTED_HEADER: [u8; 8] = [b'r', b'u', b's', b't', 0, 0, 0, 5]; |
| 106 | + // check if header is valid |
| 107 | + if header != EXPECTED_HEADER { |
| 108 | + return Err(io::Error::new( |
| 109 | + io::ErrorKind::InvalidData, |
| 110 | + format!("only metadata version 5 is supported, section header was: {:?}", header), |
| 111 | + )); |
| 112 | + } |
| 113 | + |
| 114 | + let snappy_portion = &dot_rustc[8..]; |
| 115 | + |
| 116 | + let mut snappy_decoder = SnapDecoder::new(snappy_portion); |
| 117 | + |
| 118 | + // the bytes before version string bytes, so this basically is: |
| 119 | + // 8 bytes for [b'r',b'u',b's',b't',0,0,0,5] |
| 120 | + // 4 bytes for [crate root bytes] |
| 121 | + // 1 byte for length of version string |
| 122 | + // so 13 bytes in total, and we should check the 13th byte |
| 123 | + // to know the length |
| 124 | + let mut bytes_before_version = [0u8; 13]; |
| 125 | + snappy_decoder.read_exact(&mut bytes_before_version)?; |
| 126 | + let length = bytes_before_version[12]; // what? can't use -1 indexing? |
| 127 | + |
| 128 | + let mut version_string_utf8 = vec![0u8; length as usize]; |
| 129 | + snappy_decoder.read_exact(&mut version_string_utf8)?; |
| 130 | + let version_string = String::from_utf8(version_string_utf8); |
| 131 | + version_string.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e)) |
| 132 | +} |
0 commit comments